class_name DamageEvent extends RefCounted ## The one typed payload for every hit in the game โ€” a bull goring a matador, a bear slam popping ## the bull, a thrown spear landing. It replaces the scattered, stringly Object.call(&"take_sword_hit", ## "gored") paths: an emitter (a Hitbox) fills one of these and hands it to the target's ## receive_damage(event). One shape to read, log and type-check, instead of four call conventions. ## ## It serves both victim models the game already has: the bull, which dies on one clean hit ## (lethal + cause + hit_position), and an enemy, which is launched/ragdolled by an impulse ## (knockback_dir ยท knockback_strength + knock_up). A given hit just leaves the fields it doesn't ## use at their defaults. enum Cause { GENERIC, GORED, MAULED, THROWN, SLAMMED, KICKED, DASHED } ## Who dealt the hit โ€” for attribution and for ignoring self-overlap. May be null. var source: Node = null ## What kind of hit; drives the victim's reaction (blood/vignette/FX) and any future resistances. var cause: Cause = Cause.GENERIC ## World contact point, so impact FX spawn where the hit actually landed. var hit_position: Vector3 = Vector3.ZERO ## Planar push direction (need not be normalised). Vector3.ZERO = no horizontal knockback. var knockback_dir: Vector3 = Vector3.ZERO ## Impulse magnitude behind knockback_dir โ€” the enemy launch/ragdoll strength. var knockback_strength: float = 0.0 ## Upward launch in m/s (an overhead smash pop, a leap-slam outer ring). 0 = a grounded hit. var knock_up: float = 0.0 ## One clean hit ends the run for the bull; most enemy-dealt hits are lethal to it. Enemies that ## are merely launched (not killed) by the bull's abilities set this false. var lethal: bool = true static func make(kind: Cause, at: Vector3, src: Node = null) -> DamageEvent: var e := DamageEvent.new() e.cause = kind e.hit_position = at e.source = src return e ## The single entry point every attacker uses to wound a target: build the typed event and hand it ## to receive_damage(). Falls back to the legacy string call for any target not yet migrated, so a ## caller never has to branch on which contract the victim speaks. `knock_up_v` is the already-shaped ## pop (0 = grounded); callers that scale it by distance compute it before calling. static func deal(target: Node, kind: Cause, at: Vector3, src: Node = null, knock_up_v: float = 0.0) -> void: if target == null: return if target.has_method(&"receive_damage"): var event := make(kind, at, src) event.knock_up = knock_up_v target.call(&"receive_damage", event) return if target.has_method(&"take_sword_hit"): target.call(&"take_sword_hit", make(kind, at, src).cause_name(), at) if knock_up_v > 0.0 and target.has_method(&"apply_knock_up"): target.call(&"apply_knock_up", knock_up_v) ## Lower-case name of the cause, for logs and for the bull's existing cause-keyed reactions ## ("gored" / "mauled" / "thrown") during the migration off string causes. func cause_name() -> String: return String(Cause.keys()[cause]).to_lower() ## Maps a legacy cause string onto the enum, so a Hitbox's exported damage_cause round-trips ## exactly (gored/mauled/thrown are the only ones in use). Unknown strings fall back to GENERIC. static func cause_from(text: String) -> Cause: match text.to_lower(): "gored": return Cause.GORED "mauled": return Cause.MAULED "thrown": return Cause.THROWN "slammed": return Cause.SLAMMED "kicked": return Cause.KICKED "dashed": return Cause.DASHED return Cause.GENERIC