Damage System Guide
This guide covers intercepting, modifying, and applying damage in Deadworks plugins.
Intercepting Damage
Override OnTakeDamage to intercept all damage on the server:
public override HookResult OnTakeDamage(TakeDamageEvent ev)
{
var entity = ev.Entity;
var damageInfo = ev.Info;
// Block damage to boss NPCs
if (entity.DesignerName == "npc_boss_tier3")
return HookResult.Stop;
// Allow all other damage
return HookResult.Handled;
}
Filtering by Ability
Check which ability caused the damage using VData:
public override HookResult OnTakeDamage(TakeDamageEvent ev)
{
// Get the VData name of the ability that caused damage
var vdata = ev.Info.Ability?.SubclassVData;
if (vdata?.Name == "upgrade_discord")
{
// Apply special effect for this specific ability
ApplyDOTEffect(ev.Entity, ev.Info.Attacker);
}
return HookResult.Handled;
}
Applying Damage
Simple: Hurt()
The easiest way to damage an entity:
entity.Hurt(100f, attacker, inflictor, ability, flags: 0);
Hurt() Cannot Kill
Hurt() caps damage at 1 HP regardless of the amount — Hurt(99999f, ...) will leave the target at 1 HP, not kill them. To guarantee a kill, use CTakeDamageInfo with ForceDeath | AllowSuicide flags (see below).
Guaranteed Kill
Use CTakeDamageInfo with ForceDeath | AllowSuicide to ensure death:
[ConCommand("dw_killme")]
public void OnKillMe(ConCommandContext ctx)
{
var pawn = ctx.Controller?.GetHeroPawn();
if (pawn == null) return;
using var dmgInfo = new CTakeDamageInfo(99999f, attacker: pawn);
dmgInfo.DamageFlags |= TakeDamageFlags.ForceDeath | TakeDamageFlags.AllowSuicide;
pawn.TakeDamage(dmgInfo);
}
Advanced: CTakeDamageInfo
For full control over damage parameters:
using var info = new CTakeDamageInfo(
damage: 200f,
attacker: attackerEntity,
inflictor: inflictorEntity,
ability: null,
flags: 0
);
targetEntity.TakeDamage(info);
Damage Over Time (DOT)
Use Timer.Sequence for periodic damage:
private readonly EntityData<IHandle?> _dotTimers = new();
private void ApplyDOT(CBaseEntity target, CBaseEntity attacker, float duration, float interval, float fraction)
{
// Cancel existing DOT on this target
if (_dotTimers.TryGet(target, out var existing))
existing?.Cancel();
int maxTicks = (int)(duration * 1000 / interval);
var timer = Timer.Sequence(step =>
{
// Stop if target is dead
if (target.Health <= 0)
return step.Done();
// Calculate damage as fraction of max health
var maxHealth = target.As<CCitadelPlayerPawn>()?.Controller?.PlayerDataGlobal?.MaxHealth ?? 1000;
float damage = maxHealth * fraction;
target.Hurt(damage, attacker, attacker, null);
target.EmitSound("Damage.Send.Crit");
if (step.Run >= maxTicks)
return step.Done();
return step.Wait((int)interval.Milliseconds());
});
_dotTimers[target] = timer;
}
Complete DOT Plugin
See the Scourge Example for a full implementation with configuration.
Currency Interception
Control gold and ability point flow:
public override HookResult OnModifyCurrency(ModifyCurrencyEvent ev)
{
// Block all currency gain
return HookResult.Stop;
}
Selective Currency Control
public override HookResult OnModifyCurrency(ModifyCurrencyEvent ev)
{
// Only allow starting gold, block everything else
if (ev.Source == ECurrencySource.StartingGold)
return HookResult.Handled;
return HookResult.Stop;
}
Re-issuing Currency
After blocking natural currency gain, issue custom amounts:
[GameEventHandler("player_hero_changed")]
public HookResult OnHeroChanged(GameEvent ev)
{
var pawn = ev.GetPlayerPawn("player") as CCitadelPlayerPawn;
if (pawn == null) return HookResult.Handled;
// Give custom starting currency
pawn.ModifyCurrency(ECurrencyType.Gold, 15000, ECurrencySource.FlagCapture, false, false, false);
pawn.ModifyCurrency(ECurrencyType.AbilityPoints, 17, ECurrencySource.FlagCapture, false, false, false);
return HookResult.Handled;
}
See Also
- Damage API Reference — Full API details
- Timers — Timer.Sequence for DOT
- Players — Currency types and management
- Scourge Example — Complete DOT plugin
- Deathmatch Example — Currency control in practice