5c1e881a53
Bull charge damage: - Tunnel-proof swept detection (prev→now segment) for matador gore and bear ram, so a 66 m/s dash no longer skips clean over the thin HitArea. - Horns-first cone (bull_gore_arc): running into an enemy only wounds it when charging roughly at it, not a sideways brush. - Dash is a committed lunge: bypasses the horns cone AND the survival roll, so a dash connect is a guaranteed kill. Cruise charges keep a speed-scaled roll (mat_charge_pierce). Roll ability defers to its own pop (is_rolling guard). Bear: - Horns-first swept ram detection (edge-triggered: one charge = one wound); a tunnelling or point-blank charge now lands where body_entered wouldn't. - Overhead smash lane extends faster (0.1s) and further (20 m). - Leap slam launches the bull from anywhere in the circle (ring_frac 0). - Removed stray-quote syntax errors that broke the script. Matador: - Collider-driven stab (BladeHitbox) with recovery beat + taunt-after-hit so it stops machine-gunning the bull. - A taunting matador answers a charge: spear from range, sword up close. Spear projectile masks the BULL layer so throws connect. Tests: matador_charge/stab/spear/taunt_react, bear_ram/hitbox/fx (+ unified _fake_bull stand-in). Trimmed verbose comments. Note: bear claw-emitter/death work is still WIP (3 bear_boss_test failures). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
102 lines
3.4 KiB
GDScript
102 lines
3.4 KiB
GDScript
@tool
|
|
extends Area3D
|
|
class_name Hitbox
|
|
## A designer-placeable attack hitbox: drop this into a scene, give it a CollisionShape3D
|
|
## child, and drag the shape to size/position the attack's reach in the editor. The shape
|
|
## itself decides what got hit — no distance/facing math in code.
|
|
##
|
|
## Inactive by default; the owning script calls activate() for the swing's active window and
|
|
## deactivate() when it ends. Each activation only hits a given body once (the bull's own
|
|
## i-frames also collapse a flurry, but this keeps a single swing from re-triggering).
|
|
|
|
const PhysicsLayers = preload("res://physics_layers.gd")
|
|
|
|
## How the hit is tagged on the bull's take_sword_hit ("mauled" / "gored" / …).
|
|
@export var damage_cause: String = "mauled"
|
|
## Upward launch (m/s) applied to the bull on hit — e.g. an overhead smash that pops it up.
|
|
@export var knock_up: float = 0.0
|
|
## Knock-up distance shaping, measured planar (XZ) from this hitbox's own origin to the body:
|
|
## below `knock_up_min_dist` no pop is applied (leap slam: only the outer ring launches). If
|
|
## `knock_up_max_dist` > min, the pop ramps 0→`knock_up` from min to max (overhead smash: the
|
|
## further out the hit lands, the higher the bull flies); at/again 0 it's full past the min.
|
|
@export var knock_up_min_dist: float = 0.0
|
|
@export var knock_up_max_dist: float = 0.0
|
|
|
|
var _active: bool = false
|
|
var _hit_this_swing: Dictionary = {}
|
|
# Whether the most recent active window connected. Cleared on activate(), NOT deactivate(), so the
|
|
# owner can read it after the window closes (e.g. a landed stab breaks off to taunt).
|
|
var _did_hit: bool = false
|
|
|
|
|
|
func _ready() -> void:
|
|
# Detect the bull's body (layer BULL) only; never collide or push physics.
|
|
collision_layer = 0
|
|
collision_mask = PhysicsLayers.BULL
|
|
monitorable = false
|
|
monitoring = false
|
|
if Engine.is_editor_hint():
|
|
return
|
|
body_entered.connect(_on_body_entered)
|
|
|
|
|
|
func activate() -> void:
|
|
if _active:
|
|
return
|
|
_active = true
|
|
_hit_this_swing.clear()
|
|
_did_hit = false
|
|
monitoring = true
|
|
|
|
|
|
func deactivate() -> void:
|
|
_active = false
|
|
monitoring = false
|
|
|
|
|
|
# Did the window that just closed connect? Valid until the next activate() clears it.
|
|
func did_hit() -> bool:
|
|
return _did_hit
|
|
|
|
|
|
func _on_body_entered(body: Node3D) -> void:
|
|
_apply_hit(body)
|
|
|
|
|
|
# For AoE slams the bull is usually already standing inside the shape when it activates, so
|
|
# body_entered never fires. The owner calls sweep() each active frame to catch current overlaps
|
|
# (still deduped per activation, so the bull is only hit once per slam).
|
|
func sweep() -> void:
|
|
if not _active:
|
|
return
|
|
for body in get_overlapping_bodies():
|
|
_apply_hit(body)
|
|
|
|
|
|
func _apply_hit(body: Node3D) -> void:
|
|
if not _active or not body.is_in_group(&"player"):
|
|
return
|
|
if _hit_this_swing.has(body):
|
|
return
|
|
_hit_this_swing[body] = true
|
|
_did_hit = true
|
|
body.call(&"take_sword_hit", damage_cause)
|
|
if knock_up > 0.0:
|
|
var v := _knock_up_for(body)
|
|
if v > 0.0:
|
|
body.call(&"apply_knock_up", v)
|
|
|
|
|
|
# Scale the pop by how far the body is from this hitbox's origin (planar). See the export docs.
|
|
func _knock_up_for(body: Node3D) -> float:
|
|
if knock_up_min_dist <= 0.0 and knock_up_max_dist <= 0.0:
|
|
return knock_up
|
|
var to := body.global_position - global_position
|
|
var d := Vector2(to.x, to.z).length()
|
|
if d < knock_up_min_dist:
|
|
return 0.0
|
|
if knock_up_max_dist > knock_up_min_dist:
|
|
var t := clampf((d - knock_up_min_dist) / (knock_up_max_dist - knock_up_min_dist), 0.0, 1.0)
|
|
return knock_up * t
|
|
return knock_up
|