Painted units correspond to damage types
This commit is contained in:
parent
04ead32846
commit
2be2e52fe4
3 changed files with 242 additions and 75 deletions
|
|
@ -226,73 +226,162 @@ namespace TD.Combat
|
|||
{
|
||||
Vector3 targetPos = currentTarget.transform.position;
|
||||
|
||||
// Resolve the effective combat profile for this shot: the tower's authored
|
||||
// stats, overridden by its Paint color (Red = splash, Green = poison DoT,
|
||||
// Blue = slow). Unpainted towers fire exactly as authored.
|
||||
PaintColor paint = towerInstance.Paint;
|
||||
CombatProfile profile = BuildProfile(def, paint);
|
||||
|
||||
if (def.ProjectilePrefab == null)
|
||||
{
|
||||
// Hitscan — apply damage this frame.
|
||||
ApplyDamageToTarget(def, currentTarget, targetPos);
|
||||
ApplyDamageToTarget(profile, currentTarget, targetPos);
|
||||
}
|
||||
else
|
||||
{
|
||||
SpawnProjectile(def, currentTarget);
|
||||
SpawnProjectile(def, profile, paint, currentTarget);
|
||||
}
|
||||
|
||||
FireClientRpc(targetPos);
|
||||
}
|
||||
|
||||
// ----- Paint → combat effect tuning --------------------------------
|
||||
//
|
||||
// Paint overrides the tower's effect profile (paint takes precedence over any
|
||||
// authored effect). Centralized here so the numbers are easy to find and tweak;
|
||||
// promote to a ScriptableObject if designers need to tune them without a recompile.
|
||||
private const float PaintSplashRadius = 2.0f; // world units (tiles)
|
||||
private const float PaintPoisonDpsFraction = 0.5f; // DoT/sec = base Damage × this
|
||||
private const float PaintPoisonDuration = 3.0f; // seconds
|
||||
private const float PaintSlowSpeedRetained = 0.5f; // Cold magnitude: 0.5 = half speed
|
||||
private const float PaintSlowDuration = 2.0f; // seconds
|
||||
|
||||
// The firing-relevant stats for a single shot, after applying paint overrides.
|
||||
// Mirrors the TowerDefinition fields the firing path reads so the rest of the
|
||||
// code is agnostic to whether a value came from the asset or from paint.
|
||||
private readonly struct CombatProfile
|
||||
{
|
||||
public readonly float Damage;
|
||||
public readonly DamageType DamageType;
|
||||
public readonly TargetType TargetType;
|
||||
public readonly float SplashRadius;
|
||||
public readonly float SlowFactor;
|
||||
public readonly float DotDamagePerSecond;
|
||||
public readonly float EffectDuration;
|
||||
public readonly float ProjectileSpeed;
|
||||
public readonly int ChainCount;
|
||||
public readonly float ChainRange;
|
||||
|
||||
public CombatProfile(float damage, DamageType damageType, TargetType targetType,
|
||||
float splashRadius, float slowFactor, float dotDamagePerSecond,
|
||||
float effectDuration, float projectileSpeed, int chainCount, float chainRange)
|
||||
{
|
||||
Damage = damage;
|
||||
DamageType = damageType;
|
||||
TargetType = targetType;
|
||||
SplashRadius = splashRadius;
|
||||
SlowFactor = slowFactor;
|
||||
DotDamagePerSecond = dotDamagePerSecond;
|
||||
EffectDuration = effectDuration;
|
||||
ProjectileSpeed = projectileSpeed;
|
||||
ChainCount = chainCount;
|
||||
ChainRange = chainRange;
|
||||
}
|
||||
}
|
||||
|
||||
private static CombatProfile BuildProfile(TowerDefinition def, PaintColor paint)
|
||||
{
|
||||
// Start from the tower's authored stats.
|
||||
float damage = def.Damage;
|
||||
DamageType damageType = def.DamageType;
|
||||
TargetType targetType = def.TargetType;
|
||||
float splash = def.SplashRadius;
|
||||
float slow = def.SlowFactor;
|
||||
float dot = def.DotDamagePerSecond;
|
||||
float duration = def.EffectDuration;
|
||||
|
||||
switch (paint)
|
||||
{
|
||||
case PaintColor.Red: // area splash
|
||||
targetType = TargetType.Splash;
|
||||
splash = PaintSplashRadius;
|
||||
break;
|
||||
|
||||
case PaintColor.Green: // poison damage-over-time
|
||||
damageType = DamageType.Poison;
|
||||
dot = def.Damage * PaintPoisonDpsFraction;
|
||||
duration = PaintPoisonDuration;
|
||||
break;
|
||||
|
||||
case PaintColor.Blue: // chilling slow
|
||||
damageType = DamageType.Cold;
|
||||
slow = PaintSlowSpeedRetained;
|
||||
duration = PaintSlowDuration;
|
||||
break;
|
||||
|
||||
case PaintColor.None:
|
||||
default:
|
||||
break; // fire as authored
|
||||
}
|
||||
|
||||
return new CombatProfile(damage, damageType, targetType, splash, slow, dot,
|
||||
duration, def.ProjectileSpeed, def.ChainCount, def.ChainRange);
|
||||
}
|
||||
|
||||
// ----- Damage application ------------------------------------------
|
||||
|
||||
private void ApplyDamageToTarget(TowerDefinition def, EnemyHealth primary, Vector3 primaryPos)
|
||||
private void ApplyDamageToTarget(in CombatProfile p, EnemyHealth primary, Vector3 primaryPos)
|
||||
{
|
||||
PlayerSlot owner = towerInstance.Owner;
|
||||
|
||||
switch (def.TargetType)
|
||||
switch (p.TargetType)
|
||||
{
|
||||
case TargetType.Single:
|
||||
HitEnemy(def, primary, owner);
|
||||
HitEnemy(p, primary, owner);
|
||||
break;
|
||||
|
||||
case TargetType.Splash:
|
||||
HitEnemy(def, primary, owner);
|
||||
ApplySplash(def, primary, primaryPos, owner);
|
||||
HitEnemy(p, primary, owner);
|
||||
ApplySplash(p, primary, primaryPos, owner);
|
||||
break;
|
||||
|
||||
case TargetType.Chain:
|
||||
ApplyChain(def, primary, owner);
|
||||
ApplyChain(p, primary, owner);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySplash(TowerDefinition def, EnemyHealth primary,
|
||||
private void ApplySplash(in CombatProfile p, EnemyHealth primary,
|
||||
Vector3 origin, PlayerSlot owner)
|
||||
{
|
||||
if (def.SplashRadius <= 0f) return;
|
||||
if (p.SplashRadius <= 0f) return;
|
||||
|
||||
int count = Physics.OverlapSphereNonAlloc(
|
||||
origin, def.SplashRadius, s_overlapBuffer, enemyLayerMask);
|
||||
origin, p.SplashRadius, s_overlapBuffer, enemyLayerMask);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
var eh = s_overlapBuffer[i].GetComponent<EnemyHealth>();
|
||||
if (eh == null || eh.IsDead || (object)eh == (object)primary) continue;
|
||||
HitEnemy(def, eh, owner);
|
||||
HitEnemy(p, eh, owner);
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyChain(TowerDefinition def, EnemyHealth primary, PlayerSlot owner)
|
||||
private void ApplyChain(in CombatProfile p, EnemyHealth primary, PlayerSlot owner)
|
||||
{
|
||||
var hitPositions = new List<Vector3> { primary.transform.position };
|
||||
var alreadyHit = new HashSet<EnemyHealth> { primary };
|
||||
|
||||
HitEnemy(def, primary, owner);
|
||||
HitEnemy(p, primary, owner);
|
||||
|
||||
EnemyHealth current = primary;
|
||||
for (int jump = 0; jump < def.ChainCount; jump++)
|
||||
for (int jump = 0; jump < p.ChainCount; jump++)
|
||||
{
|
||||
EnemyHealth next = null;
|
||||
float bestSqr = float.MaxValue;
|
||||
|
||||
int count = Physics.OverlapSphereNonAlloc(
|
||||
current.transform.position, def.ChainRange, s_overlapBuffer, enemyLayerMask);
|
||||
current.transform.position, p.ChainRange, s_overlapBuffer, enemyLayerMask);
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
|
|
@ -307,40 +396,41 @@ namespace TD.Combat
|
|||
|
||||
alreadyHit.Add(next);
|
||||
hitPositions.Add(next.transform.position);
|
||||
HitEnemy(def, next, owner);
|
||||
HitEnemy(p, next, owner);
|
||||
current = next;
|
||||
}
|
||||
|
||||
ChainFiredClientRpc(hitPositions.ToArray());
|
||||
}
|
||||
|
||||
private void HitEnemy(TowerDefinition def, EnemyHealth target, PlayerSlot owner)
|
||||
private void HitEnemy(in CombatProfile p, EnemyHealth target, PlayerSlot owner)
|
||||
{
|
||||
target.TakeDamage(def.Damage, def.DamageType, owner);
|
||||
ApplyStatusEffect(def, target, owner);
|
||||
target.TakeDamage(p.Damage, p.DamageType, owner);
|
||||
ApplyStatusEffect(p, target, owner);
|
||||
}
|
||||
|
||||
private void ApplyStatusEffect(TowerDefinition def, EnemyHealth target, PlayerSlot owner)
|
||||
private void ApplyStatusEffect(in CombatProfile p, EnemyHealth target, PlayerSlot owner)
|
||||
{
|
||||
if (def.EffectDuration <= 0f) return;
|
||||
if (p.EffectDuration <= 0f) return;
|
||||
|
||||
float magnitude = def.DamageType switch
|
||||
float magnitude = p.DamageType switch
|
||||
{
|
||||
DamageType.Cold => def.SlowFactor,
|
||||
DamageType.Fire => def.DotDamagePerSecond,
|
||||
DamageType.Poison => def.DotDamagePerSecond,
|
||||
DamageType.Cold => p.SlowFactor,
|
||||
DamageType.Fire => p.DotDamagePerSecond,
|
||||
DamageType.Poison => p.DotDamagePerSecond,
|
||||
_ => 0f,
|
||||
};
|
||||
|
||||
if (magnitude <= 0f) return;
|
||||
|
||||
target.GetComponent<EnemyStatus>()
|
||||
?.ApplyEffect(def.DamageType, magnitude, def.EffectDuration, owner);
|
||||
?.ApplyEffect(p.DamageType, magnitude, p.EffectDuration, owner);
|
||||
}
|
||||
|
||||
// ----- Projectile spawning -----------------------------------------
|
||||
|
||||
private void SpawnProjectile(TowerDefinition def, EnemyHealth target)
|
||||
private void SpawnProjectile(TowerDefinition def, in CombatProfile p,
|
||||
PaintColor tint, EnemyHealth target)
|
||||
{
|
||||
var go = Instantiate(def.ProjectilePrefab, transform.position, Quaternion.identity);
|
||||
|
||||
|
|
@ -356,16 +446,17 @@ namespace TD.Combat
|
|||
|
||||
proj.InitializeServer(
|
||||
target,
|
||||
def.Damage,
|
||||
def.DamageType,
|
||||
def.TargetType,
|
||||
def.SplashRadius,
|
||||
def.SlowFactor,
|
||||
def.DotDamagePerSecond,
|
||||
def.EffectDuration,
|
||||
def.ProjectileSpeed,
|
||||
p.Damage,
|
||||
p.DamageType,
|
||||
p.TargetType,
|
||||
p.SplashRadius,
|
||||
p.SlowFactor,
|
||||
p.DotDamagePerSecond,
|
||||
p.EffectDuration,
|
||||
p.ProjectileSpeed,
|
||||
enemyLayerMask,
|
||||
towerInstance.Owner);
|
||||
towerInstance.Owner,
|
||||
tint);
|
||||
|
||||
go.GetComponent<NetworkObject>().Spawn();
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue