extends CharacterBody3D class_name Player ## First-person player for Destroyulator. Built entirely in code so Main.gd can ## spawn it where the old fixed Camera3D was and reuse `player.camera` for the ## crosshair raycast. Mouse-captured look, WASD move, Space jump; L-click PUNCH, ## R-click KICK; Esc releases the mouse (click recaptures). ## ## Melee is a swappable MeleeAttack resource (reach/radius/impulse/knockback/cooldown) ## so real weapons slot in by swapping the resource + viewmodel — movement never ## changes. Hits are fired from screen-center (the mouse is captured, so its position ## is meaningless) and resolved in _physics_process, where direct_space_state is valid. ## ## Godot 4.7 GDScript 2.0. No 3.x APIs: move_and_slide() takes no args and reads/writes ## `velocity`; get_gravity() supplies the project default; mouse_mode is a property. @export var move_speed: float = 5.0 @export var accel: float = 12.0 @export var air_accel: float = 3.0 @export var jump_velocity: float = 4.5 @export var mouse_sensitivity: float = 0.0025 # radians per pixel of mouse motion @export var pitch_limit_deg: float = 89.0 @export var eye_height: float = 1.6 var camera: Camera3D # Main reads this for the crosshair raycast var _head: Node3D var _hands_rig: Node3D # cosmetic viewmodel (hands + leg), swung on attack var _r_hand: MeshInstance3D var _l_hand: MeshInstance3D var _leg: MeshInstance3D var _swing_tween: Tween var _pitch := 0.0 var _cd_left := 0.0 var _queued: MeleeAttack = null # buffered click, consumed next physics step var _punch: MeleeAttack var _kick: MeleeAttack # resting local transforms of the viewmodel parts (relative to the camera) const R_HAND_REST := Vector3(0.30, -0.32, -0.55) const L_HAND_REST := Vector3(-0.30, -0.34, -0.58) const LEG_REST := Vector3(0.06, -1.05, -0.42) func _ready() -> void: # --- collision capsule (feet at the body origin) --- var col := CollisionShape3D.new() var cap := CapsuleShape3D.new() cap.radius = 0.35 cap.height = 1.8 col.shape = cap col.position = Vector3(0, 0.9, 0) # capsule center at height/2 so feet sit at origin add_child(col) # --- head (pitch) + camera --- _head = Node3D.new() _head.name = "Head" _head.position = Vector3(0, eye_height, 0) add_child(_head) camera = Camera3D.new() camera.name = "Camera3D" _head.add_child(camera) camera.current = true _build_viewmodel() _build_crosshair() _make_attacks() Input.mouse_mode = Input.MOUSE_MODE_CAPTURED # ---------------------------------------------------------------- attacks func _make_attacks() -> void: _punch = MeleeAttack.new() _punch.reach = 1.7 _punch.radius = 0.34 _punch.impulse = 6.0 _punch.knockback = 2.5 _punch.cooldown = 0.26 _kick = MeleeAttack.new() _kick.reach = 2.2 _kick.radius = 0.42 _kick.impulse = 10.0 _kick.knockback = 5.0 _kick.cooldown = 0.48 # ---------------------------------------------------------------- viewmodel func _build_viewmodel() -> void: _hands_rig = Node3D.new() _hands_rig.name = "ViewModel" camera.add_child(_hands_rig) var mat := StandardMaterial3D.new() # semi-opaque: BOTH the alpha mode AND alpha<1 are required (alpha alone renders opaque) mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA mat.albedo_color = Color(0.86, 0.62, 0.5, 0.55) mat.no_depth_test = true # hands draw over world geo, never clip mat.render_priority = 1 mat.cull_mode = BaseMaterial3D.CULL_DISABLED mat.roughness = 0.85 _r_hand = _make_capsule(mat) _l_hand = _make_capsule(mat) _leg = _make_leg(mat) _hands_rig.add_child(_r_hand) _hands_rig.add_child(_l_hand) _hands_rig.add_child(_leg) _r_hand.position = R_HAND_REST _l_hand.position = L_HAND_REST _leg.position = LEG_REST _r_hand.rotation = Vector3(deg_to_rad(-70), 0.0, deg_to_rad(-12)) _l_hand.rotation = Vector3(deg_to_rad(-70), 0.0, deg_to_rad(12)) _leg.rotation = Vector3(deg_to_rad(-90), 0.0, 0.0) func _make_capsule(mat: StandardMaterial3D) -> MeshInstance3D: var mi := MeshInstance3D.new() var cap := CapsuleMesh.new() cap.radius = 0.055 cap.height = 0.20 # full height incl. the hemispherical caps mi.mesh = cap mi.material_override = mat mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF return mi func _make_leg(mat: StandardMaterial3D) -> MeshInstance3D: var shin := MeshInstance3D.new() var cap := CapsuleMesh.new() cap.radius = 0.075 cap.height = 0.34 shin.mesh = cap shin.material_override = mat shin.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF var foot := MeshInstance3D.new() var box := BoxMesh.new() box.size = Vector3(0.11, 0.07, 0.22) foot.mesh = box foot.material_override = mat foot.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF foot.position = Vector3(0.0, -0.20, 0.09) shin.add_child(foot) return shin func _build_crosshair() -> void: var layer := CanvasLayer.new() layer.name = "CrosshairLayer" add_child(layer) var dot := ColorRect.new() dot.color = Color(1, 1, 1, 0.85) dot.anchor_left = 0.5 dot.anchor_top = 0.5 dot.anchor_right = 0.5 dot.anchor_bottom = 0.5 dot.offset_left = -2 dot.offset_top = -2 dot.offset_right = 2 dot.offset_bottom = 2 layer.add_child(dot) # ---------------------------------------------------------------- input func _unhandled_input(event: InputEvent) -> void: if event is InputEventMouseMotion and Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: rotate_y(-event.relative.x * mouse_sensitivity) # yaw on the body only _pitch = clamp( _pitch - event.relative.y * mouse_sensitivity, deg_to_rad(-pitch_limit_deg), deg_to_rad(pitch_limit_deg)) _head.rotation.x = _pitch # pitch on the head only return if event is InputEventMouseButton and event.pressed: if Input.mouse_mode != Input.MOUSE_MODE_CAPTURED: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED # click recaptures; this one isn't an attack return if event.button_index == MOUSE_BUTTON_LEFT: _queue_attack(_punch) elif event.button_index == MOUSE_BUTTON_RIGHT: _queue_attack(_kick) return if event is InputEventKey and event.pressed and not event.echo: if event.keycode == KEY_ESCAPE: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE func _queue_attack(atk: MeleeAttack) -> void: # buffer the click; the hit resolves in _physics_process where the space state is valid. if _cd_left <= 0.0: _queued = atk # ---------------------------------------------------------------- physics / movement func _physics_process(delta: float) -> void: _cd_left = max(0.0, _cd_left - delta) # resolve a buffered melee swing if _queued != null and _cd_left <= 0.0: var atk := _queued _queued = null _cd_left = atk.cooldown var origin := camera.global_position var dir := -camera.global_transform.basis.z # camera forward = crosshair atk.strike(self, origin, dir, [get_rid()]) _swing_viewmodel(atk == _kick) # gravity (only off the floor, or it accumulates and you rocket downward) if not is_on_floor(): velocity += get_gravity() * delta if Input.is_key_pressed(KEY_SPACE) and is_on_floor(): velocity.y = jump_velocity var input_dir := Vector2( float(Input.is_key_pressed(KEY_D)) - float(Input.is_key_pressed(KEY_A)), float(Input.is_key_pressed(KEY_S)) - float(Input.is_key_pressed(KEY_W))) var wish := transform.basis * Vector3(input_dir.x, 0.0, input_dir.y) wish.y = 0.0 if wish.length() > 1.0: wish = wish.normalized() var target := wish * move_speed var a := accel if is_on_floor() else air_accel velocity.x = move_toward(velocity.x, target.x, a * move_speed * delta) velocity.z = move_toward(velocity.z, target.z, a * move_speed * delta) move_and_slide() # ---------------------------------------------------------------- cosmetic swing func _swing_viewmodel(is_kick: bool) -> void: if _swing_tween and _swing_tween.is_valid(): _swing_tween.kill() # interrupt any lingering swing so rapid clicks don't fight if is_kick: _leg.position = LEG_REST var up := LEG_REST + Vector3(0.0, 0.66, -0.12) _swing_tween = create_tween() _swing_tween.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT) _swing_tween.tween_property(_leg, "position", up, 0.10) _swing_tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) _swing_tween.tween_property(_leg, "position", LEG_REST, 0.20) else: _r_hand.position = R_HAND_REST var fwd := R_HAND_REST + Vector3(-0.06, 0.10, -0.34) _swing_tween = create_tween() _swing_tween.set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_OUT) _swing_tween.tween_property(_r_hand, "position", fwd, 0.08) _swing_tween.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) _swing_tween.tween_property(_r_hand, "position", R_HAND_REST, 0.16)