Combat overhaul: charge/gore, bear ram, matador taunt reactions
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>
This commit is contained in:
+63
-39
@@ -9,8 +9,11 @@ extends Node
|
||||
## bodies get picked up within one interval.
|
||||
##
|
||||
## Toggle from the console:
|
||||
## show_collisions · show_hitboxes · show_bones · show_raycasts (true / false)
|
||||
## show_hitboxes also surfaces the bull's and bear's body capsules, not just Area3Ds
|
||||
## show_hitboxes · show_attacks · show_bones · show_raycasts (true / false)
|
||||
## show_hitboxes — the combatants' hurt volumes: each HitArea + the player/enemy body
|
||||
## capsules, drawn as calm translucent-blue volumes (what abilities/blades hit)
|
||||
## show_attacks — the attack hitboxes (bear claw/smash/leap, matador blade, thrown
|
||||
## spear), filled translucent red and shown ONLY while the swing is actually live
|
||||
## show_states — floating AI-state tags + velocity vectors over each matador
|
||||
## show_animation_name — floating current-clip tag over every AnimationPlayer host
|
||||
## show_stats — 2D corner readout: fps / frame ms / draw calls / matador tally
|
||||
@@ -21,13 +24,14 @@ extends Node
|
||||
const REBUILD_INTERVAL := 0.5
|
||||
const STATS_INTERVAL := 0.2 # stats text is re-composed 5×/sec, not every frame
|
||||
|
||||
const COLOR_BODY := Color(0.30, 1.0, 0.45) # collision wireframes (green)
|
||||
const COLOR_AREA := Color(0.0, 0.9, 1.0) # hit-area volumes (cyan, translucent)
|
||||
const COLOR_AREA := Color(0.0, 0.9, 1.0) # combatant hurt volumes (cyan, translucent)
|
||||
const COLOR_ATTACK := Color(1.0, 0.30, 0.15) # attack hitboxes (red, translucent, overlaid)
|
||||
const COLOR_BONE := Color(1.0, 0.35, 0.9) # skeleton bones (magenta)
|
||||
const COLOR_VEL := Color(1.0, 0.85, 0.2) # matador velocity vectors (amber)
|
||||
const COLOR_ANIM := Color(0.55, 0.85, 1.0) # animation-name tags (sky blue)
|
||||
const COLOR_GRID := Color(0.45, 0.45, 0.45, 0.5)
|
||||
const AREA_ALPHA := 0.25
|
||||
const AREA_ALPHA := 0.25
|
||||
const ATTACK_ALPHA := 0.32
|
||||
|
||||
const GRID_HALF := 20 # grid extends ±GRID_HALF cells from the origin
|
||||
const GRID_STEP := 1.0 # metres between grid lines
|
||||
@@ -81,6 +85,7 @@ func ray(from: Vector3, to: Vector3, color: Color = Color.YELLOW) -> void:
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_reconcile(delta)
|
||||
_pulse_attacks()
|
||||
if DP.b("show_bones"):
|
||||
_draw_bones()
|
||||
if DP.b("show_states"):
|
||||
@@ -360,11 +365,11 @@ func _si(n: int) -> String:
|
||||
# ── Collision / hit-area overlays (incremental) ────────────────────────────────
|
||||
|
||||
func _reconcile(delta: float) -> void:
|
||||
var col := DP.b("show_collisions")
|
||||
var hit := DP.b("show_hitboxes")
|
||||
var atk := DP.b("show_attacks")
|
||||
var bone := DP.b("show_bones")
|
||||
var anim := DP.b("show_animation_name")
|
||||
if not (col or hit or bone or anim):
|
||||
if not (hit or atk or bone or anim):
|
||||
if not _overlays.is_empty():
|
||||
_clear_overlays()
|
||||
_skeletons.clear()
|
||||
@@ -372,7 +377,7 @@ func _reconcile(delta: float) -> void:
|
||||
_prev_flags = ""
|
||||
return
|
||||
|
||||
var flags := "%d%d%d%d" % [int(col), int(hit), int(bone), int(anim)]
|
||||
var flags := "%d%d%d%d" % [int(hit), int(atk), int(bone), int(anim)]
|
||||
_reconcile_t -= delta
|
||||
if flags != _prev_flags: # a toggle just changed — refresh immediately
|
||||
_prev_flags = flags
|
||||
@@ -386,7 +391,7 @@ func _reconcile(delta: float) -> void:
|
||||
var wanted: Dictionary = {}
|
||||
var scene := get_tree().current_scene
|
||||
if scene != null:
|
||||
_collect(scene, col, hit, bone, anim, wanted)
|
||||
_collect(scene, hit, atk, bone, anim, wanted)
|
||||
|
||||
# Drop overlays whose shape is gone or no longer wanted. The overlay is a child of
|
||||
# the collision shape, so it's freed with it — hold it as Variant, since assigning a
|
||||
@@ -402,10 +407,11 @@ func _reconcile(delta: float) -> void:
|
||||
for id: int in wanted:
|
||||
if not _overlays.has(id):
|
||||
var entry: Array = wanted[id]
|
||||
_overlays[id] = _attach(entry[0] as CollisionShape3D, entry[1] as bool)
|
||||
_overlays[id] = _attach(entry[0] as CollisionShape3D, entry[1] as String)
|
||||
|
||||
|
||||
func _collect(node: Node, col: bool, hit: bool, bone: bool, anim: bool, wanted: Dictionary) -> void:
|
||||
func _collect(node: Node, hit: bool, atk: bool, bone: bool, anim: bool,
|
||||
wanted: Dictionary) -> void:
|
||||
# The audience ("public") is ambient decoration, not a debug target — skip the whole
|
||||
# crowd subtree so its clapping AnimationPlayers/skeletons don't flood the overlays.
|
||||
if node.is_in_group(&"public"):
|
||||
@@ -417,18 +423,28 @@ func _collect(node: Node, col: bool, hit: bool, bone: bool, anim: bool, wanted:
|
||||
if node is CollisionShape3D:
|
||||
var cs := node as CollisionShape3D
|
||||
if cs.shape != null and not cs.disabled and _drawable(cs.shape):
|
||||
var parent := cs.get_parent()
|
||||
var is_area := parent is Area3D
|
||||
# Ragdoll bone colliders (PhysicalBone3D) are excluded — use show_bones for
|
||||
# the skeleton; otherwise every corpse floods the view with capsules.
|
||||
var is_body := parent is PhysicsBody3D and not (parent is PhysicalBone3D)
|
||||
# The bull's and bear's body capsules are combat volumes too, so they also
|
||||
# surface under show_hitboxes (not just show_collisions).
|
||||
var is_combatant := is_body and (parent.is_in_group(&"player") or parent.is_in_group(&"bear"))
|
||||
if (is_area and hit) or (is_body and col) or (is_combatant and hit):
|
||||
wanted[cs.get_instance_id()] = [cs, is_area]
|
||||
var kind := _classify(cs.get_parent())
|
||||
var show := (kind == "attack" and atk) or (kind == "hitbox" and hit)
|
||||
if show:
|
||||
wanted[cs.get_instance_id()] = [cs, kind]
|
||||
for child in node.get_children():
|
||||
_collect(child, col, hit, bone, anim, wanted)
|
||||
_collect(child, hit, atk, bone, anim, wanted)
|
||||
|
||||
|
||||
# Sort a shape's owning body/area into an overlay bucket, or "" to skip.
|
||||
# attack — a swing volume (a Hitbox area, or a thrown-spear projectile body)
|
||||
# hitbox — a combatant's hurt volume: its HitArea, or its own body capsule
|
||||
func _classify(parent: Node) -> String:
|
||||
if parent is Hitbox or parent is SpearProjectile:
|
||||
return "attack"
|
||||
if parent is Area3D:
|
||||
return "hitbox"
|
||||
# Ragdoll bone colliders (PhysicalBone3D) are excluded — use show_bones for the
|
||||
# skeleton; otherwise every corpse floods the view with capsules.
|
||||
if parent is PhysicsBody3D and not (parent is PhysicalBone3D):
|
||||
if parent.is_in_group(&"player") or parent.is_in_group(&"matador"):
|
||||
return "hitbox"
|
||||
return ""
|
||||
|
||||
|
||||
# Skip only the unbounded shapes whose debug mesh is meaningless/huge; concave arena
|
||||
@@ -437,20 +453,35 @@ func _drawable(shape: Shape3D) -> bool:
|
||||
return not (shape is WorldBoundaryShape3D or shape is HeightMapShape3D)
|
||||
|
||||
|
||||
func _attach(cs: CollisionShape3D, is_area: bool) -> MeshInstance3D:
|
||||
func _attach(cs: CollisionShape3D, kind: String) -> MeshInstance3D:
|
||||
var mi := MeshInstance3D.new()
|
||||
if is_area:
|
||||
var solid := _solid_mesh(cs.shape)
|
||||
mi.mesh = solid if solid != null else cs.shape.get_debug_mesh()
|
||||
mi.material_override = _translucent_material(COLOR_AREA)
|
||||
var solid := _solid_mesh(cs.shape)
|
||||
mi.mesh = solid if solid != null else cs.shape.get_debug_mesh()
|
||||
if kind == "attack":
|
||||
# Overlaid so a brief swing volume reads clearly on top of the action; its
|
||||
# visibility is pulsed per-frame in _pulse_attacks so it only shows while live.
|
||||
mi.material_override = _translucent_material(COLOR_ATTACK, ATTACK_ALPHA, true)
|
||||
else:
|
||||
mi.mesh = cs.shape.get_debug_mesh()
|
||||
mi.material_override = _wire_material(COLOR_BODY)
|
||||
# The calm translucent-blue volume — depth-tested so it sits in the world.
|
||||
mi.material_override = _translucent_material(COLOR_AREA, AREA_ALPHA, false)
|
||||
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
cs.add_child(mi)
|
||||
return mi
|
||||
|
||||
|
||||
# Attack overlays exist for as long as show_attacks is on, but a swing hitbox is only a
|
||||
# hazard while its Hitbox is monitoring — so show each attack volume only when it's live.
|
||||
# (A thrown spear has no monitoring gate; it stays visible for its whole flight.)
|
||||
func _pulse_attacks() -> void:
|
||||
for id: int in _overlays:
|
||||
var mi: Variant = _overlays[id]
|
||||
if not is_instance_valid(mi):
|
||||
continue
|
||||
var owner := (mi as Node).get_parent().get_parent()
|
||||
if owner is Hitbox:
|
||||
(mi as MeshInstance3D).visible = (owner as Area3D).monitoring
|
||||
|
||||
|
||||
func _clear_overlays() -> void:
|
||||
for id: int in _overlays:
|
||||
var mi: Variant = _overlays[id]
|
||||
@@ -495,19 +526,12 @@ func _line_material() -> StandardMaterial3D:
|
||||
return mat
|
||||
|
||||
|
||||
func _wire_material(color: Color) -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.albedo_color = color
|
||||
mat.no_depth_test = true
|
||||
return mat
|
||||
|
||||
|
||||
func _translucent_material(color: Color) -> StandardMaterial3D:
|
||||
func _translucent_material(color: Color, alpha: float, overlay: bool) -> StandardMaterial3D:
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.albedo_color = Color(color.r, color.g, color.b, AREA_ALPHA)
|
||||
mat.albedo_color = Color(color.r, color.g, color.b, alpha)
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
mat.depth_draw_mode = BaseMaterial3D.DEPTH_DRAW_DISABLED
|
||||
mat.no_depth_test = overlay
|
||||
return mat
|
||||
|
||||
Reference in New Issue
Block a user