extends Node class_name GrabController ## The hands half of the record ritual. Lives under Player and is driven BY Player's ## input (Player already owns the mouse), so there is no _input() here to fight over ## event ordering — Player calls try_grab()/feed_drag()/primary()/drop() directly. ## ## Flow: ## E ............. grab the Record under the crosshair -> it lifts to the hand ## drag mouse X .. slide the disc up and out of the sleeve (feed_drag) ## LMB ........... throw the freed disc (then a second LMB trashes the empty sleeve) ## G ............. drop / trash whatever's in hand ## ## Godot 4.7 GDScript 2.0. const GRAB_REACH := 2.0 ## metres in front of the camera a record is grabbable const GRAB_RADIUS := 0.55 ## forgiving fat sphere, same idea as MeleeAttack const DRAG_FULL_PX := 600.0 ## pixels of horizontal drag == one full disc pull const THROW_FORCE := 6.5 ## impulse handed to a chucked disc var _cam: Camera3D var _hold_point: Node3D var _held: Record = null # extract meter UI var _meter_root: Control var _meter_fill: ColorRect func setup(cam: Camera3D) -> void: _cam = cam _hold_point = Node3D.new() _hold_point.name = "HoldPoint" _cam.add_child(_hold_point) _hold_point.position = Vector3(0.0, -0.1, -0.5) # a touch below + in front of the eye _build_meter() # ---------------------------------------------------------------- grab ## Grab the nearest Record under the crosshair, if any within reach. Fat-sphere query ## against the physics space (mirrors MeleeAttack.strike) filtered to Record bodies. func try_grab() -> void: if _held != null: return # already holding — E is grab-only if _cam == null: return var space := _cam.get_world_3d().direct_space_state if space == null: return var origin := _cam.global_position var dir := -_cam.global_transform.basis.z var sphere := SphereShape3D.new() sphere.radius = GRAB_RADIUS var params := PhysicsShapeQueryParameters3D.new() params.shape = sphere params.transform = Transform3D(Basis.IDENTITY, origin + dir * (GRAB_REACH * 0.5)) params.collide_with_bodies = true params.collide_with_areas = false var hits := space.intersect_shape(params, 12) var best: Record = null var best_d := INF for h in hits: var col = h.get("collider") if col is Record and (col as Record).is_extractable(): var d: float = (col.global_position - origin).length_squared() if d < best_d: best_d = d best = col if best != null: best.grab(_hold_point) _held = best # ---------------------------------------------------------------- extract ## Feed horizontal mouse motion into the held record's disc pull. func feed_drag(dx: float) -> void: if _held == null or not _held.is_extractable(): return _held.set_extract(_held.extract_amount() + dx / DRAG_FULL_PX) # ---------------------------------------------------------------- primary (LMB) ## Two-stage: throw the freed disc, then a later click trashes the empty sleeve. ## Returns true if the click was consumed (so Player doesn't ALSO punch). func primary() -> bool: if _held == null: return false if _held.has_disc_in_hand(): var dir := -_cam.global_transform.basis.z if _cam != null else Vector3.FORWARD _held.throw_disc(dir, THROW_FORCE) # disc leaves; sleeve stays in hand else: _held.drop() # trash the (empty or unopened) sleeve _held = null return true # ---------------------------------------------------------------- drop (G) func drop() -> void: if _held == null: return _held.drop() _held = null # ---------------------------------------------------------------- queries func is_holding() -> bool: return _held != null ## While pulling a disc, Player routes horizontal mouse to feed_drag() instead of yaw. func is_extracting() -> bool: return _held != null and _held.is_extractable() func held_record() -> Record: return _held # ---------------------------------------------------------------- extract meter func _build_meter() -> void: var layer := CanvasLayer.new() layer.name = "GrabHUD" add_child(layer) _meter_root = Control.new() _meter_root.set_anchors_preset(Control.PRESET_CENTER) _meter_root.visible = false layer.add_child(_meter_root) var bg := ColorRect.new() bg.color = Color(0, 0, 0, 0.5) bg.position = Vector2(-90, 40) bg.size = Vector2(180, 14) _meter_root.add_child(bg) _meter_fill = ColorRect.new() _meter_fill.color = Color(1.0, 0.36, 0.62, 0.95) _meter_fill.position = Vector2(-88, 42) _meter_fill.size = Vector2(0, 10) _meter_root.add_child(_meter_fill) var lbl := Label.new() lbl.text = "PULL ←→" lbl.position = Vector2(-90, 18) lbl.add_theme_font_size_override("font_size", 13) _meter_root.add_child(lbl) func _process(_dt: float) -> void: if _meter_root == null: return var pulling := is_extracting() _meter_root.visible = pulling if pulling and _meter_fill != null: _meter_fill.size.x = 176.0 * _held.extract_amount()