destroyulator/game/scripts/MeleeAttack.gd
Monster Robot Party 5852322606 Restructure into monorepo: game/ is the Godot lane
Repo root moves up from game/ to the project folder. Lanes live as top-level
dirs (game/ now, web/ planned); LANES/ will hold parallel-session specs.
3D-STORE asset vault stays out of git. History preserved as renames.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-14 15:33:13 +10:00

70 lines
2.8 KiB
GDScript

extends Resource
class_name MeleeAttack
## Config + logic for one melee swing (punch, kick, or a real weapon later).
## Swap the resource + viewmodel and you have a new weapon; movement never changes.
##
## Hit detection is a forgiving SPHERE query against the physics space, fired from
## the camera center (crosshair) so you don't have to pixel-aim. intersect_shape is
## NOT distance-sorted, so we pick the collider nearest the camera and act on it.
##
## Godot 4.7 GDScript 2.0. Verified: PhysicsShapeQueryParameters3D, SphereShape3D,
## PhysicsDirectSpaceState3D.intersect_shape, apply_impulse(impulse, rel_pos).
@export var reach: float = 1.8 ## distance in front of the camera the hit sphere sits
@export var radius: float = 0.38 ## fat sphere so melee is forgiving, not pixel-precise
@export var impulse: float = 6.0 ## smash strength handed to Smashable.smash()
@export var knockback: float = 3.0 ## central/off-center impulse for plain RigidBody3D props
@export var cooldown: float = 0.28 ## seconds between swings with this attack
## Run the hit test and apply effects.
## caster : the player (supplies the physics world + a self-exclude RID)
## origin : camera world position
## dir : camera forward (normalized), i.e. -camera.global_transform.basis.z
## Returns the node hit (Smashable or RigidBody3D) or null. Safe to call only from
## _physics_process — that's where direct_space_state is guaranteed valid.
func strike(caster: Node3D, origin: Vector3, dir: Vector3, exclude: Array = []) -> Node:
var space := caster.get_world_3d().direct_space_state
if space == null:
return null
var sphere := SphereShape3D.new()
sphere.radius = radius
var params := PhysicsShapeQueryParameters3D.new()
params.shape = sphere
params.transform = Transform3D(Basis.IDENTITY, origin + dir * reach)
params.collide_with_bodies = true
params.collide_with_areas = false
params.exclude = exclude # [caster.get_rid()] so the fat sphere never self-hits
var hits := space.intersect_shape(params, 8)
if hits.is_empty():
return null
# intersect_shape is unsorted — pick the collider nearest the camera so a swing
# hits the first thing in front, not something behind it.
var best: Node = null
var best_d := INF
for h in hits:
var col = h.get("collider")
if col == null or not (col is Node3D):
continue
var d: float = (col.global_position - origin).length_squared()
if d < best_d:
best_d = d
best = col
if best == null:
return null
# Smashable extends RigidBody3D, so this check MUST come first or we'd only
# knock it around and never shatter it.
if best is Smashable:
best.smash(dir * impulse)
elif best is RigidBody3D:
# off-center impulse (relative to center of mass) so props tumble, not glide
var hit_pos: Vector3 = origin + dir * reach
best.apply_impulse(dir * knockback, hit_pos - best.global_position)
return best