# Combat Owns attributes, the one damage funnel, health and the downed state, melee hit detection and feel, enemies, the ability system, class kits, and the rules that keep gear and mods from dissolving the classes. It does **not** own how a body moves ([Movement.md](Movement.md), whose root-motion hooks the movement abilities use), how a player touches a downed teammate ([Interaction.md](Interaction.md)), or where a weapon's numbers come from when the weapon was crafted ([Crafting.md](Crafting.md), which produces the `FWeaponProfile` defined here). Read [Architecture.md](Architecture.md) first. Steps 7 to 11 in [`../Steps.md`](../Steps.md). ## Decisions ``` [DECIDED] The Gameplay Ability System. Attributes, abilities, effects, cooldowns, tags, cues. The earlier project hand-built a damage resolver, a health service, a cooldown tracker, an ability runner and a client-to-host relay, then spent a step making them replicate. GAS is those five things, server-authoritative with client prediction, already replicated, and the engine's own combat samples are written on it. The cost is its learning curve, which is paid once, and its opinions, which happen to be this project's: one attribute set, effects as data, abilities as classes, everything addressed by tag. ``` ``` [DECIDED] One damage funnel. Every point of damage in the game passes through one execution calculation. Melee, projectiles, abilities, falling, hazards, a shove into a pit. One place, for three reasons: modifiers from gear and upgrades apply in one place, telemetry is emitted in one place, and the no-friendly-fire rule is enforced in one place rather than remembered at twenty call sites. No ability, weapon or hazard writes Health directly. ``` ``` [DECIDED] Down before death. Zero health is downed: immobile, revivable, bleeding out. Death is what happens when nobody comes. Any player can revive; some kits do it faster. When no player is standing, everyone downed dies at once, because nobody can revive anybody. ``` ``` [DECIDED] The ability system component is on the player state for players and on the pawn for enemies. A player's cooldowns, attributes and class survive their body; an enemy's die with it. ``` ``` [SALVAGED] Kits define ability access. Gear defines stat and utility access, and crosses class lines. Mods bend numbers and change how an ability you have behaves, and never grant another class's signature ability. Without that line, free-form gear dissolves five classes into five slightly different damage dealers within one content patch. Gear and mods themselves are not built in these steps; the flag that guards the line is, because it is free now. ``` ## Layout ``` Source/Core/Combat/ ├── WeaponProfile.h // FWeaponProfile: the one shape both authored and crafted weapons produce ├── DamageMath.h / .cpp // pure: ComputeDamage(context) -> FDamageResult └── ThreatTable.h / .cpp // pure: who an enemy wants to hit Source//Combat/ // the stat block and the ability system component live in Stats/, see Stats.md ├── DamageExecution.h // UGameplayEffectExecutionCalculation: THE funnel ├── CombatantComponent.h // per body: team, downed/dead state machine, revive target, hit reaction ├── WeaponDefinition.h // UPrimaryDataAsset for authored weapons ├── WeaponActor.h // the held weapon: mesh on the hand socket, its profile, its trace sockets ├── Abilities/ │ ├── KitAbility.h // UGameplayAbility base: InputTag, bSignature, activation policy │ ├── GA_MeleeAttack.h // montage, hit window, server sweep, apply damage │ ├── GA_Dodge.h GA_Blink.h GA_ShoulderCharge.h GA_Taunt.h GA_Mark.h GA_Heal.h GA_Downed.h GA_Revive.h │ └── GA_Fireball.h GA_Volley.h // step 11, projectiles ├── ClassKitDefinition.h // UPrimaryDataAsset: abilities in slot order, starting weapon, base attributes ├── Enemies/ │ ├── EnemyDefinition.h // UPrimaryDataAsset: stats, montages, drops later │ ├── EnemyCharacter.h // ABaseCharacter with its own ASC │ ├── EnemyController.h // AAIController + StateTree + perception + the threat table │ └── EncounterSubsystem.h // UWorldSubsystem, server: spawns and counts, party-size scaling ├── ProjectileActor.h // step 11 └── Cues/ // GameplayCue notifies: impact, hit stop, camera kick, outline flash Content/Combat/ ├── Definitions/DA_Weapon_Sword, DA_Weapon_Mace, DA_Weapon_Dagger, DA_Weapon_Staff, DA_Weapon_Bow ├── Definitions/DA_Kit_Warrior, DA_Kit_Ranger, DA_Kit_Mage, DA_Kit_Cleric, DA_Kit_Rogue ├── Definitions/DA_Enemy_Goblin ├── Effects/GE_Damage, GE_Heal, GE_Cooldown_*, GE_BleedOut, GE_Invulnerable, GE_FallDamage ├── Abilities/GA_* // Blueprint children setting montages and cues only ├── Animations/AM_Melee_*, AM_Hit_* // montages with the hit-window notify states └── Cues/GC_* ``` ## Types at a glance | Type | Module | Lifetime | Notes | | --- | --- | --- | --- | | `FWeaponProfile` | Core | value | Authored or crafted, same struct | | `DamageMath::ComputeDamage` | Core | pure | The maths the execution calls | | `FThreatTable` | Core | value | Owned by an enemy controller | | `UStatBlockAttributeSet` | Gameplay (Stats) | per ASC | The stat block, see [Stats.md](Stats.md) | | `UDamageExecution` | Gameplay | asset-referenced | Referenced by `GE_Damage` | | `UCombatantComponent` | Gameplay | per body | Team, downed state, revive target, hit reaction | | `UWeaponDefinition`, `UClassKitDefinition`, `UEnemyDefinition` | Gameplay | asset | Content | | `AWeaponActor` | Gameplay | per equipped weapon | Attached to the hand socket | | `UKitAbility` and children | Gameplay | granted per ASC | Abilities | | `AEnemyCharacter`, `AEnemyController` | Gameplay | per enemy | Server-driven | | `UEncounterSubsystem` | Gameplay | world, server | Spawning and the wipe | ## Attributes The numbers combat reads and writes are on the one stat block every body carries, `UStatBlockAttributeSet`, specified in [Stats.md](Stats.md): `Health` and `MaxHealth`, `AttackPower` and `AttackSpeed`, `Armour`, `MagicResist` and `KnockbackResist`, and the two meta attributes the funnel writes. Combat owns what they mean; it does not own the set, and it adds nothing to it without a row in that document's table. ```cpp // UStatBlockAttributeSet, the parts combat uses ATTRIBUTE_ACCESSORS(UStatBlockAttributeSet, IncomingDamage) // META: written by the execution, consumed below, never replicated ATTRIBUTE_ACCESSORS(UStatBlockAttributeSet, IncomingHeal) // META, same virtual void PostGameplayEffectExecute(const FGameplayEffectModCallbackData& Data) override; // IncomingDamage: Health = clamp(Health - Incoming, 0, Max); if it reached zero and was not zero, raise // OnOutOfHealth(instigator, causer, context). Emit player_damaged / enemy_damaged. Clear the meta attribute. // IncomingHeal: overheal past MaxHealth is returned to the caller as a value, because a heal that overflows // is a launch (the salvaged joke, see Abilities). Health clamps; the surplus goes out through OnOverheal. ``` Base values are a `FStatBlockDefaults` on the kit or the enemy definition, applied through the one init effect on grant or spawn; nothing sets an attribute directly. A body's speed does not live in combat at all: it is `MoveSpeed` on the block, and a slow from a frost bolt changes it the same way mud does. ## The damage funnel ```cpp // Source/Core/Combat/DamageMath.h // pure struct FDamageContext { float BaseAmount; // from the weapon profile or the ability's SetByCaller Data.Damage FGameplayTag DamageType; // Damage.Type.Physical | Magic | True FGameplayTag Source; // Damage.Source.Melee | Projectile | Ability | Fall | Hazard | Shove float SourceAttackPower = 1.f; // captured attribute float TargetArmour = 0.f; // captured attribute, physical float TargetMagicResist = 0.f; // captured attribute, magic bool bSourceIsPlayer = false, bTargetIsPlayer = false; bool bTargetMarked = false; // Status.Debuff.Marked: everyone hits harder bool bTargetInvulnerable = false; // dodge i-frames, a shield FGameplayTagContainer TargetImmunities; // Immune.Damage. tags on the target: matching damage is zero FGameplayTagContainer GrantedTags; // from an enchanted weapon: Item.Enchant.Fire.Penetration ... }; struct FDamageResult { float FinalAmount = 0.f; bool bDiscardedFriendlyFire = false; bool bCritical = false; }; namespace DamageMath { // pure. Tested for: the friendly-fire gate; True damage ignores armour and resist; a mark multiplies; // invulnerable is zero; an Immune.Damage. tag zeroes that type; fall damage is never friendly fire // even when a player caused the fall. FDamageResult ComputeDamage(const FDamageContext& Ctx, const struct FDamageTuning& Tuning); } ``` ```cpp /** * THE funnel. Referenced by GE_Damage and by nothing else; every damaging effect in the project is GE_Damage with * SetByCaller magnitudes, or a subclass of it. Runs on the server only, as executions do. */ UCLASS() class UDamageExecution : public UGameplayEffectExecutionCalculation { GENERATED_BODY() public: UDamageExecution(); // captures AttackPower from the source; Armour, MagicResist and the State, Status and Immune tags from the target virtual void Execute_Implementation(const FGameplayEffectCustomExecutionParameters& Params, FGameplayEffectCustomExecutionOutput& Out) const override; // 1. Build FDamageContext from the spec: Data.Damage, the type and source tags on the spec, the captured // attributes, team membership of instigator and target (IGenericTeamAgentInterface), target state tags. // 2. THE FRIENDLY FIRE GATE. Both players: FinalAmount is zero. The effect still applies, so the impulse // and the cue survive. Player-on-player is impulse and ragdoll, never health loss. This is the // consequence-free griefing rule from the earlier project, enforced here so no ability can violate it. // 3. DamageMath::ComputeDamage. Modifiers from gear read the source's granted tags and attributes; nothing // is pushed into this class by another system, it reads. // 4. AddOutputModifier(IncomingDamage, Additive, FinalAmount). // 5. The attribute set's PostGameplayEffectExecute emits telemetry and raises OnOutOfHealth. This class // emits nothing itself: one emitting site per event. }; ``` Fall damage is a `GE_FallDamage` applied by the character's `Landed` through the same funnel with `Damage.Source.Fall` and no instigator; hazards are the same with `Hazard`. A shove is `GE_Damage` with zero base amount and an `Impulse` magnitude: the gate discards nothing because there is nothing, and the impulse cue lands. ## Health, down, revive, wipe ```cpp /** Per body. Turns attribute events into the downed/dead state and exposes the revive prop. */ UCLASS() class UCombatantComponent : public UActorComponent, public IInteractable { GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly) FGenericTeamId Team; // players 1, enemies 2; the attitude solver says 1 vs 2 is hostile FGuid CombatantId; // stable runtime identity for the threat table and telemetry // Server, on OnOutOfHealth: // Enemies: grant State.Dead, play the death montage through a cue, disable collision, the encounter counts it. // Players: ASC->TryActivateAbilityByClass(GA_Downed). GA_Downed grants State.Downed, applies GE_BleedOut // (30 s duration, on expiry -> Die), applies GE_Downed (MoveSpeed overridden to zero), forces a drop through the carry // component, drops the camera, and enables this component's revive collider on the Interactable channel. // Then: if no player in the world is standing, every downed player dies now (a solo down is an instant wipe; // bleed-out only matters with a party) and the encounter subsystem's wipe fires. // IInteractable, the revive prop: // GetPrompt: Interact.Verb.Revive, enabled when the target has State.Downed and the asker does not. // Interact (server): activate GA_Revive on the reviver, which holds for ReviveSeconds (faster for a Cleric), // then removes GE_BleedOut and restores a fraction of MaxHealth. Emit player_revived with down_duration_s. // Hit reaction: OnDamaged raises HitReaction_Cosmetic(direction); the animation blueprint bends the spine with // a damped spring in the direction of the hit, on top of whatever plays, and never moves the hands. }; ``` A downed player is the first prop in the game, and reviving gets the server-side range and permission re-check for free from [Interaction.md](Interaction.md). It is the reason interaction is built before combat. ## Weapons and the melee swing ```cpp // Source/Core/Combat/WeaponProfile.h /** * The one shape a weapon has at the moment of the swing. An authored UWeaponDefinition produces it directly; a * crafted item produces it through CraftingRules::MakeWeaponProfile. The melee ability, the action bar and the * damage funnel read this and nothing else, which is the seam that lets a crafted sword be a sword. */ struct FWeaponProfile { FGameplayTag WeaponTag; // Item.Weapon.Sword; a crafted item carries its family tag here float Damage = 12.f; float ReachCm = 180.f; float ArcHalfWidthCm = 60.f, ArcHalfHeightCm = 50.f; float WindupSeconds = 0.12f; // press to hit check: the tell float SwingSeconds = 0.55f; // press to next press float HitStopSeconds = 0.06f; // cosmetic, client side FGameplayTag DamageType; // Damage.Type.Physical by default FGameplayTagContainer GrantedTags; // an enchanted blade grants Item.Enchant.* here bool bTwoHanded = false; // a greatsword occupies both hands, see Interaction.md FSoftObjectPath Montage; // AM_Melee_*; a crafted weapon uses its family's montage }; ``` ```cpp UCLASS() class UGA_MeleeAttack : public UKitAbility { GENERATED_BODY() // Net execution policy: LocalPredicted. The montage plays immediately on the owner; the server plays it too and // does the hit check itself. Activation: on Input.Attack, blocked by State.Downed, State.Carrying.TwoHanded, // State.Dodging, and by its own cooldown (SwingSeconds, a GE_Cooldown_Melee with SetByCaller duration). // // ActivateAbility: // Profile = Owner->GetWeaponActor()->GetProfile(); timings divided by the body's AttackSpeed attribute // PlayMontageAndWait(Profile.Montage, rate = AttackSpeed) // predicted // WaitGameplayEvent(Event.Montage.Hit) // fired by ANS_HitWindow on the montage, at WindupSeconds // on event, SERVER ONLY (HasAuthority): // sweep a box (ArcHalfWidth x ArcHalfHeight x Reach) from the eye along the body's aim on the Enemy channel // for each hit, once per swing: Linecast eye -> hit point on the world channel; a wall blocks it // for each surviving hit: MakeOutgoingSpec(GE_Damage), SetByCaller(Data.Damage, Profile.Damage), // add DamageType and Damage.Source.Melee to the spec, ApplyToTarget // execute cue GameplayCue.Melee.Impact at the hit point on every peer // EndAbility on montage end. // // Why a single box after the wind-up rather than a per-frame sweep along the blade: it is what the earlier // project shipped, it hits the whole pack, and it reads. A blade-socket sweep is a change to this one ability // if the feel pass wants it; nothing else would move. }; ``` Feel, all cosmetic and all client-side through gameplay cues: **hit stop** (a brief `CustomTimeDilation` dip on the local player and the struck enemy, never on the server), a **camera kick** through a camera shake on the owner's controller, and the **procedural hit reaction** on the struck body. Every duration passes through the motion accessibility scale, so reduced motion turns them down. The wind-up is what makes the animation and the rules agree: the player's hit check is delayed past the press by `WindupSeconds`, and the goblin's damage lands after its own wind-up, missing if the target has stepped out of reach. The tell is real; reading it and stepping back works. **Combat feel is the honest unknown of these steps.** No document settles whether a swing lands well. Step 7 is not done until one training dummy and then one goblin are satisfying to hit, and the numbers that made it so are committed. ## Enemies ```cpp UCLASS(BlueprintType) class UEnemyDefinition : public UPrimaryDataAsset { GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly) FGameplayTag EnemyTag; // Enemy.Goblin UPROPERTY(EditDefaultsOnly) FStatBlockDefaults BaseStats; // 30 health, MoveSpeed 1: the same block a player has (Stats.md) UPROPERTY(EditDefaultsOnly) TObjectPtr Tuning; // the goblin's 320 cm/s walk; the block multiplies it like anyone's UPROPERTY(EditDefaultsOnly) FWeaponProfile Attack; // 8 damage, 1.2 s, 0.35 s wind-up: the goblin's claws UPROPERTY(EditDefaultsOnly) float AggroRangeCm = 1000.f; UPROPERTY(EditDefaultsOnly) float LeashRangeCm = 2500.f; UPROPERTY(EditDefaultsOnly) float FleeSecondsOnAllyDeath = 2.5f; // reads as cowardice; funnier than fighting to the death UPROPERTY(EditDefaultsOnly) int32 ThreatCost = 1; // what it costs an encounter budget UPROPERTY(EditDefaultsOnly) bool bRequiresRole = false; // ineligible below three players, see scaling UPROPERTY(EditDefaultsOnly) TSoftClassPtr Body; UPROPERTY(EditDefaultsOnly) TSoftObjectPtr Brain; }; ``` ```cpp // Source/Core/Combat/ThreatTable.h // pure struct FThreatTable { void AddThreat(FGuid Combatant, float Amount); // damage dealt to me, healing done near me void ForceTarget(FGuid Combatant, float Seconds); // a taunt void Decay(float DeltaSeconds); TOptional GetTarget() const; // forced target while it lasts, else highest threat void Remove(FGuid Combatant); }; ``` The controller is an `AAIController` running a StateTree from the definition, with a perception component for sight and the threat table for choice. States: Idle, Chase, Attack (wind-up, hit through the same melee ability class the player uses, on the enemy's own ASC), Flee (on an ally's death, for `FleeSecondsOnAllyDeath`), Leash (back to the spawn point past `LeashRangeCm`). Threat accrues from damage dealt and from healing done, which is why a healer needs a tank. The threat table is what makes a taunt mean something. **Party-size scaling is a reserved rule, not a built one.** The encounter subsystem reads the party size from the game state and scales the enemy budget sublinearly: 1 player 1.0, 2 players 1.75, 3 players 2.4, 4 players 3.0. Linear scaling is the obvious choice and it is wrong: four coordinated players are worth far more than four times one. Enemies with `bRequiresRole` are ineligible below three players, which is what "playable alone" means concretely: a constraint on which enemies may appear, not a damage multiplier. Only the goblin exists, so the subsystem multiplies a count and filters a list; the table is written down so the first second enemy inherits it. ## Abilities and kits ```cpp UCLASS(Abstract) class UKitAbility : public UGameplayAbility { GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly, Category = "Kit") FGameplayTag InputTag; // Input.Ability.1..4, or Input.Attack, Input.Dodge UPROPERTY(EditDefaultsOnly, Category = "Kit") bool bSignature = false; // THE flag a mod may never cross UPROPERTY(EditDefaultsOnly, Category = "Kit") FText DisplayName; // the action bar's label // Cooldown and cost are the engine's: a GE_Cooldown_ with a Cooldown. granted tag, // and an optional stamina cost effect. CommitAbility applies both. The HUD reads the cooldown tag's remaining time. }; USTRUCT() struct FKitAbilitySlot { GENERATED_BODY() UPROPERTY(EditDefaultsOnly) TSubclassOf Ability; UPROPERTY(EditDefaultsOnly) FGameplayTag InputTag; // which slot; the ability's own InputTag is the default }; UCLASS(BlueprintType) class UClassKitDefinition : public UPrimaryDataAsset { GENERATED_BODY() public: UPROPERTY(EditDefaultsOnly) FGameplayTag ClassTag; // Class.Warrior UPROPERTY(EditDefaultsOnly) FText DisplayName; UPROPERTY(EditDefaultsOnly) TArray Abilities; // slot order; at most four on the bar UPROPERTY(EditDefaultsOnly) FStatBlockDefaults BaseStats; // applied through the one init effect (Stats.md) UPROPERTY(EditDefaultsOnly) TObjectPtr StartingWeapon; UPROPERTY(EditDefaultsOnly) float ReviveSeconds = 4.f; // the Cleric's is 2 // IsDataValid: at most one bSignature ability, at most four slots, distinct input tags. }; ``` `UExtendedAbilitySystemComponent` routes input by tag: on `Input.Ability.2` pressed it activates every granted ability whose asset tags contain that input tag. The player character forwards its `IA_Ability*` and `IA_Attack` presses there and decides nothing. Granting a kit is `GrantKit(const UClassKitDefinition&)` on the player state's component: `ApplyDefaults(BaseStats)`, give each ability with its input tag, equip the starting weapon. Changing class regrants. The kits, sketched. Each is a role, a signature no mod may grant elsewhere, and a silly escalation that later content pushes on. Two abilities each for the steps; slots three and four are content. | Kit | Role | Weapon | Signature | Second ability | Escalation later | | --- | --- | --- | --- | --- | --- | | Warrior | Hold attention, absorb, open a path | Sword, or shield and mace | **Taunt**: force nearby enemies onto you | Shoulder charge: damage to enemies in a cone, impulse to teammates | Charge launches teammates across rooms | | Ranger | Sustained ranged damage, mobility | Bow (step 11); dagger until then | **Mark**: a marked target takes more from everyone | Volley | Arrows that carry things | | Mage | Burst and area, crowd control | Staff | **Blink**: short teleport along the aim | Fireball (step 11) | Blink takes whoever is touching you | | Cleric | Sustain, revives, buffs | Mace | **Mass heal** | Mend: targeted heal; **overheal launches the target** | The heal launches people over walls | | Rogue | Burst, mobility | Dagger | **Backstab**: aimed, high damage, short cooldown | Shadowstep: a self blink along the aim | | Effects an ability may apply, each through the engine's own mechanism and never by writing a value: **Damage** (`GE_Damage` through the funnel), **Heal** (`GE_Heal` into `IncomingHeal`), **Impulse** (`LaunchCharacter` on the target through a client RPC to its owner, since movement is owner-predicted, scaled by the target's `KnockbackResist`), **Taunt** (`Status.Debuff.Taunted` through `ApplyStatus`, which the enemy controller turns into `ForceTarget`; a boss's `Immune.Status.Taunted` blocks it like any status), **Mark** (`Status.Debuff.Marked` for a duration, read by the funnel), **Slow**, **Haste** and every other buff or debuff (`ApplyStatus`, see [Stats.md](Stats.md)), **Blink** and **Dodge** (root-motion sources through the movement component, with `State.Invulnerable` for the dodge's i-frames). **Parity target:** every kit completes the same gym fight solo in roughly the same time. Not equal damage, equal viability. `enemy_killed.killer_class` against `time_to_kill_s` is the measurement. **The recoverability contract**, salvaged whole: any verb that lets one player inconvenience another (a charge into a carrier, a heal off a ledge, a blink with a passenger) must have a recovery available to the victim that takes **less time than the verb took to perform**, and none may down, kill or permanently deprive. Damage is gated in the funnel; things are never destroyed. Every such verb emits `grief_action`, because a rule nobody can measure is a rule nobody can defend. ## Projectiles (step 11) `AProjectileActor` with a `UProjectileMovementComponent`, spawned on the server by `GA_Volley` and `GA_Fireball` and replicated; the firing client spawns a local cosmetic copy on activation so the shot leaves the hand without a round trip (predict the animation, not the ownership), and the cosmetic copy hides itself when the replicated one arrives. On hit the server applies `GE_Damage` with `Damage.Source.Projectile`. Travel time and drop are real, so aim matters. The bow's `FWeaponProfile` has reach zero and a montage that fires `Event.Montage.Release` instead of `Hit`. ## Networking | State | Authority | Mechanism | | --- | --- | --- | | Attributes | Server | Replicated attribute set on the ASC (Mixed mode for players, Minimal for enemies) | | Ability activation | Client requests, server decides | GAS prediction keys; LocalPredicted for the player's abilities, ServerOnly for GA_Downed | | Damage | Server | Executions run on the server only | | Hit detection | Server, using the owner's replicated aim | A little client trust in the aim, which is fine in cooperative play | | Cooldowns | Server, client predicts | Cooldown effects with prediction; the bar shows the prediction and takes the correction | | Enemy brains | Server | StateTree ticks on the server; bodies replicate movement and montages | | Impulses on players | Server decides, owner applies | Client RPC to the owner's body, because movement is owner-predicted | | Statuses on bodies | Server, own-ability ones predicted | `ApplyStatus`, see [Stats.md](Stats.md) | | Cues | Everywhere | Gameplay cues, unreliable multicast; presentation only | ## Telemetry | Event | When | Payload | | --- | --- | --- | | `enemy_spawned` | The encounter spawns | `enemy`, `spawn_point`, `party_size` | | `enemy_killed` | Health reaches zero | `enemy`, `killer_class`, `weapon`, `ability`, `time_to_kill_s` | | `ability_used` | Activation commits | `ability`, `class`, `target_count` | | `player_damaged` | Damage lands on a player | `source`, `attacker`, `amount`, `health_after` | | `player_downed` | Health reaches zero | `source`, `party_alive`, `party_size` | | `player_revived` | Revive completes | `by_class`, `down_duration_s` | | `player_died` | Bleed-out or wipe | `down_duration_s`, `cause` | | `party_wiped` | Nobody standing | `enemies_alive`, `duration_s` | | `grief_action` | A recoverable verb on a teammate | `verb`, `actor`, `target` | ## Tests - Automation: `DamageMath` for every branch above; `FThreatTable` for forced target expiry, decay and removal; `UClassKitDefinition::IsDataValid` for two signatures and five slots. - Functional: `FT_Combat_Funnel` applies `GE_Damage` from a player to a player and asserts health is unchanged and the cue fired; `FT_Combat_Down` drives a player to zero, asserts `State.Downed`, revives through the interaction path and asserts health; `FT_Combat_Swing` swings at a dummy behind a wall and asserts no damage. ## Open questions - **Q1. Does a shove into a pit count as friendly fire?** The gate discards player-to-player damage, but a shove that causes a fall produces fall damage from the world. Recoverable if the victim is downed rather than killed and can be revived. Decide when the first pit exists; the funnel already distinguishes the cases. - **Q2. Resource model.** Cooldowns only for these steps. Stamina is an attribute in the set so that adding a cost is a cost effect, not a refactor. - **Q3. Hit registration latency.** Server-authoritative with the owner's aim is the plan. At what latency does a swing feel wrong? Test at 100 ms and 150 ms in step 7 and write the number down. - **Q4. A blade sweep instead of a box.** If the box reads as imprecise once real animations arrive, sweep the weapon actor's tip and base sockets per frame during the hit window. Contained to `GA_MeleeAttack`. - **Q5. Which kits exist at all.** Five are named because two source projects between them named these five and their signatures survive contact with the guardrail. Steps 9 and 10 build two; the rest are content. - **Q6. Cross-class weapon animation.** A Warrior carrying a bow uses the bow's montage, which is authored per weapon family, not per class. That is the cheap answer and it is the plan; revisit when animation gets real.