From 06339fbed3ea3d33e4a5cfe662ad8d9ba5d9130e Mon Sep 17 00:00:00 2001 From: Zeke Abshire Date: Fri, 3 Apr 2026 23:21:32 -0500 Subject: [PATCH] feat: init project + basic ship controller --- .editorconfig | 4 + .gitattributes | 2 + .gitignore | 3 + icon.svg | 1 + icon.svg.import | 43 +++++++ main.tscn | 82 +++++++++++++ project.godot | 57 +++++++++ projectile.gd | 35 ++++++ projectile.gd.uid | 1 + projectile.tscn | 16 +++ ship_camera.gd | 46 ++++++++ ship_camera.gd.uid | 1 + ship_controller.gd | 262 +++++++++++++++++++++++++++++++++++++++++ ship_controller.gd.uid | 1 + 14 files changed, 554 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitattributes create mode 100644 .gitignore create mode 100644 icon.svg create mode 100644 icon.svg.import create mode 100644 main.tscn create mode 100644 project.godot create mode 100644 projectile.gd create mode 100644 projectile.gd.uid create mode 100644 projectile.tscn create mode 100644 ship_camera.gd create mode 100644 ship_camera.gd.uid create mode 100644 ship_controller.gd create mode 100644 ship_controller.gd.uid diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..f28239b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +root = true + +[*] +charset = utf-8 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/icon.svg b/icon.svg new file mode 100644 index 0000000..c6bbb7d --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..4a884a6 --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,43 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://c6rk0cfu71wnd" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/uastc_level=0 +compress/rdo_quality_loss=0.0 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/channel_remap/red=0 +process/channel_remap/green=1 +process/channel_remap/blue=2 +process/channel_remap/alpha=3 +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 +svg/scale=1.0 +editor/scale_with_editor_scale=false +editor/convert_colors_with_editor_theme=false diff --git a/main.tscn b/main.tscn new file mode 100644 index 0000000..276970d --- /dev/null +++ b/main.tscn @@ -0,0 +1,82 @@ +[gd_scene format=3 uid="uid://biwfc6l2lnu11"] + +[ext_resource type="Script" uid="uid://b83vmbx10mlqe" path="res://ship_controller.gd" id="1_0xm2m"] +[ext_resource type="PackedScene" uid="uid://iogkdi8dv0nv" path="res://projectile.tscn" id="2_1bvp3"] +[ext_resource type="Script" uid="uid://inqwtwfui2tr" path="res://ship_camera.gd" id="2_h2yge"] + +[sub_resource type="BoxMesh" id="BoxMesh_1bvp3"] + +[sub_resource type="BoxShape3D" id="BoxShape3D_lquwl"] + +[sub_resource type="ProceduralSkyMaterial" id="ProceduralSkyMaterial_h2yge"] + +[sub_resource type="Sky" id="Sky_1bvp3"] +sky_material = SubResource("ProceduralSkyMaterial_h2yge") + +[sub_resource type="Environment" id="Environment_0xm2m"] +background_mode = 2 +sky = SubResource("Sky_1bvp3") +ambient_light_source = 3 + +[node name="Main" type="Node3D" unique_id=1472247091] + +[node name="Ship" type="CharacterBody3D" parent="." unique_id=122234868 node_paths=PackedStringArray("ship_model", "muzzle")] +script = ExtResource("1_0xm2m") +turn_speed = 4.975 +projectile_scene = ExtResource("2_1bvp3") +ship_model = NodePath("ShipModel") +muzzle = NodePath("Muzzle") + +[node name="ShipModel" type="MeshInstance3D" parent="Ship" unique_id=352448396] +mesh = SubResource("BoxMesh_1bvp3") + +[node name="HoverRay" type="RayCast3D" parent="Ship" unique_id=1076533010] +target_position = Vector3(0, -5, 0) + +[node name="Muzzle" type="Marker3D" parent="Ship" unique_id=322799309] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.5) + +[node name="CollisionShape3D" type="CollisionShape3D" parent="Ship" unique_id=567390614] +shape = SubResource("BoxShape3D_lquwl") + +[node name="CameraRig" type="Node3D" parent="." unique_id=920354244 node_paths=PackedStringArray("target")] +script = ExtResource("2_h2yge") +target = NodePath("../Ship") +follow_distance = 8.57 +position_smoothing = 18.56 +rotation_smoothing = 14.5 + +[node name="Camera3D" type="Camera3D" parent="CameraRig" unique_id=2079353001] + +[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=1690012445] + +[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=2019756943] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -12, 0.07247639, -21.964855) + +[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D" unique_id=842749163] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -24, 0.07247639, 17.169182) + +[node name="CSGBox3D3" type="CSGBox3D" parent="CSGCombiner3D" unique_id=2106821369] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -36, 6.410921, -17.84053) + +[node name="CSGBox3D4" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1236194522] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -48, 6.410921, 14.210682) + +[node name="CSGBox3D5" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1570750803] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -36, -5.170101, -25.111805) + +[node name="CSGBox3D6" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1205494666] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -51.54701, 1.1025181, 0) + +[node name="CSGBox3D7" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1295905165] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -60, 12.749367, -34.99634) + +[node name="CSGBox3D8" type="CSGBox3D" parent="CSGCombiner3D" unique_id=749127710] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -72, 12.749367, 0) + +[node name="CSGBox3D9" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1499064177] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -16.63602, 0) +size = Vector3(500, 1, 500) + +[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1014540785] +environment = SubResource("Environment_0xm2m") diff --git a/project.godot b/project.godot new file mode 100644 index 0000000..cbf3a18 --- /dev/null +++ b/project.godot @@ -0,0 +1,57 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="Mercury" +run/main_scene="uid://biwfc6l2lnu11" +config/features=PackedStringArray("4.6", "Forward Plus") +config/icon="res://icon.svg" + +[input] + +accelerate={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":87,"key_label":0,"unicode":119,"location":0,"echo":false,"script":null) +] +} +brake={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":83,"key_label":0,"unicode":115,"location":0,"echo":false,"script":null) +] +} +strafe_left={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null) +] +} +strafe_right={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null) +] +} +boost={ +"deadzone": 0.2, +"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194325,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null) +] +} +shoot={ +"deadzone": 0.2, +"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":0,"position":Vector2(0, 0),"global_position":Vector2(0, 0),"factor":1.0,"button_index":1,"canceled":false,"pressed":false,"double_click":false,"script":null) +] +} + +[physics] + +3d/physics_engine="Jolt Physics" + +[rendering] + +rendering_device/driver.windows="d3d12" diff --git a/projectile.gd b/projectile.gd new file mode 100644 index 0000000..84d3963 --- /dev/null +++ b/projectile.gd @@ -0,0 +1,35 @@ +extends Area3D +class_name HovercraftProjectile +## Simple bullet that travels forward and destroys itself on hit or timeout. + +@export var speed: float = 80.0 +@export var lifetime: float = 3.0 +@export var damage: float = 10.0 + +var _direction: Vector3 = Vector3.FORWARD +var _age: float = 0.0 + + +func _ready() -> void: + # Use the node's forward direction if no explicit direction was set + if _direction == Vector3.FORWARD: + _direction = - global_transform.basis.z + + body_entered.connect(_on_body_entered) + + +func set_direction(dir: Vector3) -> void: + _direction = dir.normalized() + + +func _physics_process(delta: float) -> void: + global_position += _direction * speed * delta + _age += delta + if _age >= lifetime: + queue_free() + + +func _on_body_entered(body: Node3D) -> void: + if body.has_method("take_damage"): + body.take_damage(damage) + queue_free() \ No newline at end of file diff --git a/projectile.gd.uid b/projectile.gd.uid new file mode 100644 index 0000000..5099587 --- /dev/null +++ b/projectile.gd.uid @@ -0,0 +1 @@ +uid://cpqdkciafcu51 diff --git a/projectile.tscn b/projectile.tscn new file mode 100644 index 0000000..b22f633 --- /dev/null +++ b/projectile.tscn @@ -0,0 +1,16 @@ +[gd_scene format=3 uid="uid://iogkdi8dv0nv"] + +[ext_resource type="Script" uid="uid://cpqdkciafcu51" path="res://projectile.gd" id="1_a85iw"] + +[sub_resource type="SphereMesh" id="SphereMesh_akgne"] + +[sub_resource type="SphereShape3D" id="SphereShape3D_a85iw"] + +[node name="Projectile" type="Area3D" unique_id=24893414] +script = ExtResource("1_a85iw") + +[node name="MeshInstance3D" type="MeshInstance3D" parent="." unique_id=1868961436] +mesh = SubResource("SphereMesh_akgne") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1475750571] +shape = SubResource("SphereShape3D_a85iw") diff --git a/ship_camera.gd b/ship_camera.gd new file mode 100644 index 0000000..fe79f94 --- /dev/null +++ b/ship_camera.gd @@ -0,0 +1,46 @@ +extends Node3D +class_name ShipCamera +## Third-person camera for a freely flying ship. +## Stays behind and above the ship regardless of pitch/yaw. +## Attach as a sibling of the ship (child of the scene root), NOT as a child +## of the ship, so it can lag smoothly. + +@export var target: ShipController +@export var follow_distance: float = 12.0 +@export var follow_height: float = 3.0 ## offset along the ship's local UP +@export var look_ahead: float = 4.0 +@export var position_smoothing: float = 5.0 +@export var rotation_smoothing: float = 6.0 + +@onready var camera: Camera3D = $Camera3D + + +func _ready() -> void: + if not camera: + camera = Camera3D.new() + add_child(camera) + camera.current = true + + +func _physics_process(delta: float) -> void: + if not target: + return + + # "Behind" the ship in its local space (accounts for pitch + yaw) + var ship_back := target.global_transform.basis.z # +Z is behind in Godot + var ship_up := target.global_transform.basis.y + + var desired_pos := ( + target.global_position + + ship_back * follow_distance + + ship_up * follow_height + ) + + global_position = global_position.lerp(desired_pos, position_smoothing * delta) + + # Look at a point ahead of the ship + var ship_forward := -target.global_transform.basis.z + var look_target := target.global_position + ship_forward * look_ahead + + var desired_transform := global_transform.looking_at(look_target, Vector3.UP) + global_transform = global_transform.interpolate_with(desired_transform, rotation_smoothing * delta) \ No newline at end of file diff --git a/ship_camera.gd.uid b/ship_camera.gd.uid new file mode 100644 index 0000000..6cd0043 --- /dev/null +++ b/ship_camera.gd.uid @@ -0,0 +1 @@ +uid://inqwtwfui2tr diff --git a/ship_controller.gd b/ship_controller.gd new file mode 100644 index 0000000..6005e92 --- /dev/null +++ b/ship_controller.gd @@ -0,0 +1,262 @@ +extends CharacterBody3D +class_name ShipController +## FP4-style ship controller for Godot 4.6 +## +## Full 3D flight — the ship flies wherever the mouse aims. +## Mouse X = yaw, Mouse Y = pitch. W accelerates along the aim direction, +## S brakes, A/D strafe laterally. No gravity, no hover, no ground. +## The visual model applies cosmetic roll/pitch on top of the actual orientation. + +# ── Movement tuning ────────────────────────────────────────────────────────── +@export_group("Movement") +@export var max_speed: float = 30.0 +@export var acceleration: float = 40.0 +@export var brake_deceleration: float = 50.0 +@export var strafe_speed: float = 20.0 +@export var friction: float = 12.0 ## passive drag when no input + +@export_group("Boost") +@export var boost_multiplier: float = 1.8 +@export var boost_max: float = 100.0 +@export var boost_drain_rate: float = 30.0 +@export var boost_recharge_rate: float = 15.0 +@export var boost_min_to_activate: float = 10.0 + +# ── Cosmetic tilt ──────────────────────────────────────────────────────────── +@export_group("Visual Tilt") +@export var max_roll_deg: float = 25.0 ## extra bank on strafe / turning +@export var max_pitch_deg: float = 15.0 ## extra nose-down on accel +@export var tilt_lerp_speed: float = 6.0 + +# ── Mouse ──────────────────────────────────────────────────────────────────── +@export_group("Mouse") +@export var mouse_sensitivity: float = 0.003 +@export var pitch_limit_deg: float = 85.0 ## clamp so you can't fully invert + +# ── Combat ─────────────────────────────────────────────────────────────────── +@export_group("Combat") +@export var fire_rate: float = 10.0 +@export var projectile_scene: PackedScene + +# ── Node references ────────────────────────────────────────────────────────── +@export_group("Node References") +@export var ship_model: Node3D ## visual mesh that gets cosmetic tilt +@export var muzzle: Marker3D ## bullet spawn point + +# ── Internal state ─────────────────────────────────────────────────────────── +var _forward_speed: float = 0.0 +var _strafe_speed: float = 0.0 + +# Mouse-driven aim targets (body smoothly rotates toward these) +var _target_yaw: float = 0.0 +var _target_pitch: float = 0.0 +var _current_yaw: float = 0.0 +var _current_pitch: float = 0.0 + +var _boost_energy: float = 0.0 +var _is_boosting: bool = false +var _fire_cooldown: float = 0.0 + +# Cosmetic tilt state (applied to ship_model only) +var _visual_roll: float = 0.0 +var _visual_extra_pitch: float = 0.0 + + +func _ready() -> void: + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + _boost_energy = boost_max + _current_yaw = rotation.y + _current_pitch = rotation.x + _target_yaw = _current_yaw + _target_pitch = _current_pitch + + +func _unhandled_input(event: InputEvent) -> void: + if event is InputEventMouseMotion: + _target_yaw -= event.relative.x * mouse_sensitivity + _target_pitch -= event.relative.y * mouse_sensitivity + # Clamp pitch so the ship can't go fully vertical / inverted + var limit := deg_to_rad(pitch_limit_deg) + _target_pitch = clampf(_target_pitch, -limit, limit) + + if event.is_action_pressed("ui_cancel"): + if Input.mouse_mode == Input.MOUSE_MODE_CAPTURED: + Input.mouse_mode = Input.MOUSE_MODE_VISIBLE + else: + Input.mouse_mode = Input.MOUSE_MODE_CAPTURED + + +func _physics_process(delta: float) -> void: + _handle_turning(delta) + _handle_boost(delta) + _handle_movement(delta) + _apply_velocity(delta) + _handle_visual_tilt(delta) + _handle_shooting(delta) + + +# ── Turning (yaw + pitch) ─────────────────────────────────────────────────── +## Instead of lerp_angle (which breaks on large deltas and can snap backward +## near ±π), we use move_toward on the angular difference. This guarantees: +## 1. The ship always turns toward the target, never away from it. +## 2. The turn rate is capped at max_turn_rate rad/s so fast mouse flicks +## produce a fast but controlled rotation, not a teleport or reversal. + +@export var max_turn_rate: float = 12.0 ## radians/sec — max rotation speed + +func _handle_turning(delta: float) -> void: + # Yaw — target is unbounded (no wraparound bugs), difference is always correct + var yaw_diff := _target_yaw - _current_yaw + var yaw_step := minf(absf(yaw_diff), max_turn_rate * delta) + _current_yaw += signf(yaw_diff) * yaw_step + + # Pitch — same approach + var pitch_diff := _target_pitch - _current_pitch + var pitch_step := minf(absf(pitch_diff), max_turn_rate * delta) + _current_pitch += signf(pitch_diff) * pitch_step + + rotation.y = _current_yaw + rotation.x = _current_pitch + + +# ── Movement along the ship's facing direction ────────────────────────────── + +func _handle_movement(delta: float) -> void: + var forward_input: float = 0.0 + var strafe_input: float = 0.0 + + if Input.is_action_pressed("accelerate"): + forward_input = 1.0 + if Input.is_action_pressed("brake"): + forward_input -= 1.0 + if Input.is_action_pressed("strafe_left"): + strafe_input -= 1.0 + if Input.is_action_pressed("strafe_right"): + strafe_input += 1.0 + + var effective_max := max_speed * _boost_value() + var effective_accel := acceleration * _boost_value() + + # Forward / brake + if forward_input > 0.0: + _forward_speed = move_toward(_forward_speed, effective_max, effective_accel * delta) + elif forward_input < 0.0: + _forward_speed = move_toward(_forward_speed, 0.0, brake_deceleration * delta) + else: + _forward_speed = move_toward(_forward_speed, 0.0, friction * delta) + + # Strafe + if strafe_input != 0.0: + _strafe_speed = move_toward(_strafe_speed, strafe_speed * strafe_input, acceleration * delta) + else: + _strafe_speed = move_toward(_strafe_speed, 0.0, friction * delta) + + +# ── Boost ──────────────────────────────────────────────────────────────────── + +func _handle_boost(delta: float) -> void: + if Input.is_action_pressed("boost") and _boost_energy > boost_min_to_activate: + _is_boosting = true + _boost_energy = maxf(0.0, _boost_energy - boost_drain_rate * delta) + if _boost_energy <= 0.0: + _is_boosting = false + else: + _is_boosting = false + _boost_energy = minf(boost_max, _boost_energy + boost_recharge_rate * delta) + + +func _boost_value() -> float: + return boost_multiplier if _is_boosting else 1.0 + + +# ── Apply velocity in 3D ──────────────────────────────────────────────────── + +func _apply_velocity(delta: float) -> void: + # Forward is wherever the ship is pointing (includes pitch + yaw) + var forward_dir := -transform.basis.z + var right_dir := transform.basis.x + + var desired := forward_dir * _forward_speed + right_dir * _strafe_speed + + # Clamp total speed + var max_total := max_speed * _boost_value() + if desired.length() > max_total: + desired = desired.normalized() * max_total + + velocity = desired + move_and_slide() + + +# ── Cosmetic tilt on the visual model ──────────────────────────────────────── +## The CharacterBody3D itself rotates to face the aim direction (yaw + pitch). +## On top of that, the child ship_model gets extra roll from strafing/turning +## and extra pitch from acceleration — purely visual seasoning. + +func _handle_visual_tilt(delta: float) -> void: + if not ship_model: + return + + # ── Roll from turning + strafing ── + # yaw is unbounded now, so the raw difference is the true angular lag + var yaw_delta: float = _target_yaw - _current_yaw + + var strafe_input: float = 0.0 + if Input.is_action_pressed("strafe_left"): + strafe_input -= 1.0 + if Input.is_action_pressed("strafe_right"): + strafe_input += 1.0 + + var target_roll_deg: float = (-strafe_input * max_roll_deg * 0.6) + (yaw_delta * max_roll_deg * 2.0) + target_roll_deg = clampf(target_roll_deg, -max_roll_deg, max_roll_deg) + + # ── Extra pitch from acceleration ── + var forward_input: float = 0.0 + if Input.is_action_pressed("accelerate"): + forward_input = 1.0 + if Input.is_action_pressed("brake"): + forward_input -= 1.0 + + var target_extra_pitch_deg: float = - forward_input * max_pitch_deg + if _is_boosting: + target_extra_pitch_deg *= 1.4 + + # Smooth interpolation + _visual_roll = lerp(_visual_roll, deg_to_rad(target_roll_deg), tilt_lerp_speed * delta) + _visual_extra_pitch = lerp(_visual_extra_pitch, deg_to_rad(target_extra_pitch_deg), tilt_lerp_speed * delta) + + # Apply roll and extra pitch on top of the body's real orientation + ship_model.rotation = Vector3(_visual_extra_pitch, 0.0, _visual_roll) + + +# ── Shooting ───────────────────────────────────────────────────────────────── + +func _handle_shooting(delta: float) -> void: + _fire_cooldown = maxf(0.0, _fire_cooldown - delta) + + if Input.is_action_pressed("shoot") and _fire_cooldown <= 0.0 and projectile_scene: + _fire_cooldown = 1.0 / fire_rate + var bullet := projectile_scene.instantiate() as Node3D + get_tree().current_scene.add_child(bullet) + + if muzzle: + bullet.global_position = muzzle.global_position + bullet.global_transform.basis = muzzle.global_transform.basis + else: + bullet.global_position = global_position + (-transform.basis.z * 2.0) + bullet.global_transform.basis = global_transform.basis + + # Pass the full 3D forward direction to the bullet + if bullet.has_method("set_direction"): + bullet.set_direction(-global_transform.basis.z) + + +# ── Public API ─────────────────────────────────────────────────────────────── + +func get_boost_ratio() -> float: + return _boost_energy / boost_max + +func get_speed_ratio() -> float: + return _forward_speed / (max_speed * boost_multiplier) + +func is_boosting() -> bool: + return _is_boosting \ No newline at end of file diff --git a/ship_controller.gd.uid b/ship_controller.gd.uid new file mode 100644 index 0000000..9422db5 --- /dev/null +++ b/ship_controller.gd.uid @@ -0,0 +1 @@ +uid://b83vmbx10mlqe