Start Tag System

This commit is contained in:
SepComet 2026-03-10 13:11:30 +08:00
parent 34ef001ef3
commit 52f9e212b9
107 changed files with 1850 additions and 50 deletions

View File

@ -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`).
- `<EFBFBD><EFBFBD><EFBFBD>ݱ<EFBFBD>/` 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.

View File

@ -4,6 +4,6 @@ namespace Components
{
public interface IDamageReceiver
{
void TakeDamage(int damage, AttackPropertyType attackPropertyType);
void TakeDamage(AttackPayload attackPayload);
}
}

View File

@ -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);
}
}
}

View File

@ -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;
}
}

View File

@ -20,14 +20,21 @@ namespace Components
[SerializeField] private SpriteRenderer _renderer;
private TagRuntimeData[] _tagRuntimes = System.Array.Empty<TagRuntimeData>();
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<TagRuntimeData>();
}
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>();
}
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;
}
}
}

View File

@ -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<TagRuntimeData>();
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<TagRuntimeData>();
_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>();
}
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<TagRuntimeData>();
}
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);

View File

@ -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<TagRuntimeData>();
public AttackPayload Clone()
{
return new AttackPayload
{
BaseDamage = BaseDamage,
AttackPropertyType = AttackPropertyType,
TagRuntimes = InventoryCloneUtility.CloneTagRuntimes(TagRuntimes)
};
}
}
}

View File

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

View File

@ -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; }
}
}

View File

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

View File

@ -0,0 +1,10 @@
namespace GeometryTD.Definition
{
public enum TagCategory : byte
{
None = 0,
Status = 1,
NumericModifier = 2,
AttackShape = 3
}
}

View File

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

View File

@ -0,0 +1,11 @@
namespace GeometryTD.Definition
{
public enum TagTriggerPhase : byte
{
None = 0,
OnBeforeHit = 1,
OnHit = 2,
OnAfterHit = 3,
OnKill = 4
}
}

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a2a3c49c622040edbd52d0c66df2377e
timeCreated: 1773105664

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1ff28b0dd76d49ecb0f6e615df4f7a62
timeCreated: 1773105681

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1909b51822cc4368bc604674fd119123
timeCreated: 1773105715

View File

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace GeometryTD.Definition
{
public sealed class AbsoluteZeroTagEffect : EnemyStatusTagEffectBase
{
public override TagType TagType => TagType.AbsoluteZero;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: deecaa9a38c04725b5abeb5488cbe53f
timeCreated: 1773106137

View File

@ -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<int> applyDamage)
{
_ = runtime;
_ = deltaTime;
_ = applyDamage;
return false;
}
public virtual float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime)
{
_ = runtime;
return 1f;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: aefaa5745cc94c0aae92109e64d83dc1
timeCreated: 1773105755

View File

@ -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<FireTagState>(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<int> applyDamage)
{
FireTagState state = runtime.GetState<FireTagState>(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;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6609b360347c4fc9bccbe288efa60ae4
timeCreated: 1773105786

View File

@ -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<int> applyDamage);
float GetMoveSpeedMultiplier(EnemyTagStatusRuntime runtime);
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 07eac037b9274ddaa6352412c9043f62
timeCreated: 1773105725

View File

@ -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<IceTagState>(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<int> applyDamage)
{
_ = applyDamage;
IceTagState state = runtime.GetState<IceTagState>(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<IceTagState>(TagType);
return state == null ? 1f : state.SlowMultiplier;
}
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 0787cd2263b441ceba5b3ad381827677
timeCreated: 1773106119

View File

@ -0,0 +1,7 @@
namespace GeometryTD.Definition
{
public sealed class InfernoTagEffect : EnemyStatusTagEffectBase
{
public override TagType TagType => TagType.Inferno;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 2aa813ec42ea4dddb717ff28da764f15
timeCreated: 1773106082

View File

@ -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<TagType, IEnemyStatusTagEffect> EffectsByTag =
new Dictionary<TagType, IEnemyStatusTagEffect>
{
[TagType.Fire] = new FireTagEffect(),
[TagType.Inferno] = new InfernoTagEffect(),
[TagType.Ice] = new IceTagEffect(),
[TagType.AbsoluteZero] = new AbsoluteZeroTagEffect()
};
public static IReadOnlyDictionary<TagType, IEnemyStatusTagEffect> Effects => EffectsByTag;
public static bool TryGetEffect(TagType tagType, out IEnemyStatusTagEffect effect)
{
return EffectsByTag.TryGetValue(tagType, out effect);
}
}
}

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: c90264b23a9c45c0b21bc627a6109282
timeCreated: 1773105821

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: a92545f498ff4b8ab974a73d220f2cbc
timeCreated: 1773105984

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 16fa8f668caf4a6a9d384f53c86ef6d8
timeCreated: 1773105868

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 21c22a1b79d74aa58ef0054fed71b90a
timeCreated: 1773106012

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 865ffdb6e1df446fabfb2355d06ef60a
timeCreated: 1773106044

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3225e9a8d28b4133a6484f576c4662ae
timeCreated: 1773105844

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: fa016e0d4c76466b8fb665656ee4bc9e
timeCreated: 1773105940

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 546b506913154d74a9a7cd4b3aa36d12
timeCreated: 1773105919

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: b412ab82145242e3a7ce99991199cc35
timeCreated: 1773105884

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: be707e334ca74a2a8a2a9d7773e70138
timeCreated: 1773105897

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 1d017f96198041cc97f2db1cb4f84fcb
timeCreated: 1773106028

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 3081fa157ac74961a2a4ffb3a6e10851
timeCreated: 1773105999

View File

@ -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;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: d0f84f8e59bd4d0ea620742802223d87
timeCreated: 1773105968

View File

@ -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;
}
}
}

View File

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

View File

@ -0,0 +1,103 @@
using System.Collections.Generic;
namespace GeometryTD.Definition
{
public static class TagConfigRegistry
{
private static readonly Dictionary<TagType, TagDefinitionData> DefinitionsByTag =
new Dictionary<TagType, TagDefinitionData>
{
[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<TagType, TagDefinitionData> Definitions => DefinitionsByTag;
public static bool TryGetDefinition(TagType tagType, out TagDefinitionData definition)
{
return DefinitionsByTag.TryGetValue(tagType, out definition);
}
}
}

View File

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

View File

@ -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; }
}
}

View File

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

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 6fb08c76711246c2afa02d60769d76ef
timeCreated: 1773106291

View File

@ -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;
}
}
}
}
}

View File

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

View File

@ -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;
}
}
}

View File

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

View File

@ -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;
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,7 @@
namespace GeometryTD.Definition
{
public abstract class EnemyStatusTagStateBase
{
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 36215e56239c41c5bb196de6927643a8
timeCreated: 1773105536

View File

@ -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; }
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 20ad1b81a1634d06b9c8bbadec37bcd4
timeCreated: 1773105566

View File

@ -0,0 +1,8 @@
namespace GeometryTD.Definition
{
public sealed class IceTagState : EnemyStatusTagStateBase
{
public float RemainingDuration { get; set; }
public float SlowMultiplier { get; set; } = 1f;
}
}

View File

@ -0,0 +1,3 @@
fileFormatVersion: 2
guid: 43c7eb87fa774e60a894f4cc8e0089a5
timeCreated: 1773105599

View File

@ -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();
}
}
}

View File

@ -21,8 +21,13 @@ namespace GeometryTD.Entity
private readonly List<Vector3> _pathPoints = new();
private int _pathPointIndex;
private bool _isDespawnRequested;
private readonly EnemyTagStatusRuntime _tagStatusRuntime = new();
public static IReadOnlyList<EnemyEntity> 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();
}
}
}

Some files were not shown because too many files have changed in this diff Show More