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