From 52f9e212b9607d8ae608e10a09753f04c11d9ed7 Mon Sep 17 00:00:00 2001 From: SepComet <2428390463@qq.com> Date: Tue, 10 Mar 2026 13:11:30 +0800 Subject: [PATCH] Start Tag System --- AGENTS.md | 2 +- .../Scripts/Components/IDamageReceiver.cs | 2 +- .../Scripts/Components/MovementComponent.cs | 8 +- .../Scripts/Components/ShooterBullet.cs | 11 +- .../Scripts/Components/ShooterMuzzleComp.cs | 47 ++- .../Scripts/Components/TowerController.cs | 46 ++- .../Definition/DataStruct/AttackPayload.cs | 23 ++ .../DataStruct/AttackPayload.cs.meta | 11 + .../Definition/DataStruct/HitContext.cs | 13 + .../Definition/DataStruct/HitContext.cs.meta | 11 + .../Scripts/Definition/Enum/TagCategory.cs | 10 + .../Definition/Enum/TagCategory.cs.meta | 11 + .../Definition/Enum/TagTriggerPhase.cs | 11 + .../Definition/Enum/TagTriggerPhase.cs.meta | 11 + Assets/GameMain/Scripts/Definition/Event.meta | 3 + .../Definition/{ => Event}/EventEffect.meta | 0 .../{ => Event}/EventEffect/AddGoldEffect.cs | 0 .../EventEffect/AddGoldEffect.cs.meta | 0 .../EventEffect/AddRandomCompsEffect.cs | 0 .../EventEffect/AddRandomCompsEffect.cs.meta | 0 .../DamageRandomTowerEnduranceEffect.cs | 0 .../DamageRandomTowerEnduranceEffect.cs.meta | 0 .../EventEffect/EventEffectBase.cs | 0 .../EventEffect/EventEffectBase.cs.meta | 0 .../EventEffect/RemoveRandomCompEffect.cs | 0 .../RemoveRandomCompEffect.cs.meta | 0 .../{ => Event}/EventRequirement.meta | 0 .../CompCountAtLeastRequirement.cs | 0 .../CompCountAtLeastRequirement.cs.meta | 0 .../EventRequirement/EventRequirementBase.cs | 0 .../EventRequirementBase.cs.meta | 0 .../GoldAtLeastRequirement.cs | 0 .../GoldAtLeastRequirement.cs.meta | 0 .../EventRequirement/HasRelicRequirement.cs | 0 .../HasRelicRequirement.cs.meta | 0 .../TowerCountAtLeastRequirement.cs | 0 .../TowerCountAtLeastRequirement.cs.meta | 0 Assets/GameMain/Scripts/Definition/Tag.meta | 3 + .../Definition/Tag/EnemyStatusTagEffect.meta | 3 + .../AbsoluteZeroTagEffect.cs | 9 + .../AbsoluteZeroTagEffect.cs.meta | 3 + .../EnemyStatusTagEffectBase.cs | 32 ++ .../EnemyStatusTagEffectBase.cs.meta | 3 + .../Tag/EnemyStatusTagEffect/FireTagEffect.cs | 68 +++++ .../FireTagEffect.cs.meta | 3 + .../IEnemyStatusTagEffect.cs | 13 + .../IEnemyStatusTagEffect.cs.meta | 3 + .../Tag/EnemyStatusTagEffect/IceTagEffect.cs | 50 ++++ .../EnemyStatusTagEffect/IceTagEffect.cs.meta | 3 + .../EnemyStatusTagEffect/InfernoTagEffect.cs | 7 + .../InfernoTagEffect.cs.meta | 3 + .../Definition/Tag/EnemyStatusTagRegistry.cs | 26 ++ .../Tag/EnemyStatusTagRegistry.cs.meta | 11 + .../Scripts/Definition/Tag/TagConfig.meta | 3 + .../Tag/TagConfig/AbsoluteZeroTagConfig.cs | 15 + .../TagConfig/AbsoluteZeroTagConfig.cs.meta | 3 + .../Tag/TagConfig/BurnSpreadTagConfig.cs | 15 + .../Tag/TagConfig/BurnSpreadTagConfig.cs.meta | 3 + .../Definition/Tag/TagConfig/CritTagConfig.cs | 15 + .../Tag/TagConfig/CritTagConfig.cs.meta | 3 + .../Tag/TagConfig/ExecutionTagConfig.cs | 15 + .../Tag/TagConfig/ExecutionTagConfig.cs.meta | 3 + .../Definition/Tag/TagConfig/FireTagConfig.cs | 15 + .../Tag/TagConfig/FireTagConfig.cs.meta | 3 + .../Tag/TagConfig/FreezeMaskTagConfig.cs | 14 + .../Tag/TagConfig/FreezeMaskTagConfig.cs.meta | 3 + .../Definition/Tag/TagConfig/IceTagConfig.cs | 16 + .../Tag/TagConfig/IceTagConfig.cs.meta | 3 + .../Tag/TagConfig/IgniteBurstTagConfig.cs | 15 + .../TagConfig/IgniteBurstTagConfig.cs.meta | 3 + .../Tag/TagConfig/InfernoTagConfig.cs | 15 + .../Tag/TagConfig/InfernoTagConfig.cs.meta | 3 + .../Tag/TagConfig/OverpenetrateTagConfig.cs | 15 + .../TagConfig/OverpenetrateTagConfig.cs.meta | 3 + .../Tag/TagConfig/PierceTagConfig.cs | 14 + .../Tag/TagConfig/PierceTagConfig.cs.meta | 3 + .../Tag/TagConfig/ShatterTagConfig.cs | 15 + .../Tag/TagConfig/ShatterTagConfig.cs.meta | 3 + .../Definition/Tag/TagConfig/TagConfigBase.cs | 15 + .../Tag/TagConfig/TagConfigBase.cs.meta | 11 + .../Definition/Tag/TagConfigRegistry.cs | 103 +++++++ .../Definition/Tag/TagConfigRegistry.cs.meta | 11 + .../Definition/Tag/TagDefinitionData.cs | 13 + .../Definition/Tag/TagDefinitionData.cs.meta | 11 + .../Definition/Tag/TagEffectHandler.meta | 3 + .../AttackShapeTagEffectHandler.cs | 41 +++ .../AttackShapeTagEffectHandler.cs.meta | 11 + .../NumericTagEffectHandler.cs | 107 +++++++ .../NumericTagEffectHandler.cs.meta | 11 + .../Definition/Tag/TagEffectResolver.cs | 99 +++++++ .../Definition/Tag/TagEffectResolver.cs.meta | 11 + .../Scripts/Definition/Tag/TagStates.meta | 8 + .../Tag/TagStates/EnemyStatusTagStateBase.cs | 7 + .../TagStates/EnemyStatusTagStateBase.cs.meta | 3 + .../Definition/Tag/TagStates/FireTagState.cs | 9 + .../Tag/TagStates/FireTagState.cs.meta | 3 + .../Definition/Tag/TagStates/IceTagState.cs | 8 + .../Tag/TagStates/IceTagState.cs.meta | 3 + .../Scripts/Entity/EntityData/BulletData.cs | 21 +- .../Scripts/Entity/EntityLogic/EnemyEntity.cs | 64 +++- .../EntityLogic/EnemyTagStatusRuntime.cs | 94 ++++++ .../EntityLogic/EnemyTagStatusRuntime.cs.meta | 11 + .../Scene/Map/TowerPlacementService.cs | 7 + .../Tests/EditMode/TagCombatRuntimeTests.cs | 277 ++++++++++++++++++ .../EditMode/TagCombatRuntimeTests.cs.meta | 11 + docs/CodeX-TODO.md | 47 ++- docs/TagSystemDesign.md | 202 +++++++++++++ 107 files changed, 1850 insertions(+), 50 deletions(-) create mode 100644 Assets/GameMain/Scripts/Definition/DataStruct/AttackPayload.cs create mode 100644 Assets/GameMain/Scripts/Definition/DataStruct/AttackPayload.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/DataStruct/HitContext.cs create mode 100644 Assets/GameMain/Scripts/Definition/DataStruct/HitContext.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Enum/TagCategory.cs create mode 100644 Assets/GameMain/Scripts/Definition/Enum/TagCategory.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Enum/TagTriggerPhase.cs create mode 100644 Assets/GameMain/Scripts/Definition/Enum/TagTriggerPhase.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Event.meta rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/AddGoldEffect.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/AddGoldEffect.cs.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/AddRandomCompsEffect.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/AddRandomCompsEffect.cs.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/DamageRandomTowerEnduranceEffect.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/DamageRandomTowerEnduranceEffect.cs.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/EventEffectBase.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/EventEffectBase.cs.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/RemoveRandomCompEffect.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventEffect/RemoveRandomCompEffect.cs.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/CompCountAtLeastRequirement.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/CompCountAtLeastRequirement.cs.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/EventRequirementBase.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/EventRequirementBase.cs.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/GoldAtLeastRequirement.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/GoldAtLeastRequirement.cs.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/HasRelicRequirement.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/HasRelicRequirement.cs.meta (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/TowerCountAtLeastRequirement.cs (100%) rename Assets/GameMain/Scripts/Definition/{ => Event}/EventRequirement/TowerCountAtLeastRequirement.cs.meta (100%) create mode 100644 Assets/GameMain/Scripts/Definition/Tag.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/AbsoluteZeroTagEffect.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/AbsoluteZeroTagEffect.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/EnemyStatusTagEffectBase.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/EnemyStatusTagEffectBase.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IEnemyStatusTagEffect.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IEnemyStatusTagEffect.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/InfernoTagEffect.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/InfernoTagEffect.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagRegistry.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagRegistry.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/BurnSpreadTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/BurnSpreadTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/CritTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/CritTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/ExecutionTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/ExecutionTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/FreezeMaskTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/FreezeMaskTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/IceTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/IceTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/IgniteBurstTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/IgniteBurstTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/OverpenetrateTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/OverpenetrateTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/TagConfigBase.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfig/TagConfigBase.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/AttackShapeTagEffectHandler.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/AttackShapeTagEffectHandler.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagEffectResolver.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagEffectResolver.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagStates.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagStates/EnemyStatusTagStateBase.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagStates/EnemyStatusTagStateBase.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagStates/FireTagState.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagStates/FireTagState.cs.meta create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagStates/IceTagState.cs create mode 100644 Assets/GameMain/Scripts/Definition/Tag/TagStates/IceTagState.cs.meta create mode 100644 Assets/GameMain/Scripts/Entity/EntityLogic/EnemyTagStatusRuntime.cs create mode 100644 Assets/GameMain/Scripts/Entity/EntityLogic/EnemyTagStatusRuntime.cs.meta create mode 100644 Assets/Tests/EditMode/TagCombatRuntimeTests.cs create mode 100644 Assets/Tests/EditMode/TagCombatRuntimeTests.cs.meta diff --git a/AGENTS.md b/AGENTS.md index cb1fddf..0176bea 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,7 +20,7 @@ If uncertain, prefer **simple direct code** over generalized frameworks. - `Assets/GameMain/Configs/` and `Assets/GameMain/DataTables/` store runtime configuration and data tables. - `StreamingAssets/` is for runtime-loaded files that must be preserved on build. - `docs/` contains design notes (see `docs/GameDesign.md`). -- `���ݱ�/` is a top-level data table workspace; keep it in sync with `Assets/GameMain/DataTables/` when exporting. +- `数据表/` is a top-level data table workspace; keep it in sync with `Assets/GameMain/DataTables/` when exporting. ### Build, Test, and Development Commands - Open the project with Unity Hub and load `GeometryTD` as a Unity project. diff --git a/Assets/GameMain/Scripts/Components/IDamageReceiver.cs b/Assets/GameMain/Scripts/Components/IDamageReceiver.cs index d0067bc..e5f1e13 100644 --- a/Assets/GameMain/Scripts/Components/IDamageReceiver.cs +++ b/Assets/GameMain/Scripts/Components/IDamageReceiver.cs @@ -4,6 +4,6 @@ namespace Components { public interface IDamageReceiver { - void TakeDamage(int damage, AttackPropertyType attackPropertyType); + void TakeDamage(AttackPayload attackPayload); } } diff --git a/Assets/GameMain/Scripts/Components/MovementComponent.cs b/Assets/GameMain/Scripts/Components/MovementComponent.cs index 54180ec..44a4297 100644 --- a/Assets/GameMain/Scripts/Components/MovementComponent.cs +++ b/Assets/GameMain/Scripts/Components/MovementComponent.cs @@ -7,11 +7,13 @@ namespace Components private bool _isMoving = false; private Vector3 _direction = Vector3.forward; private float _speed = 0; + private float _speedMultiplier = 1f; private Transform _cachedTransform; public void OnInit(float speed, Transform transform) { _speed = speed; + _speedMultiplier = 1f; _cachedTransform = transform; } @@ -26,16 +28,18 @@ namespace Components public void OnReset() { _speed = 0; + _speedMultiplier = 1f; _cachedTransform = null; _isMoving = false; } private void Move(float deltaTime = 0) { - _cachedTransform.Translate(_direction * _speed * deltaTime); + _cachedTransform.Translate(_direction * (_speed * _speedMultiplier) * deltaTime); } public void SetMove(bool isMoving) => _isMoving = isMoving; public void SetDirection(Vector3 direction) => _direction = direction; + public void SetSpeedMultiplier(float speedMultiplier) => _speedMultiplier = Mathf.Max(0f, speedMultiplier); } -} \ No newline at end of file +} diff --git a/Assets/GameMain/Scripts/Components/ShooterBullet.cs b/Assets/GameMain/Scripts/Components/ShooterBullet.cs index 122d44d..63cdecb 100644 --- a/Assets/GameMain/Scripts/Components/ShooterBullet.cs +++ b/Assets/GameMain/Scripts/Components/ShooterBullet.cs @@ -13,8 +13,7 @@ namespace Components private Transform _target; private float _speed; - private int _damage; - private AttackPropertyType _attackPropertyType; + private AttackPayload _attackPayload; private float _lifetime; private float _runtimeMaxLifetime; private bool _isRunning; @@ -23,8 +22,7 @@ namespace Components public void OnShow(BulletData bulletData) { _target = bulletData != null ? bulletData.Target : null; - _damage = bulletData != null ? Mathf.Max(0, bulletData.Damage) : 0; - _attackPropertyType = bulletData != null ? bulletData.AttackPropertyType : AttackPropertyType.None; + _attackPayload = bulletData?.AttackPayload?.Clone() ?? new AttackPayload(); _speed = bulletData != null && bulletData.Speed > 0f ? bulletData.Speed : _defaultSpeed; _runtimeMaxLifetime = bulletData != null && bulletData.MaxLifetime > 0f ? bulletData.MaxLifetime : _maxLifetime; _lifetime = 0f; @@ -46,8 +44,7 @@ namespace Components { _target = null; _speed = 0f; - _damage = 0; - _attackPropertyType = AttackPropertyType.None; + _attackPayload = new AttackPayload(); _lifetime = 0f; _runtimeMaxLifetime = _maxLifetime; _isRunning = false; @@ -117,7 +114,7 @@ namespace Components { if (mono is IDamageReceiver damageReceiver) { - damageReceiver.TakeDamage(_damage, _attackPropertyType); + damageReceiver.TakeDamage(_attackPayload); break; } } diff --git a/Assets/GameMain/Scripts/Components/ShooterMuzzleComp.cs b/Assets/GameMain/Scripts/Components/ShooterMuzzleComp.cs index d8f9932..d8f8f52 100644 --- a/Assets/GameMain/Scripts/Components/ShooterMuzzleComp.cs +++ b/Assets/GameMain/Scripts/Components/ShooterMuzzleComp.cs @@ -20,14 +20,21 @@ namespace Components [SerializeField] private SpriteRenderer _renderer; + private TagRuntimeData[] _tagRuntimes = System.Array.Empty(); + public int AttackDamage => _attackDamage; public AttackMethodType AttackMethodType => _attackMethodType; - public void OnInit(int attackDamage, AttackMethodType attackMethodType = AttackMethodType.NormalBullet, int bulletTypeId = 501) + public void OnInit( + int attackDamage, + AttackMethodType attackMethodType = AttackMethodType.NormalBullet, + int bulletTypeId = 501, + TagRuntimeData[] tagRuntimes = null) { _attackDamage = Mathf.Max(1, attackDamage); _attackMethodType = attackMethodType; _bulletTypeId = Mathf.Max(1, bulletTypeId); + _tagRuntimes = CloneTagRuntimes(tagRuntimes); } public void OnReset() @@ -35,6 +42,7 @@ namespace Components _attackDamage = 1; _attackMethodType = AttackMethodType.None; _bulletTypeId = 501; + _tagRuntimes = System.Array.Empty(); } public void SetColor(Color color) @@ -64,16 +72,47 @@ namespace Components Transform spawnPoint = _muzzlePoint != null ? _muzzlePoint : transform; int bulletEntityId = GameEntry.Entity.GenerateSerialId(); + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = _attackDamage, + AttackPropertyType = attackPropertyType, + TagRuntimes = CloneTagRuntimes(_tagRuntimes) + }; BulletData bulletData = new BulletData( bulletEntityId, _bulletTypeId, spawnPoint.position, target, - _attackDamage, - _bulletSpeed, - attackPropertyType); + attackPayload, + _bulletSpeed); GameEntry.Entity.ShowBullet(bulletData); return true; } + + private static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] tagRuntimes) + { + if (tagRuntimes == null || tagRuntimes.Length <= 0) + { + return System.Array.Empty(); + } + + TagRuntimeData[] cloned = new TagRuntimeData[tagRuntimes.Length]; + for (int i = 0; i < tagRuntimes.Length; i++) + { + TagRuntimeData runtime = tagRuntimes[i]; + if (runtime == null) + { + continue; + } + + cloned[i] = new TagRuntimeData + { + TagType = runtime.TagType, + TotalStack = runtime.TotalStack + }; + } + + return cloned; + } } } diff --git a/Assets/GameMain/Scripts/Components/TowerController.cs b/Assets/GameMain/Scripts/Components/TowerController.cs index 28e3b64..99a9803 100644 --- a/Assets/GameMain/Scripts/Components/TowerController.cs +++ b/Assets/GameMain/Scripts/Components/TowerController.cs @@ -31,6 +31,7 @@ namespace Components private Color _muzzleColor = Color.white; private Color _bearingColor = Color.white; private Color _baseColor = Color.white; + private TagRuntimeData[] _tagRuntimes = System.Array.Empty(); public Transform CurrentTarget => _currentTarget; @@ -63,8 +64,9 @@ namespace Components float rotateSpeed = ResolveFloatValue(stats.RotateSpeed, _towerLevel, 180f, 1f); float attackRange = ResolveFloatValue(stats.AttackRange, _towerLevel, 5f, 0.1f); float attackSpeed = ResolveFloatValue(stats.AttackSpeed, _towerLevel, 1f, 0.01f); + _tagRuntimes = ResolveRuntimeTags(stats); - _muzzleComp?.OnInit(attackDamage, stats.AttackMethodType); + _muzzleComp?.OnInit(attackDamage, stats.AttackMethodType, tagRuntimes: _tagRuntimes); _bearingComp?.OnInit(rotateSpeed, attackRange); _baseComp?.OnInit(attackSpeed, stats.AttackPropertyType); ApplyComponentColors(); @@ -80,6 +82,7 @@ namespace Components _currentTarget = null; _retargetTimer = 0f; _towerLevel = MinTowerLevel; + _tagRuntimes = System.Array.Empty(); _muzzleComp?.OnReset(); _bearingComp?.OnReset(); _baseComp?.OnReset(); @@ -221,6 +224,47 @@ namespace Components return target != null && target.gameObject.activeInHierarchy; } + private static TagRuntimeData[] CloneTagRuntimes(TagRuntimeData[] source) + { + if (source == null || source.Length <= 0) + { + return System.Array.Empty(); + } + + TagRuntimeData[] cloned = new TagRuntimeData[source.Length]; + for (int i = 0; i < source.Length; i++) + { + TagRuntimeData runtime = source[i]; + if (runtime == null) + { + continue; + } + + cloned[i] = new TagRuntimeData + { + TagType = runtime.TagType, + TotalStack = runtime.TotalStack + }; + } + + return cloned; + } + + private static TagRuntimeData[] ResolveRuntimeTags(TowerStatsData stats) + { + if (stats == null) + { + return System.Array.Empty(); + } + + if (stats.TagRuntimes != null && stats.TagRuntimes.Length > 0) + { + return CloneTagRuntimes(stats.TagRuntimes); + } + + return TowerTagAggregationService.BuildRuntimeTagsFromUniqueTags(stats.Tags); + } + private static int ResolveIntValue(int[] values, int level, int fallback, int minValue) { int resolved = Mathf.Max(minValue, fallback); diff --git a/Assets/GameMain/Scripts/Definition/DataStruct/AttackPayload.cs b/Assets/GameMain/Scripts/Definition/DataStruct/AttackPayload.cs new file mode 100644 index 0000000..6a98e4a --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/DataStruct/AttackPayload.cs @@ -0,0 +1,23 @@ +using System; +using GeometryTD.CustomUtility; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class AttackPayload + { + public int BaseDamage { get; set; } + public AttackPropertyType AttackPropertyType { get; set; } + public TagRuntimeData[] TagRuntimes { get; set; } = Array.Empty(); + + public AttackPayload Clone() + { + return new AttackPayload + { + BaseDamage = BaseDamage, + AttackPropertyType = AttackPropertyType, + TagRuntimes = InventoryCloneUtility.CloneTagRuntimes(TagRuntimes) + }; + } + } +} diff --git a/Assets/GameMain/Scripts/Definition/DataStruct/AttackPayload.cs.meta b/Assets/GameMain/Scripts/Definition/DataStruct/AttackPayload.cs.meta new file mode 100644 index 0000000..1fd0037 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/DataStruct/AttackPayload.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4ed2fd20f9feb0f4d9448029b5bf63df +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/DataStruct/HitContext.cs b/Assets/GameMain/Scripts/Definition/DataStruct/HitContext.cs new file mode 100644 index 0000000..02e2f66 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/DataStruct/HitContext.cs @@ -0,0 +1,13 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class HitContext + { + public AttackPayload AttackPayload { get; set; } + public int FinalDamage { get; set; } + public bool IsCriticalHit { get; set; } + public bool IsKilled { get; set; } + } +} diff --git a/Assets/GameMain/Scripts/Definition/DataStruct/HitContext.cs.meta b/Assets/GameMain/Scripts/Definition/DataStruct/HitContext.cs.meta new file mode 100644 index 0000000..cf5b77d --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/DataStruct/HitContext.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c26677bee93904e4da19e339c6e3a803 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Enum/TagCategory.cs b/Assets/GameMain/Scripts/Definition/Enum/TagCategory.cs new file mode 100644 index 0000000..d302e16 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/TagCategory.cs @@ -0,0 +1,10 @@ +namespace GeometryTD.Definition +{ + public enum TagCategory : byte + { + None = 0, + Status = 1, + NumericModifier = 2, + AttackShape = 3 + } +} diff --git a/Assets/GameMain/Scripts/Definition/Enum/TagCategory.cs.meta b/Assets/GameMain/Scripts/Definition/Enum/TagCategory.cs.meta new file mode 100644 index 0000000..a7ba430 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/TagCategory.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8c0d6ea8de936264db4aa2f5df963d2b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Enum/TagTriggerPhase.cs b/Assets/GameMain/Scripts/Definition/Enum/TagTriggerPhase.cs new file mode 100644 index 0000000..cfa9d7f --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/TagTriggerPhase.cs @@ -0,0 +1,11 @@ +namespace GeometryTD.Definition +{ + public enum TagTriggerPhase : byte + { + None = 0, + OnBeforeHit = 1, + OnHit = 2, + OnAfterHit = 3, + OnKill = 4 + } +} diff --git a/Assets/GameMain/Scripts/Definition/Enum/TagTriggerPhase.cs.meta b/Assets/GameMain/Scripts/Definition/Enum/TagTriggerPhase.cs.meta new file mode 100644 index 0000000..b21cf7d --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Enum/TagTriggerPhase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: bdfe1f7eba0a78742ae112cd61e9da49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Event.meta b/Assets/GameMain/Scripts/Definition/Event.meta new file mode 100644 index 0000000..27781f6 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Event.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a2a3c49c622040edbd52d0c66df2377e +timeCreated: 1773105664 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/EventEffect.meta b/Assets/GameMain/Scripts/Definition/Event/EventEffect.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect.meta rename to Assets/GameMain/Scripts/Definition/Event/EventEffect.meta diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/AddGoldEffect.cs b/Assets/GameMain/Scripts/Definition/Event/EventEffect/AddGoldEffect.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/AddGoldEffect.cs rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/AddGoldEffect.cs diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/AddGoldEffect.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventEffect/AddGoldEffect.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/AddGoldEffect.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/AddGoldEffect.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/AddRandomCompsEffect.cs b/Assets/GameMain/Scripts/Definition/Event/EventEffect/AddRandomCompsEffect.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/AddRandomCompsEffect.cs rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/AddRandomCompsEffect.cs diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/AddRandomCompsEffect.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventEffect/AddRandomCompsEffect.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/AddRandomCompsEffect.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/AddRandomCompsEffect.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/DamageRandomTowerEnduranceEffect.cs b/Assets/GameMain/Scripts/Definition/Event/EventEffect/DamageRandomTowerEnduranceEffect.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/DamageRandomTowerEnduranceEffect.cs rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/DamageRandomTowerEnduranceEffect.cs diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/DamageRandomTowerEnduranceEffect.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventEffect/DamageRandomTowerEnduranceEffect.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/DamageRandomTowerEnduranceEffect.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/DamageRandomTowerEnduranceEffect.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/EventEffectBase.cs b/Assets/GameMain/Scripts/Definition/Event/EventEffect/EventEffectBase.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/EventEffectBase.cs rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/EventEffectBase.cs diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/EventEffectBase.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventEffect/EventEffectBase.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/EventEffectBase.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/EventEffectBase.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/RemoveRandomCompEffect.cs b/Assets/GameMain/Scripts/Definition/Event/EventEffect/RemoveRandomCompEffect.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/RemoveRandomCompEffect.cs rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/RemoveRandomCompEffect.cs diff --git a/Assets/GameMain/Scripts/Definition/EventEffect/RemoveRandomCompEffect.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventEffect/RemoveRandomCompEffect.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventEffect/RemoveRandomCompEffect.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventEffect/RemoveRandomCompEffect.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement.meta b/Assets/GameMain/Scripts/Definition/Event/EventRequirement.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement.meta rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement.meta diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/CompCountAtLeastRequirement.cs b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/CompCountAtLeastRequirement.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/CompCountAtLeastRequirement.cs rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/CompCountAtLeastRequirement.cs diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/CompCountAtLeastRequirement.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/CompCountAtLeastRequirement.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/CompCountAtLeastRequirement.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/CompCountAtLeastRequirement.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/EventRequirementBase.cs b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/EventRequirementBase.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/EventRequirementBase.cs rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/EventRequirementBase.cs diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/EventRequirementBase.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/EventRequirementBase.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/EventRequirementBase.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/EventRequirementBase.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/GoldAtLeastRequirement.cs b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/GoldAtLeastRequirement.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/GoldAtLeastRequirement.cs rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/GoldAtLeastRequirement.cs diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/GoldAtLeastRequirement.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/GoldAtLeastRequirement.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/GoldAtLeastRequirement.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/GoldAtLeastRequirement.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/HasRelicRequirement.cs b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/HasRelicRequirement.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/HasRelicRequirement.cs rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/HasRelicRequirement.cs diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/HasRelicRequirement.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/HasRelicRequirement.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/HasRelicRequirement.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/HasRelicRequirement.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/TowerCountAtLeastRequirement.cs b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/TowerCountAtLeastRequirement.cs similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/TowerCountAtLeastRequirement.cs rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/TowerCountAtLeastRequirement.cs diff --git a/Assets/GameMain/Scripts/Definition/EventRequirement/TowerCountAtLeastRequirement.cs.meta b/Assets/GameMain/Scripts/Definition/Event/EventRequirement/TowerCountAtLeastRequirement.cs.meta similarity index 100% rename from Assets/GameMain/Scripts/Definition/EventRequirement/TowerCountAtLeastRequirement.cs.meta rename to Assets/GameMain/Scripts/Definition/Event/EventRequirement/TowerCountAtLeastRequirement.cs.meta diff --git a/Assets/GameMain/Scripts/Definition/Tag.meta b/Assets/GameMain/Scripts/Definition/Tag.meta new file mode 100644 index 0000000..9363d2f --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1ff28b0dd76d49ecb0f6e615df4f7a62 +timeCreated: 1773105681 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect.meta b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect.meta new file mode 100644 index 0000000..20bbd72 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1909b51822cc4368bc604674fd119123 +timeCreated: 1773105715 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/AbsoluteZeroTagEffect.cs b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/AbsoluteZeroTagEffect.cs new file mode 100644 index 0000000..2a63242 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/AbsoluteZeroTagEffect.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace GeometryTD.Definition +{ + public sealed class AbsoluteZeroTagEffect : EnemyStatusTagEffectBase + { + public override TagType TagType => TagType.AbsoluteZero; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/AbsoluteZeroTagEffect.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/AbsoluteZeroTagEffect.cs.meta new file mode 100644 index 0000000..fdcc37c --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/AbsoluteZeroTagEffect.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: deecaa9a38c04725b5abeb5488cbe53f +timeCreated: 1773106137 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/EnemyStatusTagEffectBase.cs b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/EnemyStatusTagEffectBase.cs new file mode 100644 index 0000000..a2dd86e --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/EnemyStatusTagEffectBase.cs @@ -0,0 +1,32 @@ +using System; +using GeometryTD.Entity; + +namespace GeometryTD.Definition +{ + public abstract class EnemyStatusTagEffectBase : IEnemyStatusTagEffect + { + public abstract TagType TagType { get; } + + public virtual void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime, + TagRuntimeData runtimeData) + { + _ = attackPayload; + _ = runtime; + _ = runtimeData; + } + + public virtual bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action applyDamage) + { + _ = runtime; + _ = deltaTime; + _ = applyDamage; + return false; + } + + public virtual float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime) + { + _ = runtime; + return 1f; + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/EnemyStatusTagEffectBase.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/EnemyStatusTagEffectBase.cs.meta new file mode 100644 index 0000000..f69d99e --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/EnemyStatusTagEffectBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: aefaa5745cc94c0aae92109e64d83dc1 +timeCreated: 1773105755 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs new file mode 100644 index 0000000..ffc144e --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs @@ -0,0 +1,68 @@ +using System; +using GeometryTD.Entity; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public sealed class FireTagEffect : EnemyStatusTagEffectBase + { + public override TagType TagType => TagType.Fire; + + public override void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime, + TagRuntimeData runtimeData) + { + Debug.Assert(TagConfigRegistry.TryGetDefinition(TagType, out TagDefinitionData definition)); + + FireTagConfig config = definition.Config as FireTagConfig; + Debug.Assert(config != null); + if (!config.IsImplemented) + { + return; + } + + FireTagState state = runtime.GetOrCreateState(TagType); + float burnDamagePerSecond = Mathf.Max(0f, + config.BurnDamagePerSecondPerStack * runtimeData.TotalStack); + + if (burnDamagePerSecond <= 0f) + { + return; + } + + state.RemainingDuration = Mathf.Max(state.RemainingDuration, config.BurnDurationSeconds); + state.DamagePerSecond = Mathf.Max(state.DamagePerSecond, burnDamagePerSecond); + runtime.Activate(TagType); + } + + public override bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action applyDamage) + { + FireTagState state = runtime.GetState(TagType); + Debug.Assert(state != null); + + float resolvedDeltaTime = Mathf.Max(0f, deltaTime); + if (resolvedDeltaTime <= 0f || state.RemainingDuration <= 0f) + { + return state.RemainingDuration > 0f; + } + + float appliedDuration = Mathf.Min(resolvedDeltaTime, state.RemainingDuration); + state.RemainingDuration = Mathf.Max(0f, state.RemainingDuration - appliedDuration); + state.PendingDamage += state.DamagePerSecond * appliedDuration; + + int resolvedDamage = Mathf.FloorToInt(state.PendingDamage); + if (resolvedDamage > 0) + { + applyDamage?.Invoke(resolvedDamage); + state.PendingDamage -= resolvedDamage; + } + + if (state.RemainingDuration > 0f) + { + return true; + } + + state.PendingDamage = 0f; + return false; + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs.meta new file mode 100644 index 0000000..f3ed9c7 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/FireTagEffect.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6609b360347c4fc9bccbe288efa60ae4 +timeCreated: 1773105786 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IEnemyStatusTagEffect.cs b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IEnemyStatusTagEffect.cs new file mode 100644 index 0000000..d91056e --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IEnemyStatusTagEffect.cs @@ -0,0 +1,13 @@ +using System; +using GeometryTD.Entity; + +namespace GeometryTD.Definition +{ + public interface IEnemyStatusTagEffect + { + TagType TagType { get; } + void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime, TagRuntimeData runtimeData); + bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action applyDamage); + float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime); + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IEnemyStatusTagEffect.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IEnemyStatusTagEffect.cs.meta new file mode 100644 index 0000000..6ad6edc --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IEnemyStatusTagEffect.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 07eac037b9274ddaa6352412c9043f62 +timeCreated: 1773105725 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs new file mode 100644 index 0000000..d1cf3eb --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs @@ -0,0 +1,50 @@ +using System; +using GeometryTD.Entity; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public sealed class IceTagEffect : EnemyStatusTagEffectBase + { + public override TagType TagType => TagType.Ice; + + public override void Apply(AttackPayload attackPayload, EnemyTagStatusRuntime runtime, + TagRuntimeData runtimeData) + { + _ = attackPayload; + Debug.Assert(runtimeData != null); + Debug.Assert(runtimeData.TotalStack > 0); + Debug.Assert(TagConfigRegistry.TryGetDefinition(TagType, out TagDefinitionData definition)); + + IceTagConfig config = definition.Config as IceTagConfig; + Debug.Assert(config != null); + if (!config.IsImplemented) + { + return; + } + + IceTagState state = runtime.GetOrCreateState(TagType); + float slowMultiplier = Mathf.Max(config.MinMoveSpeedMultiplier, + 1f - runtimeData.TotalStack * config.SlowRatioPerStack); + state.RemainingDuration = Mathf.Max(state.RemainingDuration, config.SlowDurationSeconds); + state.SlowMultiplier = Mathf.Min(state.SlowMultiplier, slowMultiplier); + runtime.Activate(TagType); + } + + public override bool Tick(EnemyTagStatusRuntime runtime, float deltaTime, Action applyDamage) + { + _ = applyDamage; + IceTagState state = runtime.GetState(TagType); + Debug.Assert(state != null); + + state.RemainingDuration = Mathf.Max(0f, state.RemainingDuration - Mathf.Max(0f, deltaTime)); + return state.RemainingDuration > 0f; + } + + public override float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime) + { + IceTagState state = runtime.GetState(TagType); + return state == null ? 1f : state.SlowMultiplier; + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs.meta new file mode 100644 index 0000000..8442b6c --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/IceTagEffect.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 0787cd2263b441ceba5b3ad381827677 +timeCreated: 1773106119 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/InfernoTagEffect.cs b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/InfernoTagEffect.cs new file mode 100644 index 0000000..a4843d4 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/InfernoTagEffect.cs @@ -0,0 +1,7 @@ +namespace GeometryTD.Definition +{ + public sealed class InfernoTagEffect : EnemyStatusTagEffectBase + { + public override TagType TagType => TagType.Inferno; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/InfernoTagEffect.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/InfernoTagEffect.cs.meta new file mode 100644 index 0000000..d7a4bf9 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagEffect/InfernoTagEffect.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 2aa813ec42ea4dddb717ff28da764f15 +timeCreated: 1773106082 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagRegistry.cs b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagRegistry.cs new file mode 100644 index 0000000..db2b069 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagRegistry.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using GeometryTD.Entity; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class EnemyStatusTagRegistry + { + private static readonly Dictionary EffectsByTag = + new Dictionary + { + [TagType.Fire] = new FireTagEffect(), + [TagType.Inferno] = new InfernoTagEffect(), + [TagType.Ice] = new IceTagEffect(), + [TagType.AbsoluteZero] = new AbsoluteZeroTagEffect() + }; + + public static IReadOnlyDictionary Effects => EffectsByTag; + + public static bool TryGetEffect(TagType tagType, out IEnemyStatusTagEffect effect) + { + return EffectsByTag.TryGetValue(tagType, out effect); + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagRegistry.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagRegistry.cs.meta new file mode 100644 index 0000000..242fdf3 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/EnemyStatusTagRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 49ed835d5a444f199fa74b2cb4d06a51 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig.meta new file mode 100644 index 0000000..99cee6f --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: c90264b23a9c45c0b21bc627a6109282 +timeCreated: 1773105821 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs new file mode 100644 index 0000000..6ff564e --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class AbsoluteZeroTagConfig : TagConfigBase + { + public AbsoluteZeroTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float BonusSlowDurationSeconds { get; set; } = 0f; + public float BonusSlowRatioPerStack { get; set; } = 0f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs.meta new file mode 100644 index 0000000..d73038c --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/AbsoluteZeroTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: a92545f498ff4b8ab974a73d220f2cbc +timeCreated: 1773105984 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/BurnSpreadTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/BurnSpreadTagConfig.cs new file mode 100644 index 0000000..f2789b7 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/BurnSpreadTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class BurnSpreadTagConfig : TagConfigBase + { + public BurnSpreadTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float SpreadRadius { get; set; } = 0f; + public float SpreadDamageRate { get; set; } = 0f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/BurnSpreadTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/BurnSpreadTagConfig.cs.meta new file mode 100644 index 0000000..f1103a7 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/BurnSpreadTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 16fa8f668caf4a6a9d384f53c86ef6d8 +timeCreated: 1773105868 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/CritTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/CritTagConfig.cs new file mode 100644 index 0000000..b97228f --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/CritTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class CritTagConfig : TagConfigBase + { + public CritTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float CritChancePerStack { get; set; } = 0.1f; + public float CritDamageMultiplier { get; set; } = 1.5f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/CritTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/CritTagConfig.cs.meta new file mode 100644 index 0000000..d78ce7f --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/CritTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 21c22a1b79d74aa58ef0054fed71b90a +timeCreated: 1773106012 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ExecutionTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ExecutionTagConfig.cs new file mode 100644 index 0000000..068fa75 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ExecutionTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class ExecutionTagConfig : TagConfigBase + { + public ExecutionTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float TargetHealthThreshold { get; set; } = 0.3f; + public float DamageBonusPerStack { get; set; } = 0.5f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ExecutionTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ExecutionTagConfig.cs.meta new file mode 100644 index 0000000..5aa2081 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ExecutionTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 865ffdb6e1df446fabfb2355d06ef60a +timeCreated: 1773106044 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs new file mode 100644 index 0000000..5b80c04 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class FireTagConfig : TagConfigBase + { + public FireTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float BurnDurationSeconds { get; set; } = 3f; + public float BurnDamagePerSecondPerStack { get; set; } = 20f; + } +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs.meta new file mode 100644 index 0000000..57d3791 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FireTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3225e9a8d28b4133a6484f576c4662ae +timeCreated: 1773105844 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FreezeMaskTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FreezeMaskTagConfig.cs new file mode 100644 index 0000000..904d210 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FreezeMaskTagConfig.cs @@ -0,0 +1,14 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class FreezeMaskTagConfig : TagConfigBase + { + public FreezeMaskTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float FreezeBuildUpPerStack { get; set; } = 0f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FreezeMaskTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FreezeMaskTagConfig.cs.meta new file mode 100644 index 0000000..874eee2 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/FreezeMaskTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: fa016e0d4c76466b8fb665656ee4bc9e +timeCreated: 1773105940 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IceTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IceTagConfig.cs new file mode 100644 index 0000000..4a7a9aa --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IceTagConfig.cs @@ -0,0 +1,16 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class IceTagConfig : TagConfigBase + { + public IceTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float SlowDurationSeconds { get; set; } = 2f; + public float SlowRatioPerStack { get; set; } = 0.2f; + public float MinMoveSpeedMultiplier { get; set; } = 0.4f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IceTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IceTagConfig.cs.meta new file mode 100644 index 0000000..c4a6bfc --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IceTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 546b506913154d74a9a7cd4b3aa36d12 +timeCreated: 1773105919 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IgniteBurstTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IgniteBurstTagConfig.cs new file mode 100644 index 0000000..eb5695c --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IgniteBurstTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class IgniteBurstTagConfig : TagConfigBase + { + public IgniteBurstTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float BurstRadius { get; set; } = 0f; + public float BurstDamageRate { get; set; } = 0f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IgniteBurstTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IgniteBurstTagConfig.cs.meta new file mode 100644 index 0000000..3b11bff --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/IgniteBurstTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: b412ab82145242e3a7ce99991199cc35 +timeCreated: 1773105884 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs new file mode 100644 index 0000000..416390c --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class InfernoTagConfig : TagConfigBase + { + public InfernoTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public float BonusBurnDurationSeconds { get; set; } = 0f; + public float BonusBurnDamagePerSecondPerStack { get; set; } = 0f; + } +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs.meta new file mode 100644 index 0000000..c28a8dc --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/InfernoTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: be707e334ca74a2a8a2a9d7773e70138 +timeCreated: 1773105897 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/OverpenetrateTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/OverpenetrateTagConfig.cs new file mode 100644 index 0000000..5578b3b --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/OverpenetrateTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class OverpenetrateTagConfig : TagConfigBase + { + public OverpenetrateTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public int ExtraPenetrationCount { get; set; } = 0; + public float RemainingDamageRate { get; set; } = 0f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/OverpenetrateTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/OverpenetrateTagConfig.cs.meta new file mode 100644 index 0000000..c8a0108 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/OverpenetrateTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 1d017f96198041cc97f2db1cb4f84fcb +timeCreated: 1773106028 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs new file mode 100644 index 0000000..e29ecc9 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs @@ -0,0 +1,14 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class PierceTagConfig : TagConfigBase + { + public PierceTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public int ExtraPierceCount { get; set; } = 0; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs.meta new file mode 100644 index 0000000..a8e581b --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/PierceTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 3081fa157ac74961a2a4ffb3a6e10851 +timeCreated: 1773105999 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs new file mode 100644 index 0000000..10c1f0c --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class ShatterTagConfig : TagConfigBase + { + public ShatterTagConfig(bool isImplemented) : base(isImplemented) + { + } + + public bool RequiresSlowedTarget { get; set; } = true; + public float DamageBonusPerStack { get; set; } = 0f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs.meta new file mode 100644 index 0000000..cfc5477 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/ShatterTagConfig.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: d0f84f8e59bd4d0ea620742802223d87 +timeCreated: 1773105968 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/TagConfigBase.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/TagConfigBase.cs new file mode 100644 index 0000000..11635a4 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/TagConfigBase.cs @@ -0,0 +1,15 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public abstract class TagConfigBase + { + public bool IsImplemented { get; set; } + + protected TagConfigBase(bool isImplemented) + { + IsImplemented = isImplemented; + } + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfig/TagConfigBase.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/TagConfigBase.cs.meta new file mode 100644 index 0000000..8e9182c --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfig/TagConfigBase.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c2d0d4db7370b9f4b9d4d2cc4e84e46c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs b/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs new file mode 100644 index 0000000..9288457 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; + +namespace GeometryTD.Definition +{ + public static class TagConfigRegistry + { + private static readonly Dictionary DefinitionsByTag = + new Dictionary + { + [TagType.Fire] = new TagDefinitionData + { + TagType = TagType.Fire, + Category = TagCategory.Status, + TriggerPhase = TagTriggerPhase.OnAfterHit, + Config = new FireTagConfig(true) + }, + [TagType.BurnSpread] = new TagDefinitionData + { + TagType = TagType.BurnSpread, + Category = TagCategory.AttackShape, + TriggerPhase = TagTriggerPhase.OnKill, + Config = new BurnSpreadTagConfig(false) + }, + [TagType.IgniteBurst] = new TagDefinitionData + { + TagType = TagType.IgniteBurst, + Category = TagCategory.AttackShape, + TriggerPhase = TagTriggerPhase.OnKill, + Config = new IgniteBurstTagConfig(false) + }, + [TagType.Inferno] = new TagDefinitionData + { + TagType = TagType.Inferno, + Category = TagCategory.Status, + TriggerPhase = TagTriggerPhase.OnAfterHit, + Config = new InfernoTagConfig(false) + }, + [TagType.Ice] = new TagDefinitionData + { + TagType = TagType.Ice, + Category = TagCategory.Status, + TriggerPhase = TagTriggerPhase.OnAfterHit, + Config = new IceTagConfig(true) + }, + [TagType.FreezeMask] = new TagDefinitionData + { + TagType = TagType.FreezeMask, + Category = TagCategory.AttackShape, + TriggerPhase = TagTriggerPhase.OnAfterHit, + Config = new FreezeMaskTagConfig(false) + }, + [TagType.Shatter] = new TagDefinitionData + { + TagType = TagType.Shatter, + Category = TagCategory.NumericModifier, + TriggerPhase = TagTriggerPhase.OnBeforeHit, + Config = new ShatterTagConfig(false) + }, + [TagType.AbsoluteZero] = new TagDefinitionData + { + TagType = TagType.AbsoluteZero, + Category = TagCategory.Status, + TriggerPhase = TagTriggerPhase.OnAfterHit, + Config = new AbsoluteZeroTagConfig(false) + }, + [TagType.Pierce] = new TagDefinitionData + { + TagType = TagType.Pierce, + Category = TagCategory.AttackShape, + TriggerPhase = TagTriggerPhase.OnHit, + Config = new PierceTagConfig(false) + }, + [TagType.Crit] = new TagDefinitionData + { + TagType = TagType.Crit, + Category = TagCategory.NumericModifier, + TriggerPhase = TagTriggerPhase.OnBeforeHit, + Config = new CritTagConfig(true) + }, + [TagType.Overpenetrate] = new TagDefinitionData + { + TagType = TagType.Overpenetrate, + Category = TagCategory.AttackShape, + TriggerPhase = TagTriggerPhase.OnHit, + Config = new OverpenetrateTagConfig(false) + }, + [TagType.Execution] = new TagDefinitionData + { + TagType = TagType.Execution, + Category = TagCategory.NumericModifier, + TriggerPhase = TagTriggerPhase.OnBeforeHit, + Config = new ExecutionTagConfig(true) + } + }; + + public static IReadOnlyDictionary Definitions => DefinitionsByTag; + + public static bool TryGetDefinition(TagType tagType, out TagDefinitionData definition) + { + return DefinitionsByTag.TryGetValue(tagType, out definition); + } + } +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs.meta new file mode 100644 index 0000000..0544550 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagConfigRegistry.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 717bc2031fdf31e45a3de05cf4e2f24f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs b/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs new file mode 100644 index 0000000..033e7c6 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs @@ -0,0 +1,13 @@ +using System; + +namespace GeometryTD.Definition +{ + [Serializable] + public sealed class TagDefinitionData + { + public TagType TagType { get; set; } + public TagCategory Category { get; set; } + public TagTriggerPhase TriggerPhase { get; set; } + public TagConfigBase Config { get; set; } + } +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs.meta new file mode 100644 index 0000000..122b92c --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagDefinitionData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: c0011d6ce1b3dd14a89c6af6524cc732 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler.meta b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler.meta new file mode 100644 index 0000000..255631a --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 6fb08c76711246c2afa02d60769d76ef +timeCreated: 1773106291 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/AttackShapeTagEffectHandler.cs b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/AttackShapeTagEffectHandler.cs new file mode 100644 index 0000000..ca9f24e --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/AttackShapeTagEffectHandler.cs @@ -0,0 +1,41 @@ +namespace GeometryTD.Definition +{ + public static class AttackShapeTagEffectHandler + { + public static void Apply(HitContext hitContext, TagTriggerPhase triggerPhase) + { + if (hitContext?.AttackPayload?.TagRuntimes == null || hitContext.AttackPayload.TagRuntimes.Length <= 0) + { + return; + } + + for (int i = 0; i < hitContext.AttackPayload.TagRuntimes.Length; i++) + { + TagRuntimeData runtime = hitContext.AttackPayload.TagRuntimes[i]; + if (runtime == null || runtime.TotalStack <= 0) + { + continue; + } + + if (!TagConfigRegistry.TryGetDefinition(runtime.TagType, out TagDefinitionData definition) || + definition == null || + definition.Category != TagCategory.AttackShape || + definition.TriggerPhase != triggerPhase || + definition.Config == null) + { + continue; + } + + switch (runtime.TagType) + { + case TagType.BurnSpread: + case TagType.IgniteBurst: + case TagType.FreezeMask: + case TagType.Pierce: + case TagType.Overpenetrate: + break; + } + } + } + } +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/AttackShapeTagEffectHandler.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/AttackShapeTagEffectHandler.cs.meta new file mode 100644 index 0000000..1cefac8 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/AttackShapeTagEffectHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 434d659f0e0963d40a7da7e03b144966 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs new file mode 100644 index 0000000..952c7fa --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs @@ -0,0 +1,107 @@ +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class NumericTagEffectHandler + { + public static void ApplyBeforeHit( + HitContext hitContext, + int targetCurrentHealth, + int targetMaxHealth, + bool targetHasSlowStatus, + float? critRoll = null) + { + if (hitContext == null || hitContext.AttackPayload == null) + { + return; + } + + TagRuntimeData[] tagRuntimes = hitContext.AttackPayload.TagRuntimes; + if (tagRuntimes == null || tagRuntimes.Length <= 0) + { + return; + } + + for (int i = 0; i < tagRuntimes.Length; i++) + { + TagRuntimeData runtime = tagRuntimes[i]; + if (runtime == null || runtime.TotalStack <= 0) + { + continue; + } + + if (!TagConfigRegistry.TryGetDefinition(runtime.TagType, out TagDefinitionData definition) || + definition == null || + definition.Category != TagCategory.NumericModifier || + definition.TriggerPhase != TagTriggerPhase.OnBeforeHit || + definition.Config == null) + { + continue; + } + + switch (runtime.TagType) + { + case TagType.Crit: + ApplyCrit(hitContext, runtime.TotalStack, definition.Config as CritTagConfig, critRoll); + break; + case TagType.Execution: + ApplyExecution(hitContext, runtime.TotalStack, targetCurrentHealth, targetMaxHealth, definition.Config as ExecutionTagConfig); + break; + case TagType.Shatter: + ApplyShatter(hitContext, runtime.TotalStack, targetHasSlowStatus, definition.Config as ShatterTagConfig); + break; + } + } + + hitContext.IsKilled = targetCurrentHealth > 0 && hitContext.FinalDamage >= targetCurrentHealth; + } + + private static void ApplyCrit(HitContext hitContext, int stack, CritTagConfig config, float? critRoll) + { + if (config == null || !config.IsImplemented || stack <= 0) + { + return; + } + + float chance = Mathf.Clamp01(stack * config.CritChancePerStack); + float resolvedCritRoll = critRoll ?? Random.value; + if (resolvedCritRoll > chance) + { + return; + } + + hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * config.CritDamageMultiplier)); + hitContext.IsCriticalHit = true; + } + + private static void ApplyExecution( + HitContext hitContext, + int stack, + int targetCurrentHealth, + int targetMaxHealth, + ExecutionTagConfig config) + { + if (config == null || !config.IsImplemented || stack <= 0 || targetMaxHealth <= 0) + { + return; + } + + float healthRatio = Mathf.Clamp01((float)Mathf.Max(0, targetCurrentHealth) / targetMaxHealth); + if (healthRatio > config.TargetHealthThreshold) + { + return; + } + + float multiplier = 1f + stack * config.DamageBonusPerStack; + hitContext.FinalDamage = Mathf.Max(0, Mathf.RoundToInt(hitContext.FinalDamage * multiplier)); + } + + private static void ApplyShatter(HitContext hitContext, int stack, bool targetHasSlowStatus, ShatterTagConfig config) + { + _ = hitContext; + _ = stack; + _ = targetHasSlowStatus; + _ = config; + } + } +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs.meta new file mode 100644 index 0000000..ad9ae68 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagEffectHandler/NumericTagEffectHandler.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: eef83fab509bf1b4aaea8504c8a2fc7f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagEffectResolver.cs b/Assets/GameMain/Scripts/Definition/Tag/TagEffectResolver.cs new file mode 100644 index 0000000..417ec7d --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagEffectResolver.cs @@ -0,0 +1,99 @@ +using GeometryTD.Entity; +using UnityEngine; + +namespace GeometryTD.Definition +{ + public static class TagEffectResolver + { + public static HitContext ResolveBeforeHit( + AttackPayload attackPayload, + int targetCurrentHealth, + int targetMaxHealth, + bool targetHasSlowStatus, + float? critRoll = null) + { + _ = targetHasSlowStatus; + AttackPayload resolvedPayload = attackPayload?.Clone() ?? new AttackPayload(); + HitContext hitContext = new HitContext + { + AttackPayload = resolvedPayload, + FinalDamage = Mathf.Max(0, resolvedPayload.BaseDamage), + IsCriticalHit = false, + IsKilled = targetCurrentHealth > 0 && resolvedPayload.BaseDamage >= targetCurrentHealth + }; + + NumericTagEffectHandler.ApplyBeforeHit( + hitContext, + targetCurrentHealth, + targetMaxHealth, + targetHasSlowStatus, + critRoll); + return hitContext; + } + + public static void ApplyAfterHit(AttackPayload attackPayload, EnemyTagStatusRuntime targetStatusRuntime) + { + if (attackPayload != null && targetStatusRuntime != null) + { + TagRuntimeData[] tagRuntimes = attackPayload.TagRuntimes; + if (tagRuntimes != null && tagRuntimes.Length > 0) + { + foreach (var tag in tagRuntimes) + { + if (tag == null || tag.TotalStack <= 0) + { + continue; + } + + if (!TagConfigRegistry.TryGetDefinition(tag.TagType, out TagDefinitionData definition) || + definition == null || + definition.Category != TagCategory.Status || + definition.TriggerPhase != TagTriggerPhase.OnAfterHit || + definition.Config == null || + !EnemyStatusTagRegistry.TryGetEffect(tag.TagType, out IEnemyStatusTagEffect effect)) + { + continue; + } + + effect.Apply(attackPayload, targetStatusRuntime, tag); + } + } + } + + AttackShapeTagEffectHandler.Apply(new HitContext + { + AttackPayload = attackPayload + }, TagTriggerPhase.OnAfterHit); + } + + public static void ApplyOnHit(HitContext hitContext) + { + AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnHit); + } + + public static void ApplyOnKill(HitContext hitContext) + { + AttackShapeTagEffectHandler.Apply(hitContext, TagTriggerPhase.OnKill); + } + + public static int GetTagStack(TagRuntimeData[] tagRuntimes, TagType tagType) + { + if (tagRuntimes == null || tagRuntimes.Length <= 0 || tagType == TagType.None) + { + return 0; + } + + foreach (var tag in tagRuntimes) + { + if (tag == null || tag.TagType != tagType || tag.TotalStack <= 0) + { + continue; + } + + return tag.TotalStack; + } + + return 0; + } + } +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagEffectResolver.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagEffectResolver.cs.meta new file mode 100644 index 0000000..8d728b7 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagEffectResolver.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 5e0b2b45c147579459081b36d26d471c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagStates.meta b/Assets/GameMain/Scripts/Definition/Tag/TagStates.meta new file mode 100644 index 0000000..b333dc8 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagStates.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 0f2ab8a7e488575498ac933a125360d1 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagStates/EnemyStatusTagStateBase.cs b/Assets/GameMain/Scripts/Definition/Tag/TagStates/EnemyStatusTagStateBase.cs new file mode 100644 index 0000000..1b80158 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagStates/EnemyStatusTagStateBase.cs @@ -0,0 +1,7 @@ +namespace GeometryTD.Definition +{ + public abstract class EnemyStatusTagStateBase + { + } + +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagStates/EnemyStatusTagStateBase.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagStates/EnemyStatusTagStateBase.cs.meta new file mode 100644 index 0000000..f147ded --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagStates/EnemyStatusTagStateBase.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 36215e56239c41c5bb196de6927643a8 +timeCreated: 1773105536 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagStates/FireTagState.cs b/Assets/GameMain/Scripts/Definition/Tag/TagStates/FireTagState.cs new file mode 100644 index 0000000..12bb3ed --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagStates/FireTagState.cs @@ -0,0 +1,9 @@ +namespace GeometryTD.Definition +{ + public sealed class FireTagState : EnemyStatusTagStateBase + { + public float RemainingDuration { get; set; } + public float DamagePerSecond { get; set; } + public float PendingDamage { get; set; } + } +} diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagStates/FireTagState.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagStates/FireTagState.cs.meta new file mode 100644 index 0000000..7a5019f --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagStates/FireTagState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 20ad1b81a1634d06b9c8bbadec37bcd4 +timeCreated: 1773105566 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagStates/IceTagState.cs b/Assets/GameMain/Scripts/Definition/Tag/TagStates/IceTagState.cs new file mode 100644 index 0000000..f99287c --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagStates/IceTagState.cs @@ -0,0 +1,8 @@ +namespace GeometryTD.Definition +{ + public sealed class IceTagState : EnemyStatusTagStateBase + { + public float RemainingDuration { get; set; } + public float SlowMultiplier { get; set; } = 1f; + } +} \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Definition/Tag/TagStates/IceTagState.cs.meta b/Assets/GameMain/Scripts/Definition/Tag/TagStates/IceTagState.cs.meta new file mode 100644 index 0000000..c319575 --- /dev/null +++ b/Assets/GameMain/Scripts/Definition/Tag/TagStates/IceTagState.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 43c7eb87fa774e60a894f4cc8e0089a5 +timeCreated: 1773105599 \ No newline at end of file diff --git a/Assets/GameMain/Scripts/Entity/EntityData/BulletData.cs b/Assets/GameMain/Scripts/Entity/EntityData/BulletData.cs index e5be59d..cc93b53 100644 --- a/Assets/GameMain/Scripts/Entity/EntityData/BulletData.cs +++ b/Assets/GameMain/Scripts/Entity/EntityData/BulletData.cs @@ -8,27 +8,24 @@ namespace GeometryTD.Entity.EntityData public class BulletData : EntityDataBase { [SerializeField] private Transform _target = null; - [SerializeField] private int _damage = 0; [SerializeField] private float _speed = 0f; [SerializeField] private float _maxLifetime = 3f; - [SerializeField] private AttackPropertyType _attackPropertyType = AttackPropertyType.None; + [SerializeField] private AttackPayload _attackPayload = new AttackPayload(); public BulletData( int entityId, int typeId, Vector3 position, Transform target, - int damage, + AttackPayload attackPayload, float speed, - AttackPropertyType attackPropertyType = AttackPropertyType.None, float maxLifetime = 3f) : base(entityId, typeId) { Position = position; _target = target; - _damage = damage; + _attackPayload = attackPayload?.Clone() ?? new AttackPayload(); _speed = speed; - _attackPropertyType = attackPropertyType; _maxLifetime = maxLifetime; } @@ -38,12 +35,6 @@ namespace GeometryTD.Entity.EntityData set => _target = value; } - public int Damage - { - get => _damage; - set => _damage = value; - } - public float Speed { get => _speed; @@ -56,10 +47,10 @@ namespace GeometryTD.Entity.EntityData set => _maxLifetime = value; } - public AttackPropertyType AttackPropertyType + public AttackPayload AttackPayload { - get => _attackPropertyType; - set => _attackPropertyType = value; + get => _attackPayload; + set => _attackPayload = value?.Clone() ?? new AttackPayload(); } } } diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs index bd36282..c547018 100644 --- a/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyEntity.cs @@ -21,8 +21,13 @@ namespace GeometryTD.Entity private readonly List _pathPoints = new(); private int _pathPointIndex; private bool _isDespawnRequested; + private readonly EnemyTagStatusRuntime _tagStatusRuntime = new(); public static IReadOnlyList ActiveEnemies => _activeEnemies; + public int CurrentHealth => _currentHealth; + public int MaxHealth => _maxHealth; + public bool HasSlowStatus => _tagStatusRuntime.GetMoveSpeedMultiplier() < 0.999f; + public float MoveSpeedMultiplier => _tagStatusRuntime.GetMoveSpeedMultiplier(); public static bool TryConsumeKilledFlag(int entityId) { @@ -46,6 +51,7 @@ namespace GeometryTD.Entity _isDespawnRequested = false; _maxHealth = 1; _currentHealth = 1; + _tagStatusRuntime.Reset(); if (userData is EnemyData enemyData) { _speed = enemyData.Speed; @@ -74,6 +80,7 @@ namespace GeometryTD.Entity protected override void OnUpdate(float elapseSeconds, float realElapseSeconds) { base.OnUpdate(elapseSeconds, realElapseSeconds); + _tagStatusRuntime.Tick(elapseSeconds, ApplyDamageFromStatus); if (_pathPoints.Count > 0) { @@ -90,6 +97,7 @@ namespace GeometryTD.Entity _isDespawnRequested = false; _maxHealth = 0; _currentHealth = 0; + _tagStatusRuntime.Reset(); _activeEnemies.Remove(this); base.OnHide(isShutdown, userData); @@ -100,26 +108,24 @@ namespace GeometryTD.Entity _activeEnemies.Remove(this); } - public void TakeDamage(int damage, AttackPropertyType attackPropertyType) + public void TakeDamage(AttackPayload attackPayload) { - _ = attackPropertyType; - if (_isDespawnRequested || damage <= 0 || _currentHealth <= 0) + if (_isDespawnRequested || _currentHealth <= 0) { return; } - int previousHealth = _currentHealth; - _currentHealth = Mathf.Max(0, _currentHealth - damage); - if (_maxHealth > 0) + HitContext hitContext = TagEffectResolver.ResolveBeforeHit( + attackPayload, + _currentHealth, + _maxHealth, + HasSlowStatus); + ApplyDirectDamage(hitContext.FinalDamage); + TagEffectResolver.ApplyOnHit(hitContext); + TagEffectResolver.ApplyAfterHit(hitContext.AttackPayload, _tagStatusRuntime); + if (hitContext.IsKilled) { - GameEntry.HPBar?.ShowHPBar(this, (float)previousHealth / _maxHealth, - (float)_currentHealth / _maxHealth); - } - - if (_currentHealth <= 0) - { - _killedEnemyEntityIds.Add(Id); - RequestDespawn(); + TagEffectResolver.ApplyOnKill(hitContext); } } @@ -155,6 +161,7 @@ namespace GeometryTD.Entity return; } + _movementComponent.SetSpeedMultiplier(_tagStatusRuntime.GetMoveSpeedMultiplier()); _movementComponent.SetMove(true); _movementComponent.SetDirection(direction); _movementComponent.OnUpdate(elapseSeconds, realElapseSeconds); @@ -188,5 +195,34 @@ namespace GeometryTD.Entity _movementComponent.SetMove(false); GameEntry.Entity.HideEntity(Entity); } + + private void ApplyDamageFromStatus(int damage) + { + ApplyDirectDamage(damage); + } + + private void ApplyDirectDamage(int damage) + { + if (_isDespawnRequested || damage <= 0 || _currentHealth <= 0) + { + return; + } + + int previousHealth = _currentHealth; + _currentHealth = Mathf.Max(0, _currentHealth - damage); + if (_maxHealth > 0) + { + GameEntry.HPBar?.ShowHPBar(this, (float)previousHealth / _maxHealth, + (float)_currentHealth / _maxHealth); + } + + if (_currentHealth > 0) + { + return; + } + + _killedEnemyEntityIds.Add(Id); + RequestDespawn(); + } } } diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyTagStatusRuntime.cs b/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyTagStatusRuntime.cs new file mode 100644 index 0000000..aba5176 --- /dev/null +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyTagStatusRuntime.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Generic; +using GeometryTD.Definition; +using UnityEngine; + +namespace GeometryTD.Entity +{ + public sealed class EnemyTagStatusRuntime + { + private readonly Dictionary _statesByTag = new(); + private readonly List _activeTags = new(); + + public bool HasStatus(TagType tagType) + { + return _statesByTag.ContainsKey(tagType); + } + + public void Reset() + { + _statesByTag.Clear(); + _activeTags.Clear(); + } + + public void Activate(TagType tagType) + { + if (_activeTags.Contains(tagType)) + { + return; + } + + _activeTags.Add(tagType); + } + + public TState GetState(TagType tagType) where TState : EnemyStatusTagStateBase + { + if (!_statesByTag.TryGetValue(tagType, out EnemyStatusTagStateBase state)) + { + return null; + } + + Debug.Assert(state is TState); + return state as TState; + } + + public TState GetOrCreateState(TagType tagType) where TState : EnemyStatusTagStateBase, new() + { + if (_statesByTag.TryGetValue(tagType, out EnemyStatusTagStateBase existingState)) + { + Debug.Assert(existingState is TState); + return existingState as TState; + } + + TState state = new TState(); + _statesByTag[tagType] = state; + return state; + } + + public void Tick(float deltaTime, Action applyDamage) + { + float resolvedDeltaTime = Mathf.Max(0f, deltaTime); + if (resolvedDeltaTime <= 0f) + { + return; + } + + for (int i = _activeTags.Count - 1; i >= 0; i--) + { + TagType tagType = _activeTags[i]; + Debug.Assert(EnemyStatusTagRegistry.TryGetEffect(tagType, out IEnemyStatusTagEffect effect)); + + if (effect.Tick(this, resolvedDeltaTime, applyDamage)) + { + continue; + } + + _statesByTag.Remove(tagType); + _activeTags.RemoveAt(i); + } + } + + public float GetMoveSpeedMultiplier() + { + float multiplier = 1f; + for (int i = 0; i < _activeTags.Count; i++) + { + TagType tagType = _activeTags[i]; + Debug.Assert(EnemyStatusTagRegistry.TryGetEffect(tagType, out IEnemyStatusTagEffect effect)); + multiplier = Mathf.Min(multiplier, effect.GetMoveSpeedMultiplier(this)); + } + + return multiplier; + } + } +} diff --git a/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyTagStatusRuntime.cs.meta b/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyTagStatusRuntime.cs.meta new file mode 100644 index 0000000..2988ef6 --- /dev/null +++ b/Assets/GameMain/Scripts/Entity/EntityLogic/EnemyTagStatusRuntime.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: fe0965914fd71e34ea696bfd546efc66 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs b/Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs index ce86a9f..4d3a42b 100644 --- a/Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs +++ b/Assets/GameMain/Scripts/Scene/Map/TowerPlacementService.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using GeometryTD.CustomUtility; using GeometryTD.Definition; using GeometryTD.Entity; using GeometryTD.Entity.EntityData; @@ -232,6 +233,7 @@ namespace GeometryTD.Map AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Physics, + TagRuntimes = Array.Empty(), Tags = Array.Empty() }; case 1: @@ -244,6 +246,7 @@ namespace GeometryTD.Map AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Fire, + TagRuntimes = Array.Empty(), Tags = Array.Empty() }; case 2: @@ -256,6 +259,7 @@ namespace GeometryTD.Map AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Ice, + TagRuntimes = Array.Empty(), Tags = Array.Empty() }; case 3: @@ -268,6 +272,7 @@ namespace GeometryTD.Map AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Poison, + TagRuntimes = Array.Empty(), Tags = Array.Empty() }; default: @@ -280,6 +285,7 @@ namespace GeometryTD.Map AttackSpeed = new[] { 1.5f, 1.2f, 1.1f, 1.0f, 0.8f }, AttackMethodType = AttackMethodType.NormalBullet, AttackPropertyType = AttackPropertyType.Physics, + TagRuntimes = Array.Empty(), Tags = Array.Empty() }; } @@ -302,6 +308,7 @@ namespace GeometryTD.Map AttackSpeed = source.AttackSpeed != null ? (float[])source.AttackSpeed.Clone() : Array.Empty(), AttackMethodType = source.AttackMethodType, AttackPropertyType = source.AttackPropertyType, + TagRuntimes = InventoryCloneUtility.CloneTagRuntimes(source.TagRuntimes), Tags = copiedTags }; } diff --git a/Assets/Tests/EditMode/TagCombatRuntimeTests.cs b/Assets/Tests/EditMode/TagCombatRuntimeTests.cs new file mode 100644 index 0000000..042dba0 --- /dev/null +++ b/Assets/Tests/EditMode/TagCombatRuntimeTests.cs @@ -0,0 +1,277 @@ +using Components; +using GeometryTD.Definition; +using GeometryTD.Entity; +using GeometryTD.Entity.EntityData; +using NUnit.Framework; +using UnityEngine; + +namespace GeometryTD.Tests.EditMode +{ + public sealed class TagEffectResolverTests + { + [Test] + public void ResolveBeforeHit_Crit_CanIncreaseDamage() + { + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + AttackPropertyType = AttackPropertyType.Physics, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Crit, TotalStack = 1 } + } + }; + + HitContext hitContext = TagEffectResolver.ResolveBeforeHit(attackPayload, 100, 100, false, 0.05f); + + Assert.That(hitContext.IsCriticalHit, Is.True); + Assert.That(hitContext.FinalDamage, Is.EqualTo(150)); + } + + [Test] + public void ResolveBeforeHit_Execution_OnlyTriggersOnLowHealthTarget() + { + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + AttackPropertyType = AttackPropertyType.Physics, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Execution, TotalStack = 1 } + } + }; + + HitContext lowHealthHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 30, 100, false); + HitContext highHealthHit = TagEffectResolver.ResolveBeforeHit(attackPayload, 31, 100, false); + + Assert.That(lowHealthHit.FinalDamage, Is.EqualTo(150)); + Assert.That(highHealthHit.FinalDamage, Is.EqualTo(100)); + } + } + + public sealed class EnemyTagStatusRuntimeTests + { + [Test] + public void ApplyAfterHit_Fire_AddsBurnDamageOverTime() + { + EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime(); + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Fire, TotalStack = 2 } + } + }; + int totalDamage = 0; + + TagEffectResolver.ApplyAfterHit(attackPayload, runtime); + Assert.That(runtime.HasStatus(TagType.Fire), Is.True); + + runtime.Tick(1f, damage => totalDamage += damage); + runtime.Tick(1f, damage => totalDamage += damage); + runtime.Tick(1f, damage => totalDamage += damage); + + Assert.That(runtime.HasStatus(TagType.Fire), Is.False); + Assert.That(totalDamage, Is.EqualTo(120)); + } + + [Test] + public void ApplyAfterHit_Fire_UsesElapsedDeltaTimeInsteadOfIndependentInterval() + { + EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime(); + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Fire, TotalStack = 1 } + } + }; + int totalDamage = 0; + + TagEffectResolver.ApplyAfterHit(attackPayload, runtime); + + runtime.Tick(0.5f, damage => totalDamage += damage); + runtime.Tick(0.5f, damage => totalDamage += damage); + runtime.Tick(2f, damage => totalDamage += damage); + + Assert.That(totalDamage, Is.EqualTo(60)); + Assert.That(runtime.HasStatus(TagType.Fire), Is.False); + } + + [Test] + public void ApplyAfterHit_Ice_AddsSlowAndRecoversAfterDuration() + { + EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime(); + AttackPayload attackPayload = new AttackPayload + { + BaseDamage = 100, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Ice, TotalStack = 2 } + } + }; + + TagEffectResolver.ApplyAfterHit(attackPayload, runtime); + + Assert.That(runtime.HasStatus(TagType.Ice), Is.True); + Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(0.6f).Within(0.001f)); + + runtime.Tick(2f, null); + + Assert.That(runtime.HasStatus(TagType.Ice), Is.False); + Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(1f).Within(0.001f)); + } + + [Test] + public void Tick_OnlyUpdatesActivatedStatusTags() + { + EnemyTagStatusRuntime runtime = new EnemyTagStatusRuntime(); + int totalDamage = 0; + + runtime.Tick(5f, damage => totalDamage += damage); + + Assert.That(totalDamage, Is.EqualTo(0)); + Assert.That(runtime.GetMoveSpeedMultiplier(), Is.EqualTo(1f).Within(0.001f)); + } + } + + public sealed class EnemyStatusTagRegistryTests + { + [Test] + public void Registry_Registers_All_Status_Tags() + { + Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Fire, out IEnemyStatusTagEffect fireEffect), Is.True); + Assert.That(fireEffect, Is.TypeOf()); + + Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Ice, out IEnemyStatusTagEffect iceEffect), Is.True); + Assert.That(iceEffect, Is.TypeOf()); + + Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.Inferno, out IEnemyStatusTagEffect infernoEffect), Is.True); + Assert.That(infernoEffect, Is.TypeOf()); + + Assert.That(EnemyStatusTagRegistry.TryGetEffect(TagType.AbsoluteZero, out IEnemyStatusTagEffect absoluteZeroEffect), Is.True); + Assert.That(absoluteZeroEffect, Is.TypeOf()); + } + } + + public sealed class TagConfigRegistryTests + { + [Test] + public void Definitions_Register_All_Twelve_Tags() + { + Assert.That(TagConfigRegistry.Definitions.Count, Is.EqualTo(12)); + Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Fire, out _), Is.True); + Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Execution, out _), Is.True); + Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Overpenetrate, out _), Is.True); + } + + [Test] + public void Definitions_Assign_Documented_Categories_And_Phases() + { + AssertDefinition(TagType.Fire, TagCategory.Status, TagTriggerPhase.OnAfterHit); + AssertDefinition(TagType.Crit, TagCategory.NumericModifier, TagTriggerPhase.OnBeforeHit); + AssertDefinition(TagType.Pierce, TagCategory.AttackShape, TagTriggerPhase.OnHit); + AssertDefinition(TagType.BurnSpread, TagCategory.AttackShape, TagTriggerPhase.OnKill); + } + + [Test] + public void Definitions_Keep_Unimplemented_Tags_As_Explicit_NoOpPlaceholders() + { + Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Shatter, out TagDefinitionData shatter), Is.True); + Assert.That(shatter.Config, Is.TypeOf()); + Assert.That(((ShatterTagConfig)shatter.Config).IsImplemented, Is.False); + + Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Inferno, out TagDefinitionData inferno), Is.True); + Assert.That(inferno.Config, Is.TypeOf()); + Assert.That(((InfernoTagConfig)inferno.Config).IsImplemented, Is.False); + + Assert.That(TagConfigRegistry.TryGetDefinition(TagType.Pierce, out TagDefinitionData pierce), Is.True); + Assert.That(pierce.Config, Is.TypeOf()); + Assert.That(((PierceTagConfig)pierce.Config).IsImplemented, Is.False); + } + + [Test] + public void AttackShape_Placeholders_Are_Routable_Without_Runtime_Effect() + { + HitContext hitContext = new HitContext + { + AttackPayload = new AttackPayload + { + BaseDamage = 100, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Pierce, TotalStack = 1 }, + new TagRuntimeData { TagType = TagType.BurnSpread, TotalStack = 1 }, + new TagRuntimeData { TagType = TagType.FreezeMask, TotalStack = 1 } + } + }, + FinalDamage = 100 + }; + + Assert.DoesNotThrow(() => TagEffectResolver.ApplyOnHit(hitContext)); + Assert.DoesNotThrow(() => TagEffectResolver.ApplyAfterHit(hitContext.AttackPayload, new EnemyTagStatusRuntime())); + Assert.DoesNotThrow(() => TagEffectResolver.ApplyOnKill(hitContext)); + } + + private static void AssertDefinition(TagType tagType, TagCategory expectedCategory, TagTriggerPhase expectedPhase) + { + Assert.That(TagConfigRegistry.TryGetDefinition(tagType, out TagDefinitionData definition), Is.True); + Assert.That(definition, Is.Not.Null); + Assert.That(definition.Category, Is.EqualTo(expectedCategory)); + Assert.That(definition.TriggerPhase, Is.EqualTo(expectedPhase)); + } + } + + public sealed class AttackPayloadFlowTests + { + [Test] + public void ShooterBullet_PassesAttackPayloadToDamageReceiver() + { + GameObject bulletObject = new GameObject("Bullet"); + GameObject targetObject = new GameObject("Target"); + try + { + ShooterBullet shooterBullet = bulletObject.AddComponent(); + RecordingDamageReceiver receiver = targetObject.AddComponent(); + targetObject.transform.position = new Vector3(0.01f, 0f, 0f); + + AttackPayload payload = new AttackPayload + { + BaseDamage = 80, + AttackPropertyType = AttackPropertyType.Fire, + TagRuntimes = new[] + { + new TagRuntimeData { TagType = TagType.Fire, TotalStack = 1 } + } + }; + BulletData bulletData = new BulletData(1, 501, Vector3.zero, targetObject.transform, payload, 12f); + + shooterBullet.OnShow(bulletData); + shooterBullet.Tick(0.02f); + + Assert.That(receiver.LastPayload, Is.Not.Null); + Assert.That(receiver.LastPayload.BaseDamage, Is.EqualTo(80)); + Assert.That(receiver.LastPayload.AttackPropertyType, Is.EqualTo(AttackPropertyType.Fire)); + Assert.That(receiver.LastPayload.TagRuntimes, Has.Length.EqualTo(1)); + Assert.That(receiver.LastPayload.TagRuntimes[0].TagType, Is.EqualTo(TagType.Fire)); + } + finally + { + Object.DestroyImmediate(bulletObject); + Object.DestroyImmediate(targetObject); + } + } + + private sealed class RecordingDamageReceiver : MonoBehaviour, IDamageReceiver + { + public AttackPayload LastPayload { get; private set; } + + public void TakeDamage(AttackPayload attackPayload) + { + LastPayload = attackPayload?.Clone(); + } + } + } +} diff --git a/Assets/Tests/EditMode/TagCombatRuntimeTests.cs.meta b/Assets/Tests/EditMode/TagCombatRuntimeTests.cs.meta new file mode 100644 index 0000000..476d590 --- /dev/null +++ b/Assets/Tests/EditMode/TagCombatRuntimeTests.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 86fbd79741310d04bae44ad6ec103c84 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/docs/CodeX-TODO.md b/docs/CodeX-TODO.md index 57e5285..8d56d4e 100644 --- a/docs/CodeX-TODO.md +++ b/docs/CodeX-TODO.md @@ -1,6 +1,6 @@ # CodeX TODO -最后更新:2026-03-09 +最后更新:2026-03-10 > 目标:基于当前仓库现状,为 `docs/TODO.md` 的 M1 收口补一份更可执行的补充顺序。 > 原则:先收主流程,再补硬规则,最后统一文档与验收口径。 @@ -130,7 +130,7 @@ | [x] | S4-03 | 先固化 Tag 系统设计与首发范围 | `docs/TagSystemDesign.md`
`docs/CodeX-TODO.md` | Tag 的来源、汇总、生效与首发集合口径固定 | | [x] | S4-04 | 实现组件实例 Tag 的统一生成入口 | `Assets/GameMain/Scripts/Definition/`
`Assets/GameMain/Scripts/CustomComponent/PlayerInventory/` | 掉落、商店、初始种子、事件奖励共用同一生成结果 | | [x] | S4-05 | 实现组塔后的 Tag 汇总与展示入口 | `Assets/GameMain/Scripts/Definition/`
`Assets/GameMain/Scripts/CustomComponent/PlayerInventory/`
`Assets/GameMain/Scripts/UI/` | 组件 Tag 可汇总为塔级结果,且 UI 展示口径一致 | -| [ ] | S4-06 | 实现首批基础 Tag 的战斗生效 | `Assets/GameMain/Scripts/Entity/`
`Assets/GameMain/Scripts/Components/`
`Assets/GameMain/Scripts/Definition/` | 首发 6~8 个基础 Tag 至少完成一批可验证效果 | +| [ ] | S4-06 | 实现首批基础 Tag 的战斗生效 | `Assets/GameMain/Scripts/Entity/`
`Assets/GameMain/Scripts/Components/`
`Assets/GameMain/Scripts/Definition/` | 首发 6~8 个基础 Tag 至少完成一批可验证效果,且战斗入口与状态运行时结构可继续扩展 | | [ ] | S4-07 | 补齐 Tag 规则与数据表的映射关系 | `Assets/GameMain/Scripts/DataTable/`
`Assets/GameMain/Scripts/Definition/` | 表字段不是只存在而未被消费,Tag 参数可配置可解释 | > 2026-03-09 更新:`InventoryRarityRuleService` 已落地;塔品质计算与组件品质归一化已统一收口,`PlayerInventoryTowerAssemblyService`、`ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 已接入同一规则入口。你已确认 Unity Test Runner 中 `Assets/Tests/EditMode` 全部通过,其中包含新增的 `InventoryRarityRuleServiceTests`。 @@ -140,6 +140,12 @@ > 2026-03-09 更新:`S4-04` 已落地 `InventoryTagRuleService` 与 `InventoryTagSourceType`;组件实例 Tag 现在统一按 `PossibleTag + Tag.txt.MinRarity + 品质预算` 生成,并只保留当前正式首发 7 个 Tag。`ShopFormUseCase`、`EnemyDropResolver`、`InventorySeedUtility` 已接入该入口,样例库存的组件与塔展示 Tag 已同步为统一结果;同时新增 `InventoryTagRuleServiceTests`。当前 CLI 下 `dotnet build GeometryTD.sln` 仍因本机缺少 Unity 引用和 `Unity.SourceGenerators*.dll` 失败,未能替代 Unity Test Runner 完成最终验证。 > > 2026-03-09 更新:`S4-05` 已新增 `TagRuntimeData` 与 `TowerTagAggregationService`;组塔与样例塔现在统一生成塔级 `TagRuntimes`,并保留兼容 `Tags` 投影。`RepoForm`、`CombatFinishForm`、`ItemDescForm` 的塔展示已切到聚合结果,重复 Tag 以 `xN` 文本显示;组件展示仍沿用组件实例 `Tags`。同时新增 `TowerTagAggregationServiceTests`;本轮改动后的最终验证仍以 Unity Test Runner 实跑结果为准。 +> +> 2026-03-10 更新:`S4-06` 已完成第一段落地。战斗链现在已从旧的 `damage + AttackPropertyType` 入口提升为携带 `TagRuntimeData[]` 的命中载荷;`AttackPayload`、`HitContext`、`TagEffectResolver`、`EnemyTagStatusRuntime` 已接入主命中链。当前已实际生效的基础 Tag 为 `Fire`、`Ice`、`Crit`、`Execution`:其中 `Fire` 已改为独立 DOT 公式,并按 `EnemyEntity` 每帧提供的 `deltaTime` 连续结算;`Ice` 已有独立减速状态;`Crit` 与 `Execution` 已进入命中前数值修正。状态类 Tag 已按“每个 Tag 各自配置文件、状态文件、效果文件”的方式拆开,`EnemyTagStatusRuntime` 只负责按激活 Tag 动态 Tick,不再持有具体 Tag 字段。 +> +> 2026-03-10 更新:当前 `S4-06` 仍未整体完成。`Shatter`、`Inferno`、`AbsoluteZero` 仍处于“已注册但 no-op 占位”的状态;`BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 仍只有分类与占位路由,没有实际战斗效果。因此当前仓库状态应视为“首批 4 个基础 Tag 已进入战斗,首发 7 个 Tag 尚未收满”,而不是 `S4-06` 全量完成。 +> +> 2026-03-10 更新:`S4-07` 仍未开始正式收口。当前 Tag 参数仍主要承载在代码侧 `TagConfigRegistry` 与各 Tag 配置类中,`Tag.txt` / DataTable 仍只提供基础字典与 `MinRarity` 输入;文档中约定的 `TagRule` 表、触发阶段、权重、效果参数等尚未形成 DataRow 与运行时消费闭环。因此 `S4-07` 继续保持未完成状态。 ### S4-01 边界结论 @@ -168,6 +174,36 @@ - 当前正式首发基础集合为:`Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero`。 - `BurnSpread`、`Pierce`、`Overpenetrate`、`FreezeMask`、`IgniteBurst` 默认放到后续扩展阶段,不作为 `S4` 当前完成标准。 +### S4 当前进度结论(2026-03-10) + +- `S4-02 ~ S4-05` 已完成,品质、组件实例 Tag 生成、塔级 `TagRuntimeData` 汇总、以及 UI 展示口径都已收口。 +- `S4-06` 已完成第一阶段:战斗链已支持 Tag 透传与统一结算,`Fire`、`Ice`、`Crit`、`Execution` 已有可验证效果,状态类 Tag 的内部结构也已从集中字段改为注册式运行时。 +- 当前 `S4-06` 的未完成部分集中在首发剩余 3 个 Tag:`Shatter`、`Inferno`、`AbsoluteZero`。它们虽然已进入 `TagType`、配置类、注册表和占位效果类,但还没有实际战斗语义。 +- `BurnSpread`、`IgniteBurst`、`FreezeMask`、`Pierce`、`Overpenetrate` 已进入分类与配置骨架,但仍属于后续扩展,不计入当前 `S4` 的完成标准。 +- `S4-07` 仍未开始正式实现。当前仓库只有“代码内注册表配置”,还没有把 `TagRule` 设计回写成 DataTable / DataRow / 运行时消费链。 + +### S4-06 当前代码状态 + +- 命中链路已统一为 `AttackPayload -> HitContext -> TagEffectResolver`,子弹命中时不再只传裸 `damage`。 +- `Tower` 侧的 `TagRuntimes` 已能透传到战斗,且保留了旧 `Tags -> TagRuntimes` 的兼容入口,避免旧塔或旧展示数据在战斗中完全失效。 +- 状态类 Tag 现在按单 Tag 文件拆分: + - 配置:每个 Tag 一个 `TagConfig` + - 运行时状态:每个状态类 Tag 一个 `TagState` + - 效果:每个状态类 Tag 一个 `TagEffect` +- `EnemyTagStatusRuntime` 已不再硬编码 `burn/slow` 字段,而是按激活的状态类 Tag 动态调度 Tick。 +- `Fire` 已改为独立 DOT 公式:当前使用独立 `BurnDamagePerSecondPerStack`,并按敌人实体每帧提供的 `deltaTime` 连续结算,不再依赖命中伤害或内部 `TickInterval`。 +- `Ice` 仍是独立减速状态,移速倍率通过状态运行时聚合得到。 +- `Crit`、`Execution` 已通过数值修正链路生效;`Shatter` 仍仅有占位配置与入口,没有实际增伤逻辑。 + +### S4 后续执行计划 + +1. 先补完 `S4-06` 的首发缺口,只做正式首发集合剩余的 `Shatter`、`Inferno`、`AbsoluteZero`,不提前展开传播、爆炸、穿透、多命中体系。 +2. `Shatter` 优先接入现有命中前数值修正链,直接消费“目标已减速”这一当前已存在的状态查询能力。 +3. `Inferno` 与 `AbsoluteZero` 优先作为对已有 `Fire` / `Ice` 的强化 Tag 落地,不额外引入第二套状态系统;要求仍沿用当前“单 Tag 配置 + 单 Tag 效果 + 注册式状态运行时”的结构。 +4. 首发 7 个 Tag 全部进入战斗并补齐对应 EditMode 测试后,再将 `S4-06` 标记为完成。 +5. 之后进入 `S4-07`:新增并消费 `TagRule` 表或等价 DataTable 映射,把当前代码内的 Tag 参数逐步迁移为可配置数据,而不是继续堆在 `TagConfigRegistry` 默认值里。 +6. `S4-07` 完成标准仍以“表字段被实际消费、参数可解释、运行时与文档口径一致”为准,不要求一步到位把全部 12 个 Tag 都配置化。 + ## 阶段 S5 - 收口耐久规则 | 状态 | ID | 任务 | 交付物路径 | 验收标准 | @@ -191,7 +227,7 @@ 1. 先做 `S1`,把主流程真正收成一条稳定可验收的链。 2. `S2` 已完成口径对齐,`NodeMapForm` 不再作为 M1 阻塞项。 3. 接下来优先做 `S3`,补齐出战合法性,解决 `P0-10`。 -4. 再做 `S4`,统一品质 / Tag 的规则口径,解决 `P0-11`。 +4. 再做 `S4`,统一品质 / Tag 的规则口径,解决 `P0-11`。其中当前优先级已经收口为:先补完 `S4-06` 的首发 7 Tag,再做 `S4-07` 的数据表映射。 5. 最后做 `S5`,决定耐久是完整收口还是同步缩范围,解决 `P0-12`。 6. 全部完成后做 `S6`,补测试并同步文档状态。 @@ -199,8 +235,9 @@ 1. `S1` 与 `S2` 已完成口径收口,可直接进入规则侧收尾 2. `S3-01 ~ S3-04` 已完成,当前可转入 `S4` -3. 接下来决定 `S4` 和 `S5` 是完整实现还是同步缩范围 -4. 最后补 `S6-01 ~ S6-04` +3. 当前先继续完成 `S4-06`,补齐 `Shatter`、`Inferno`、`AbsoluteZero` +4. 然后进入 `S4-07`,把 `TagRule` / DataTable 映射真正接进运行时 +5. 最后补 `S6-01 ~ S6-04` ## 备注 diff --git a/docs/TagSystemDesign.md b/docs/TagSystemDesign.md index 11bc803..39c0b8d 100644 --- a/docs/TagSystemDesign.md +++ b/docs/TagSystemDesign.md @@ -465,3 +465,205 @@ Tag 触发阶段固定拆成四段: - 第一批战斗效果优先做状态类与数值修正类,不优先做穿透 / 爆炸 / 传播 - MVP 正式首发集合固定为 `Fire`、`Ice`、`Crit`、`Execution`、`Shatter`、`Inferno`、`AbsoluteZero` - `BurnSpread` 明确后移,不作为当前首发集合成员 + +# 总体结构 + +我们设计 3 个流派方向: + +1. 🔥 元素爆发流(AOE清场) + +2. ❄ 控制叠层流(减速控制) + +3. 🎯 穿透暴击流(单体爆发) + +每个流派 4 个 Tag,其中: + +- 2 个基础属性 Tag + +- 1 个机制 Tag + +- 1 个高阶(稀有)Tag + +总 Tag 数 = 12 + +## 一、🔥 元素爆发流 + +核心定位: + +> 中等频率攻击 + 元素叠层 + 击杀爆炸清场 + +## 4 个 Tag +### 1️⃣ Fire(基础) + +效果: + +- 攻击附带灼烧(持续伤害) + +- 可叠加 3 层 + +### 2️⃣ BurnSpread(机制) + +触发: + +- 目标死亡时将灼烧传播给周围 2 个敌人 + +### 3️⃣ IgniteBurst(组合触发) + +触发条件: + +- 同一塔 ≥2 Fire Tag + +效果: + +- 满层灼烧爆炸(小范围AOE) + +4️⃣ Inferno(稀有) + +品质限定:紫及以上 + +效果: + +爆炸伤害 ×2 + +灼烧叠层上限 +2 + +流派触发逻辑 +条件 效果 +1 Fire 基础DOT +2 Fire IgniteBurst 激活 +Fire + BurnSpread 爆炸后传播 +Inferno + 2 Fire 大范围爆炸 +二、❄ 控制叠层流 + +核心定位: + +降速 + 冻结 + 稳定推进 + +4 个 Tag +1️⃣ Ice(基础) + +效果: + +攻击附带减速 + +可叠加 5 层 + +2️⃣ FreezeMark(机制) + +触发: + +目标叠满 5 层冻结 1 秒 + +3️⃣ Shatter(组合触发) + +触发: + +冻结目标受到暴击时额外伤害 + +4️⃣ AbsoluteZero(稀有) + +品质限定:紫及以上 + +效果: + +冻结时间 +50% + +冻结爆裂产生小范围伤害 + +流派触发逻辑 +条件 效果 +1 Ice 减速 +Ice + FreezeMark 满层冻结 +冻结 + Shatter 爆裂伤害 +AbsoluteZero 群控升级 +三、🎯 穿透暴击流 + +核心定位: + +低频高伤 + 直线穿透 + 单体爆发 + +4 个 Tag +1️⃣ Pierce(基础) + +效果: + +子弹穿透 2 个敌人 + +2️⃣ Crit(基础) + +效果: + ++15% 暴击率 + +3️⃣ Overpenetrate(机制) + +触发: + +穿透第一个敌人后伤害提升 30% + +4️⃣ Execution(稀有) + +品质限定:紫及以上 + +效果: + +暴击对生命低于30%目标伤害 ×2 + +流派触发逻辑 +条件 效果 +Pierce 直线清场 +Crit 随机爆发 +Pierce + Crit Overpenetrate 激活 +Execution 斩杀强化 +四、品质与 Tag 关系设计 + +建议规则: + +品质 Tag 数量 +白 1 +绿 1 +蓝 2 +紫 2 + 稀有概率 +红 3(含稀有) + +这样: + +低品质仍有存在价值 + +高品质增加构筑复杂度 + +稀有 Tag 不会泛滥 + +五、组合系统可视化建议 + +在组装界面显示: + +🔥 元素爆发(已激活) + +灼烧爆炸 + +传播效果 + +❄ 冰霜控制(未激活) + +需要 FreezeMark + +这样玩家会有“完成拼图”的感觉。 + +六、数值安全机制(防止爆炸) + +每个机制必须有: + +触发冷却(0.5~1秒) + +最大叠层限制 + +爆炸不触发爆炸(防无限连锁) + +群体上限目标数 + +例如: + +BurnSpread 最多传播 3 次。 + +否则后期怪物多时会帧率爆炸。 \ No newline at end of file