Merge branch 'tech-test/3d-controller'
This commit is contained in:
@ -179,4 +179,68 @@ Performance Culling & Caching: For performance-intensive scenarios like asteroid
|
||||
#### 2. Component "API" & Wiring System
|
||||
Component Contracts: To facilitate the upcoming visual wiring system, we will formalize the "API" for ControlPanel and Databank resources. This will be done by creating new scripts that extend the base classes and override the get_input_sockets() and get_output_signals() functions to explicitly define what signals and functions each component provides.
|
||||
|
||||
Static vs. Resource-Based API: We've concluded that using extended Resource scripts to define these APIs is superior to using static functions on the node scripts. This decouples the data contract from the implementation and allows a single scene to be used with multiple different data configurations, which is critical for a flexible wiring system.
|
||||
Static vs. Resource-Based API: We've concluded that using extended Resource scripts to define these APIs is superior to using static functions on the node scripts. This decouples the data contract from the implementation and allows a single scene to be used with multiple different data configurations, which is critical for a flexible wiring system.
|
||||
|
||||
## Project Development Status Update: 31/10/25
|
||||
|
||||
### 3D Character Controller & Movement Tech Demo (Cycle 3)
|
||||
|
||||
Work has proceeded on a tech demo for the 3D character controller, establishing a robust, physics-based system for zero-G movement. The architecture has been refactored to prioritize a clean separation of concerns, with a central "pawn" acting as a physics integrator and modular "controllers" acting as the "brains" for different movement types.
|
||||
|
||||
### ✅ Implemented Features
|
||||
|
||||
#### Pawn/Controller Architecture: The character is split into several key classes:
|
||||
|
||||
CharacterPawn3D: The core CharacterBody3D. It acts as a "dumb" physics integrator, holding velocity and angular_velocity, integrating rotation, and calling move_and_slide(). It no longer contains movement-specific state logic.
|
||||
|
||||
PlayerController3D: Gathers all hardware input (keyboard, mouse) and packages it into KeyInput dictionaries (pressed, held, released) to send to the pawn via RPC.
|
||||
|
||||
EVAMovementComponent: Refactored into a "dumb tool". It exposes functions like apply_thrusters() and apply_orientation() which are called by other controllers.
|
||||
|
||||
ZeroGMovementComponent: This is now the "brain" for all zero-G movement. It receives all inputs from the pawn and contains its own internal state machine (IDLE, REACHING, GRIPPING, CLIMBING, CHARGING_LAUNCH).
|
||||
|
||||
#### Contextual Movement Logic:
|
||||
|
||||
The ZeroGMovementComponent decides when to use the EVA suit. In its IDLE state, it checks for fresh movement input (movement_input_was_neutral) before calling the EVAMovementComponent's apply_thrusters function.
|
||||
|
||||
This successfully implements "coast on release," where releasing a grip (_release_current_grip) flags the movement input as "stale," preventing the EVA suit from engaging even if the key is still held.
|
||||
|
||||
#### EVA/Jetpack Controls:
|
||||
|
||||
The EVAMovementComponent provides force-based linear movement (WASD, Shift/Ctrl) and torque-based angular roll (Q/E).
|
||||
|
||||
A body-orientation function (_orient_pawn) allows the pawn to auto-align with the camera's forward direction.
|
||||
|
||||
#### Physics-Based Grip System:
|
||||
|
||||
GripArea3D: A composition-based Area3D node provides the interface for all grabbable objects. It requires its parent to implement functions like get_grip_transform and get_push_off_normal.
|
||||
|
||||
Grip Detection: The CharacterPawn3D uses a GripDetector Area3D to find GripArea3D nodes in range and passes this nearby_grips list to the ZeroGMovementComponent.
|
||||
|
||||
GRIPPING State: This state is now fully physics-based. Instead of setting the pawn's global_transform, the _apply_grip_physics function uses a PD controller to apply linear forces (to move to the offset position) and angular torques (to align with the grip's orientation).
|
||||
|
||||
Grip Orientation: The gripping logic correctly calculates the closest of two opposing orientations (e.g., "up" or "down" on a bar) by comparing the pawn's current up vector to the grip's potential up vectors.
|
||||
|
||||
Grip Rolling: While in the GRIPPING state, the player can use Q/E to override the auto-orientation and apply roll torque around the grip's axis.
|
||||
|
||||
#### Physics-Based Climbing:
|
||||
|
||||
CLIMBING State: This state applies lerp'd velocity to move the pawn, allowing it to interact with physics.
|
||||
|
||||
Climb Targeting: The _find_best_grip function successfully identifies the next valid grip within a configurable climb_angle_threshold_deg cone.
|
||||
|
||||
Handover: Logic in _process_climbing correctly identifies when the pawn is close enough to the next_grip_target to _perform_grip_handover.
|
||||
|
||||
Climb Release: The pawn will correctly release its grip and enter the IDLE state (coasting) if it moves past the current_grip by release_past_grip_threshold without a new target being found.
|
||||
|
||||
### ❌ Not Yet Implemented / Pending Tasks
|
||||
|
||||
REACHING State: The REACHING state exists but its logic (_process_reaching) is a stub that instantly calls _try_initiate_reach. The full implementation (e.g., procedural animation/IK moving the hand to the target) is pending.
|
||||
|
||||
CHARGING_LAUNCH State: The state exists and the execution logic is present (_handle_launch_charge, _execute_launch), but the state transition logic in _update_state does not currently allow entering this state from GRIPPING (it's overshadowed by the _start_climb check).
|
||||
|
||||
Ladder (3D) & Walking (3D) States: The CharacterPawn3D has high-level states for GRIPPING_LADDER and WALKING, but the movement functions (_apply_ladder_movement, _apply_walking_movement) are stubs.
|
||||
|
||||
Generic Surface Grab: The TODO to allow the ZeroGMovementComponent to grab any physics surface (not just a GripArea3D) is not implemented.
|
||||
|
||||
EVA Stabilization: The _apply_stabilization_torques function in EVAMovementComponent is still a placeholder.
|
||||
@ -1,8 +1,8 @@
|
||||
# Game Design Document: Project Stardust Drifter (Working Title)
|
||||
# Game Design Document: Project Millimeters of Aluminum (Working Title)
|
||||
|
||||
## 1. Game Vision & Concept
|
||||
|
||||
Project Stardust Drifter is a top-down 2D spaceship simulation game that emphasizes realistic orbital mechanics, deep ship management, and cooperative crew gameplay. Players take on roles as members of a multi-species crew aboard a modular, physically simulated spaceship.
|
||||
Project Millimeters of Aluminum is a top-down 2D spaceship simulation game that emphasizes realistic orbital mechanics, deep ship management, and cooperative crew gameplay. Players take on roles as members of a multi-species crew aboard a modular, physically simulated spaceship.
|
||||
|
||||
The game's aesthetic is inspired by the technical, gritty, and high-contrast 2D style of games like Barotrauma, focusing on diegetic interfaces and detailed, functional components. The core experience is about planning and executing complex maneuvers in a hazardous, procedurally generated star system, where understanding the ship's systems is as important as piloting skill.
|
||||
|
||||
@ -23,8 +23,8 @@ The game world is a procedurally generated star system created by the StarSystem
|
||||
|
||||
### 2. N-Body Physics Simulation
|
||||
Major bodies in orbit (CelestialBody class) are goveerened by a simplified n-body gravity simulation. Physical objects with player interaction (ships, crew characters, detached components, and eventually stations) are governed by a realistic N-body gravitational simulation, managed by the OrbitalMechanics library.
|
||||
- Objects inherit from a base OrbitalBody2D class, ensuring consistent physics.
|
||||
- This allows for complex and emergent orbital behaviors, such as tidal forces and stable elliptical orbits.
|
||||
- Objects inherit from a base OrbitalBody2D class, ensuring consistent physics.
|
||||
- This allows for complex and emergent orbital behaviors, such as tidal forces and stable elliptical orbits.
|
||||
|
||||
### 3. Modular Spaceship
|
||||
|
||||
@ -39,16 +39,16 @@ The player's ship is not a monolithic entity but a collection of distinct, physi
|
||||
### 4. Advanced Navigation Computer
|
||||
This is the primary crew interface for long-range travel.
|
||||
- Maneuver Planning: The computer can calculate various orbital transfers, each with strategic trade-offs:
|
||||
- Hohmann Transfer: The most fuel-efficient route.
|
||||
- Fast Transfer: A quicker but more fuel-intensive option.
|
||||
- Brachistochrone (Torchship) Trajectory: For ships with high-efficiency engines like Ion Drives, enabling constant-thrust travel.
|
||||
- Gravity Assist: Planned for future implementation.
|
||||
- Hohmann Transfer: The most fuel-efficient route.
|
||||
- Fast Transfer: A quicker but more fuel-intensive option.
|
||||
- Brachistochrone (Torchship) Trajectory: For ships with high-efficiency engines like Ion Drives, enabling constant-thrust travel.
|
||||
- Gravity Assist: Planned for future implementation.
|
||||
- Tactical Map: A fully interactive UI map that replaces custom drawing with instanced, clickable icons for all bodies. It features:
|
||||
- Zoom-to-cursor and click-and-drag panning.
|
||||
- Predictive orbital path drawing for all objects.
|
||||
- Icon culling at a distance to reduce clutter.
|
||||
- Custom hover effects and detailed tooltips with "sensor data."
|
||||
- A "picture-in-picture" SubViewport showing the ship's main camera view.
|
||||
- Zoom-to-cursor and click-and-drag panning.
|
||||
- Predictive orbital path drawing for all objects.
|
||||
- Icon culling at a distance to reduce clutter.
|
||||
- Custom hover effects and detailed tooltips with "sensor data."
|
||||
- A "picture-in-picture" SubViewport showing the ship's main camera view.
|
||||
|
||||
### 5. Multi-Species Crew (Player Classes)
|
||||
|
||||
@ -62,17 +62,33 @@ Character progression is based on distinct species with physical advantages and
|
||||
|
||||
- Ship AI: A non-physical class that interacts directly with ship systems at the cost of high power and heat generation.
|
||||
|
||||
### 6. Runtime Component Design & Engineering
|
||||
|
||||
To move beyond pre-defined ship parts, the game will feature an in-game system for players to design, prototype, and manufacture their own components. This is achieved through a "Component Blueprint" architecture that separates a component's data definition from its physical form.
|
||||
|
||||
- **Component Blueprints:** A `ComponentBlueprint` is a `Resource` file (`.tres`) that acts as a schematic. It contains metadata (name, description), a reference to a generic base scene (e.g., a "thruster chassis"), and a dictionary of overridden properties (e.g., `{"thrust_force": 7500, "mass": 120}`).
|
||||
|
||||
- **Generic Template Scenes:** Instead of dozens of unique component scenes, the game will use a small number of generic, unconfigured "template" scenes (e.g., `generic_thruster.tscn`, `generic_power_plant.tscn`). These scenes have scripts with exported variables that define their performance characteristics.
|
||||
|
||||
- **The Design Lab:** Players will use a dedicated `SystemStation` (the "Design Lab") to create and modify blueprints. This UI will dynamically generate controls (sliders, input fields) based on the exported variables of the selected template scene. Players can tweak parameters, balancing trade-offs like performance vs. resource cost, and save the result as a new blueprint resource in their personal data folder.
|
||||
|
||||
- **Networked Construction:** When a player builds an object in-game, they are selecting one of their saved blueprints.
|
||||
1. The client sends an RPC to the server with the path to the chosen `ComponentBlueprint` resource.
|
||||
2. The server validates the request and loads the blueprint. (This requires a system for syncing player-created blueprints to the server upon connection).
|
||||
3. A global `ComponentFactory` singleton on the server takes the blueprint, instantiates the correct generic template scene, and applies the blueprint's property overrides to the new instance.
|
||||
4. This fully-configured node is then passed to the `MultiplayerSpawner`, which replicates the object across the network, ensuring all clients see the correctly customized component.
|
||||
|
||||
## 4. Technical Overview
|
||||
|
||||
- Architecture: The project uses a decoupled, modular architecture heavily reliant on a global SignalBus for inter-scene communication and a GameManager for global state. Ships feature their own local ShipSignalBus for internal component communication.
|
||||
- Key Scripts:
|
||||
- OrbitalBody2D.gd: The base class for all physical objects.
|
||||
- Spaceship.gd: The central hub for a player ship.
|
||||
- Thruster.gd: A self-contained, physically simulated thruster component.
|
||||
- ThrusterController.gd: Contains advanced autopilot and manual control logic (PD controller, bang-coast-bang maneuvers).
|
||||
- NavigationComputer.gd: Manages the UI and high-level maneuver planning.
|
||||
- MapDrawer.gd: A Control node that manages the interactive map UI.
|
||||
- MapIcon.gd: The reusable UI component for map objects.
|
||||
- OrbitalBody2D.gd: The base class for all physical objects.
|
||||
- Spaceship.gd: The central hub for a player ship.
|
||||
- Thruster.gd: A self-contained, physically simulated thruster component.
|
||||
- ThrusterController.gd: Contains advanced autopilot and manual control logic (PD controller, bang-coast-bang maneuvers).
|
||||
- NavigationComputer.gd: Manages the UI and high-level maneuver planning.
|
||||
- MapDrawer.gd: A Control node that manages the interactive map UI.
|
||||
- MapIcon.gd: The reusable UI component for map objects.
|
||||
|
||||
- Art Style: Aims for a Barotraumainspired aesthetic using 2D ragdolls (Skeleton2D, PinJoint2D), detailed sprites with normal maps, and high-contrast dynamic lighting (PointLight2D, LightOccluder2D).
|
||||
|
||||
|
||||
8
Init_Prompt.md
Normal file
8
Init_Prompt.md
Normal file
@ -0,0 +1,8 @@
|
||||
You are a Godot 4.5 Code assistant. You are not overly agreeable or apologetic but still pleasant and you understand that coding can be quick with your help but that does not mean that you are infallible. Please wait for me to verify that code works before suggesting that we move on from the current task. Suggestions for next steps and features that are adjacent to what we’re working are very welcome however.
|
||||
|
||||
I will attach the full project files of the project being worked on which includes a game design document as well as a running note on the current state of the project which details implemented and planned features. Read these and report back to me. Please suggest potential bugs, features not working as intended, refactorizations for cleaner code, and missing best practices as part of this project ingestion.
|
||||
|
||||
Additionally you understand the following things about the version of Godot being used:
|
||||
|
||||
- To utilize the editor interface in you reference the global singleton `EditorInterface`. You do not need to call a function to get the a reference to it.
|
||||
- `xform()` is not a function on transform objects. To achieve the same effect you would use simple transform multiplication (`Transform_A * Transform_B)`)
|
||||
6
eva_suit_controller.tscn
Normal file
6
eva_suit_controller.tscn
Normal file
@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bm1rbv4tuppbc"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d4jka2etva22s" path="res://scenes/tests/3d/eva_movement_component.gd" id="1_mb22m"]
|
||||
|
||||
[node name="EVASuitController" type="Node3D"]
|
||||
script = ExtResource("1_mb22m")
|
||||
@ -1,26 +1,24 @@
|
||||
[gd_scene load_steps=22 format=3 uid="uid://didt2nsdtbmra"]
|
||||
[gd_scene load_steps=20 format=3 uid="uid://didt2nsdtbmra"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_nqe0s"]
|
||||
[ext_resource type="PackedScene" uid="uid://bho8x10x4oab7" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_foqop"]
|
||||
[ext_resource type="PackedScene" uid="uid://d3hitk62fice4" path="res://scenes/ship/builder/pieces/bulkhead.tscn" id="4_dmrms"]
|
||||
[ext_resource type="PackedScene" uid="uid://2n42nstcj1n0" path="res://scenes/ship/components/hardware/system_station.tscn" id="5_nqe0s"]
|
||||
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="6_ft4kn"]
|
||||
[ext_resource type="Script" uid="uid://cskf26i7vnxug" path="res://scenes/ship/computer/control_panel.gd" id="6_oqcn4"]
|
||||
[ext_resource type="Resource" uid="uid://dghg3pbws42yu" path="res://scenes/ship/computer/shards/helm_logic_databank.tres" id="7_dmrms"]
|
||||
[ext_resource type="Resource" uid="uid://c4wyouanvf86c" path="res://scenes/ship/computer/panels/button_panel.tres" id="7_vmx8o"]
|
||||
[ext_resource type="Resource" uid="uid://57y6igb07e10" path="res://scenes/ship/computer/panels/readout_screen.tres" id="8_83bu1"]
|
||||
[ext_resource type="Resource" uid="uid://dl7g67mtqkfx2" path="res://scenes/ship/computer/panels/sensor_panel.tres" id="9_xwy4s"]
|
||||
[ext_resource type="Script" uid="uid://diu2tgusi3vmt" path="res://scenes/ship/computer/shards/sensor_databank.gd" id="9_ixntg"]
|
||||
[ext_resource type="PackedScene" uid="uid://dt1t2n7dewucw" path="res://scenes/ship/computer/UI/button_panel.tscn" id="10_px2ne"]
|
||||
[ext_resource type="Resource" uid="uid://bx7wgunvy5hfa" path="res://scenes/ship/computer/shards/helm_ship_status.tres" id="11_83bu1"]
|
||||
[ext_resource type="Script" uid="uid://cfbyqvnvf3hna" path="res://scenes/ship/computer/shards/helm_logic_databank.gd" id="10_wkxbw"]
|
||||
[ext_resource type="PackedScene" uid="uid://cdbqjkgsj02or" path="res://scenes/ship/computer/UI/readout_screen_panel.tscn" id="11_erhv3"]
|
||||
[ext_resource type="Script" uid="uid://t12etsdx2h38" path="res://scenes/ship/computer/shards/nav_selection_databank.gd" id="11_xwy4s"]
|
||||
[ext_resource type="Script" uid="uid://ceqdi6jobefnc" path="res://scenes/ship/computer/shards/helm_autopilot_databank.gd" id="12_4epkn"]
|
||||
[ext_resource type="PackedScene" uid="uid://rd1c22nsru8y" path="res://scenes/ship/computer/UI/sensor_panel.tscn" id="12_q1rtr"]
|
||||
[ext_resource type="PackedScene" uid="uid://c0bb77rmyatr0" path="res://scenes/ship/components/hardware/thruster.tscn" id="12_vmx8o"]
|
||||
[ext_resource type="Resource" uid="uid://g4ho63f30vjm" path="res://scenes/ship/computer/shards/nav_selection_databank.tres" id="12_wkxbw"]
|
||||
[ext_resource type="PackedScene" uid="uid://dvpy3urgtm62n" path="res://scenes/ship/components/hardware/spawner.tscn" id="13_83bu1"]
|
||||
[ext_resource type="PackedScene" uid="uid://pq55j75t3fda" path="res://scenes/ship/computer/UI/throttle_lever_panel.tscn" id="13_rsa1x"]
|
||||
[ext_resource type="Resource" uid="uid://dwhpjwuobcqdu" path="res://scenes/ship/computer/shards/helm_autopilot_databank.tres" id="13_xfp3q"]
|
||||
[ext_resource type="Resource" uid="uid://b0suy3sxjwhtv" path="res://scenes/ship/computer/shards/sensor_databank.tres" id="13_xwy4s"]
|
||||
[ext_resource type="Resource" uid="uid://6jj1jd14cdlt" path="res://scenes/ship/computer/shards/nav_hohman_planner.tres" id="15_fll2s"]
|
||||
[ext_resource type="Script" uid="uid://ctgl5kxyagw0f" path="res://scenes/ship/computer/shards/helm_ship_status.gd" id="13_wkxbw"]
|
||||
[ext_resource type="Script" uid="uid://ghluwjd5c5ul" path="res://scenes/ship/computer/shards/nav_brachistochrone_planner.gd" id="14_xwy4s"]
|
||||
[ext_resource type="Script" uid="uid://bghu5lhcbcfmh" path="res://scenes/ship/computer/shards/nav_hohman_planner.gd" id="15_fll2s"]
|
||||
[ext_resource type="Script" uid="uid://dsbn7ushwqrko" path="res://scenes/ship/computer/shards/nav_intercept_solver.gd" id="16_vufgi"]
|
||||
[ext_resource type="Script" uid="uid://0f6v6iu3o5qo" path="res://scenes/ship/computer/shards/nav_projection_shard.gd" id="17_34v0b"]
|
||||
|
||||
[node name="Module" type="Node2D"]
|
||||
physics_interpolation_mode = 2
|
||||
@ -109,17 +107,9 @@ base_mass = 0.0
|
||||
|
||||
[node name="Station" parent="." instance=ExtResource("5_nqe0s")]
|
||||
position = Vector2(0, -10)
|
||||
panels = Array[ExtResource("6_oqcn4")]([ExtResource("8_83bu1"), ExtResource("8_83bu1"), ExtResource("9_xwy4s"), ExtResource("7_vmx8o")])
|
||||
panel_scenes = Array[PackedScene]([ExtResource("11_erhv3"), ExtResource("11_erhv3"), ExtResource("12_q1rtr"), ExtResource("10_px2ne"), ExtResource("13_rsa1x")])
|
||||
installed_databanks = Array[ExtResource("6_ft4kn")]([ExtResource("11_83bu1"), ExtResource("7_dmrms"), ExtResource("13_xfp3q"), ExtResource("12_wkxbw"), ExtResource("15_fll2s"), ExtResource("13_xwy4s")])
|
||||
grid_size_x = 1
|
||||
grid_size_y = 1
|
||||
attachment_type = 0
|
||||
databank_installations = Array[Script]([ExtResource("10_wkxbw"), ExtResource("12_4epkn"), ExtResource("13_wkxbw"), ExtResource("9_ixntg"), ExtResource("11_xwy4s"), ExtResource("14_xwy4s"), ExtResource("15_fll2s"), ExtResource("16_vufgi"), ExtResource("17_34v0b")])
|
||||
physics_mode = 2
|
||||
base_mass = 1.0
|
||||
linear_velocity = Vector2(0, 0)
|
||||
angular_velocity = 0.0
|
||||
inertia = 1.0
|
||||
|
||||
[node name="Thruster" parent="." instance=ExtResource("12_vmx8o")]
|
||||
position = Vector2(-95, -130)
|
||||
|
||||
@ -12,15 +12,23 @@ config_version=5
|
||||
|
||||
config/name="space_simulation"
|
||||
run/main_scene="uid://dogqi2c58qdc0"
|
||||
config/features=PackedStringArray("4.4", "Forward Plus")
|
||||
config/features=PackedStringArray("4.5", "Forward Plus")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
[autoload]
|
||||
|
||||
OrbitalMechanics="*res://scripts/singletons/orbital_mechanics.gd"
|
||||
SignalBus="*res://scripts/singletons/signal_bus.gd"
|
||||
GameManager="*res://scripts/singletons/game_manager.gd"
|
||||
PopupManager="*res://scripts/singletons/popup_manager.gd"
|
||||
Constants="*res://scripts/singletons/constants.gd"
|
||||
NetworkHandler="*res://scripts/network/network_handler.gd"
|
||||
|
||||
[display]
|
||||
|
||||
window/vsync/vsync_mode=0
|
||||
|
||||
[dotnet]
|
||||
|
||||
project/assembly_name="space_simulation"
|
||||
|
||||
[editor_plugins]
|
||||
|
||||
@ -83,6 +91,71 @@ interact={
|
||||
"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":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
toggle_wiring_panel={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":true,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":70,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
move_left_3d={
|
||||
"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)
|
||||
]
|
||||
}
|
||||
move_right_3d={
|
||||
"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)
|
||||
]
|
||||
}
|
||||
move_forward_3d={
|
||||
"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)
|
||||
]
|
||||
}
|
||||
move_backward_3d={
|
||||
"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)
|
||||
]
|
||||
}
|
||||
roll_right_3d={
|
||||
"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":69,"key_label":0,"unicode":101,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
roll_left_3d={
|
||||
"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":81,"key_label":0,"unicode":113,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
interact_3d={
|
||||
"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":70,"key_label":0,"unicode":102,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
spacebar_3d={
|
||||
"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":32,"key_label":0,"unicode":32,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
right_click={
|
||||
"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":2,"canceled":false,"pressed":false,"double_click":false,"script":null)
|
||||
]
|
||||
}
|
||||
move_up_3d={
|
||||
"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)
|
||||
]
|
||||
}
|
||||
move_down_3d={
|
||||
"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":4194326,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
left_click={
|
||||
"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)
|
||||
]
|
||||
}
|
||||
|
||||
[layer_names]
|
||||
|
||||
@ -93,15 +166,18 @@ interact={
|
||||
2d_physics/layer_4="projectiles"
|
||||
2d_physics/layer_5="bulkheads"
|
||||
2d_physics/layer_6="characters"
|
||||
3d_physics/layer_16="grip"
|
||||
|
||||
[physics]
|
||||
|
||||
common/physics_jitter_fix=0.0
|
||||
3d/default_linear_damp=0.0
|
||||
3d/sleep_threshold_linear=0.0
|
||||
2d/default_gravity=0.0
|
||||
2d/default_gravity_vector=Vector2(0, 0)
|
||||
2d/default_linear_damp=0.0
|
||||
2d/sleep_threshold_linear=0.0
|
||||
common/physics_interpolation=true
|
||||
|
||||
[plugins]
|
||||
|
||||
|
||||
@ -125,7 +125,7 @@ func _generate_tooltip_text() -> String:
|
||||
return "\n".join(info)
|
||||
|
||||
func _format_seconds_to_mmss(seconds: float) -> String:
|
||||
var total_seconds = int(seconds)
|
||||
var minutes = total_seconds / 60
|
||||
var total_seconds: int = int(seconds)
|
||||
var minutes: int = total_seconds / 60
|
||||
var seconds_rem = total_seconds % 60
|
||||
return "%d min, %d sec" % [minutes, seconds_rem]
|
||||
|
||||
45
scenes/UI/ui_window.gd
Normal file
45
scenes/UI/ui_window.gd
Normal file
@ -0,0 +1,45 @@
|
||||
# CustomWindow.gd
|
||||
extends VBoxContainer
|
||||
class_name UiWindow
|
||||
|
||||
## Emitted when the custom "Flip" button is pressed.
|
||||
signal flip_button_pressed
|
||||
signal close_requested(c: Control)
|
||||
|
||||
@onready var title_bar: PanelContainer = $TitleBar
|
||||
@onready var title_label: Label = %TitleLabel
|
||||
@onready var flip_button: Button = %FlipButton
|
||||
@onready var close_button: Button = %CloseButton
|
||||
@onready var content_container: MarginContainer = %ContentContainer
|
||||
|
||||
var is_dragging: bool = false
|
||||
var title: String = ""
|
||||
|
||||
func _ready():
|
||||
# Connect the buttons to their functions
|
||||
close_button.pressed.connect(_close) # Or emit_signal("close_requested")
|
||||
flip_button.pressed.connect(flip_button_pressed.emit)
|
||||
|
||||
# Connect the title bar's input signal to handle dragging
|
||||
title_bar.gui_input.connect(_on_title_bar_gui_input)
|
||||
|
||||
# Set the window title from the property
|
||||
title_label.text = title
|
||||
|
||||
func _close():
|
||||
close_requested.emit(self)
|
||||
|
||||
# This function adds your main content (like the PanelFrame) into the window.
|
||||
func set_content(content_node: Node):
|
||||
for child in content_container.get_children():
|
||||
content_container.remove_child(child)
|
||||
|
||||
content_container.add_child(content_node)
|
||||
|
||||
func _on_title_bar_gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
is_dragging = event.is_pressed()
|
||||
|
||||
if event is InputEventMouseMotion and is_dragging:
|
||||
# When dragging, move the entire window by the mouse's relative motion.
|
||||
self.position += event.relative
|
||||
1
scenes/UI/ui_window.gd.uid
Normal file
1
scenes/UI/ui_window.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://d3g84xgbh8nlp
|
||||
44
scenes/UI/ui_window.tscn
Normal file
44
scenes/UI/ui_window.tscn
Normal file
@ -0,0 +1,44 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://cdnowhkg5cq88"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d3g84xgbh8nlp" path="res://scenes/UI/ui_window.gd" id="1_11aw0"]
|
||||
|
||||
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_11aw0"]
|
||||
size = Vector2(16, 16)
|
||||
|
||||
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_ishqf"]
|
||||
size = Vector2(16, 16)
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer"]
|
||||
offset_right = 196.0
|
||||
offset_bottom = 36.0
|
||||
script = ExtResource("1_11aw0")
|
||||
|
||||
[node name="TitleBar" type="PanelContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="TitleBar"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="TitleBar/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Placeholder Title"
|
||||
|
||||
[node name="FlipButton" type="Button" parent="TitleBar/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
icon = SubResource("PlaceholderTexture2D_11aw0")
|
||||
|
||||
[node name="CloseButton" type="Button" parent="TitleBar/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
icon = SubResource("PlaceholderTexture2D_ishqf")
|
||||
|
||||
[node name="ContentContainer" type="MarginContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/margin_left = 4
|
||||
theme_override_constants/margin_top = 4
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 4
|
||||
@ -3,7 +3,7 @@ class_name PilotBall
|
||||
|
||||
# --- Movement Constants (Friction Simulation) ---
|
||||
# When in open space (no module overlap), movement is zeroed out quickly.
|
||||
const EXTERIOR_DRAG_FACTOR: float = 0.05
|
||||
const EXTERIOR_DRAG_FACTOR: float = 0.05
|
||||
|
||||
# When pushing off hullplates (low friction, slow acceleration)
|
||||
const INTERIOR_SLUGGISH_SPEED: float = 100.0
|
||||
@ -11,7 +11,7 @@ const INTERIOR_SLUGGISH_ACCEL: float = 5 # Low acceleration, simulating mass and
|
||||
|
||||
# When gripping a ladder (high friction, direct control)
|
||||
const LADDER_SPEED: float = 100.0
|
||||
const LADDER_ACCEL: float = 20 # High acceleration, simulating direct grip
|
||||
const LADDER_ACCEL: float = 20 # High acceleration, simulating direct grip
|
||||
|
||||
@onready var camera: Camera2D = $Camera2D
|
||||
@onready var overlap_area: Area2D = $OverlapDetector
|
||||
@ -29,7 +29,7 @@ enum MovementState {
|
||||
}
|
||||
|
||||
var current_state: MovementState = MovementState.NO_CONTROL
|
||||
var ladder_area: Area2D = null # Area of the ladder currently overlapped
|
||||
var ladder_area: Area2D = null # Area of the ladder currently overlapped
|
||||
var is_grabbing_ladder: bool = false # True if 'Space' is held while on ladder
|
||||
|
||||
# --- Overlap Detection (Assuming you use Area2D for detection) ---
|
||||
@ -56,7 +56,6 @@ func set_interaction_input(just_pressed: bool, is_held: bool):
|
||||
func _ready():
|
||||
# Set up overlap signals if they aren't already connected in the scene file
|
||||
# You must have an Area2D child on PilotBall to detect overlaps.
|
||||
|
||||
overlap_area.body_entered.connect(on_body_entered)
|
||||
overlap_area.body_exited.connect(on_body_exited)
|
||||
overlap_area.area_entered.connect(_on_station_area_entered)
|
||||
@ -94,18 +93,17 @@ func exit_station_state():
|
||||
func _physics_process(delta):
|
||||
# This script now runs on the server and its state is synced to clients.
|
||||
# It no longer checks for local input authority.
|
||||
|
||||
if current_state == MovementState.IN_STATION:
|
||||
move_and_slide()
|
||||
return
|
||||
|
||||
|
||||
_update_movement_state() # This function now uses the new variables
|
||||
process_interaction() # Process any interaction presses
|
||||
process_interaction() # Process any interaction presses
|
||||
|
||||
# Reset input flags for the next frame
|
||||
_interact_just_pressed = false
|
||||
_interact_held = false
|
||||
_interact_held = false
|
||||
# The 'input_dir' now comes from our variable, not the Input singleton.
|
||||
var input_dir = _movement_input
|
||||
|
||||
@ -120,18 +118,12 @@ func _physics_process(delta):
|
||||
|
||||
move_and_slide()
|
||||
|
||||
|
||||
# --- Ladder Input and Launch Logic ---
|
||||
|
||||
# This function is called every physics frame by _physics_process().
|
||||
func process_interaction():
|
||||
# If the interact button was not pressed this frame, do nothing.
|
||||
if not _interact_just_pressed:
|
||||
return
|
||||
|
||||
# If we just occupied a station this frame, do nothing more.
|
||||
# This prevents the same input from instantly disengaging.
|
||||
|
||||
|
||||
# Priority 1: Disengage from a station if we are in one.
|
||||
if current_station:
|
||||
current_station.disengage(self)
|
||||
@ -142,13 +134,13 @@ func process_interaction():
|
||||
current_station = nearby_station
|
||||
current_station.occupy(self)
|
||||
return
|
||||
|
||||
|
||||
# Priority 3: Handle ladder launch logic.
|
||||
# This part of the old logic was in _handle_interaction_input,
|
||||
# but it's cleaner to check for the release of the button here.
|
||||
if current_state == MovementState.LADDER_GRIP and not _interact_held:
|
||||
# Launch the player away from the ladder when the interact button is released.
|
||||
var launch_direction = -_movement_input.normalized()
|
||||
var launch_direction = - _movement_input.normalized()
|
||||
if launch_direction == Vector2.ZERO:
|
||||
# Default launch: use the character's forward direction
|
||||
launch_direction = Vector2.UP.rotated(rotation)
|
||||
@ -173,7 +165,7 @@ func _update_movement_state():
|
||||
if overlapping_modules > 0:
|
||||
if is_grabbing_ladder:
|
||||
# If we were grabbing a ladder but released 'interact', we transition to zero-G interior
|
||||
is_grabbing_ladder = false
|
||||
is_grabbing_ladder = false
|
||||
current_state = MovementState.ZERO_G_INTERIOR
|
||||
return
|
||||
|
||||
|
||||
@ -18,7 +18,7 @@ debug_color = Color(0.61528, 0.358023, 1, 1)
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
|
||||
[node name="Camera2D" type="Camera2D" parent="."]
|
||||
zoom = Vector2(2, 2)
|
||||
zoom = Vector2(4, 4)
|
||||
|
||||
[node name="OverlapDetector" type="Area2D" parent="."]
|
||||
|
||||
|
||||
@ -2,7 +2,6 @@
|
||||
class_name Module
|
||||
extends OrbitalBody2D
|
||||
|
||||
# --- New properties inherited from Spaceship ---
|
||||
@export var ship_name: String = "Unnamed Ship" # Only relevant for the root module
|
||||
@export var hull_integrity: float = 100.0 # This could also be a calculated property later
|
||||
|
||||
@ -72,7 +71,7 @@ func attach_component(component: Component, global_pos: Vector2, parent_piece: S
|
||||
# --- UPDATED: Logic now uses the helper function ---
|
||||
func _recalculate_collision_shape():
|
||||
# This logic is much simpler now. We just iterate over relevant children.
|
||||
var combined_polygons = []
|
||||
var _combined_polygons = []
|
||||
|
||||
for piece in get_structural_pieces():
|
||||
# You would use logic here to transform the piece's local shape
|
||||
@ -96,7 +95,6 @@ func clear_module():
|
||||
|
||||
_recalculate_collision_shape()
|
||||
|
||||
# --- New function inherited from Spaceship ---
|
||||
# Damage can have a position for breach effects.
|
||||
func take_damage(amount: float, damage_position: Vector2):
|
||||
hull_integrity -= amount
|
||||
|
||||
@ -1,12 +1,16 @@
|
||||
extends Component
|
||||
extends Area3D
|
||||
class_name Spawner
|
||||
|
||||
@onready var mp_spawner: MultiplayerSpawner = $MultiplayerSpawner
|
||||
|
||||
# This spawner will register itself with the GameManager when it enters the scene.
|
||||
func _ready():
|
||||
super()
|
||||
# super()
|
||||
# We wait one frame to ensure singletons are ready.
|
||||
await get_tree().process_frame
|
||||
GameManager.register_spawner(self)
|
||||
|
||||
func can_spawn() -> bool:
|
||||
return get_overlapping_bodies().is_empty()
|
||||
# We can add properties to the spawner later, like which faction it belongs to,
|
||||
# or a reference to the body it's orbiting for initial velocity calculation.
|
||||
|
||||
@ -1,7 +1,16 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dvpy3urgtm62n"]
|
||||
[gd_scene load_steps=3 format=3 uid="uid://dvpy3urgtm62n"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://db1u2qqihhnq4" path="res://scenes/ship/components/hardware/spawner.gd" id="1_lldyu"]
|
||||
|
||||
[node name="Spawner" type="Node2D"]
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_lldyu"]
|
||||
radius = 1.0
|
||||
|
||||
[node name="Spawner" type="Area3D"]
|
||||
script = ExtResource("1_lldyu")
|
||||
metadata/_custom_type_script = "uid://calosd13bkakg"
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("SphereShape3D_lldyu")
|
||||
|
||||
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="."]
|
||||
_spawnable_scenes = PackedStringArray("uid://7yc6a07xoccy")
|
||||
spawn_path = NodePath("..")
|
||||
|
||||
@ -1,58 +1,60 @@
|
||||
@tool
|
||||
class_name SystemStation
|
||||
extends Component
|
||||
|
||||
signal occupancy_changed(is_occupied: bool)
|
||||
|
||||
# --- Configuration ---
|
||||
@export var panels: Array[ControlPanel]
|
||||
var UiWindowScene = preload("res://scenes/UI/ui_window.tscn")
|
||||
|
||||
@export var panel_scenes: Array[PackedScene]
|
||||
@export var databank_installations: Array[Script]
|
||||
@export var installed_databanks: Array[Databank]
|
||||
|
||||
@onready var panel_frame: PanelFrame = $PanelFrame
|
||||
|
||||
## The saved schematic resource for this station's wiring.
|
||||
var wiring_schematic: WiringSchematic
|
||||
|
||||
# --- State ---
|
||||
var occupants: Array[PilotBall] = []
|
||||
var active_shard_instances: Array[Node] = []
|
||||
var active_shard_instances: Array[Databank] = []
|
||||
var persistent_panel_instances: Array[BasePanel] = []
|
||||
# NEW: We now track which panels belong to which occupant
|
||||
var occupant_panel_map: Dictionary = {}
|
||||
|
||||
# --- LIFECYCLE ---
|
||||
|
||||
func _ready():
|
||||
super()
|
||||
# --- FIX: Instantiate and initialize shards ONCE at startup ---
|
||||
# The logic is now persistent and runs as long as the station exists.
|
||||
var root_module = get_root_module()
|
||||
if not is_instance_valid(root_module):
|
||||
push_error("Station could not find its root module!")
|
||||
return
|
||||
|
||||
panel_frame.populate_panels(panel_scenes)
|
||||
#for panel_scene in panel_scenes:
|
||||
#if not panel_scene: continue
|
||||
#
|
||||
#var panel_instance = panel_scene.instantiate()
|
||||
#if not panel_instance is BasePanel:
|
||||
#panel_instance.queue_free()
|
||||
#continue
|
||||
#
|
||||
#panel_frame.add_child(panel_instance)
|
||||
#persistent_panel_instances.append(panel_instance)
|
||||
|
||||
for shard_resource in installed_databanks:
|
||||
if not shard_resource or not shard_resource.logic_script: continue
|
||||
|
||||
for DatabankScript in databank_installations:
|
||||
if not DatabankScript: continue
|
||||
var installed_databank = DatabankScript.new()
|
||||
if installed_databank is not Databank:
|
||||
installed_databank.queue_free()
|
||||
continue
|
||||
|
||||
add_child(installed_databank)
|
||||
active_shard_instances.append(installed_databank)
|
||||
if installed_databank.has_method("initialize"):
|
||||
installed_databank.initialize(root_module)
|
||||
|
||||
# for shard_resource in installed_databanks:
|
||||
# if not shard_resource or not shard_resource.logic_script: continue
|
||||
|
||||
var shard_instance = Node.new()
|
||||
shard_instance.set_script(shard_resource.logic_script)
|
||||
add_child(shard_instance) # Add as a permanent child
|
||||
active_shard_instances.append(shard_instance)
|
||||
# var shard_instance = Node.new()
|
||||
# shard_instance.set_script(shard_resource.logic_script)
|
||||
# add_child(shard_instance) # Add as a permanent child
|
||||
# active_shard_instances.append(shard_instance)
|
||||
|
||||
if shard_instance.has_method("initialize"):
|
||||
shard_instance.initialize(root_module)
|
||||
# if shard_instance.has_method("initialize"):
|
||||
# shard_instance.initialize(root_module)
|
||||
|
||||
_connect_internals(persistent_panel_instances, active_shard_instances)
|
||||
_connect_internals([], active_shard_instances)
|
||||
|
||||
# Future: Connections wbetween shards and other hardware would be made here.
|
||||
|
||||
@ -77,8 +79,8 @@ func occupy(character: PilotBall):
|
||||
character.enter_station_state()
|
||||
character.global_position = global_position # Move character to the station
|
||||
|
||||
# --- FIX: Launch UI for THIS character only ---
|
||||
on_player_interact(character)
|
||||
# --- Launch UI for THIS character only ---
|
||||
launch_interfaces_for_occupant(character)
|
||||
|
||||
occupancy_changed.emit(true)
|
||||
|
||||
@ -96,35 +98,50 @@ func disengage(character: PilotBall):
|
||||
|
||||
# --- UI MANAGEMENT ---
|
||||
|
||||
# This function is called when a player character interacts with the station.
|
||||
func on_player_interact(player: PilotBall):
|
||||
# 1. Ask our PanelWorld for a new window view.
|
||||
var window_view = await panel_frame.create_window_view()
|
||||
|
||||
# 2. Pass this fully-formed window to the player.
|
||||
var player_ui_container = player.get_ui_container()
|
||||
if is_instance_valid(player_ui_container):
|
||||
player_ui_container.add_child(window_view)
|
||||
window_view.popup_centered()
|
||||
|
||||
occupant_panel_map[player] = window_view # We now store the viewports to clean up
|
||||
|
||||
func close_interfaces_for_occupant(character: PilotBall):
|
||||
if occupant_panel_map.has(character):
|
||||
for panel in occupant_panel_map[character]:
|
||||
if is_instance_valid(panel):
|
||||
panel.queue_free()
|
||||
occupant_panel_map[character].queue_free()
|
||||
occupant_panel_map.erase(character)
|
||||
|
||||
func close_interface(c: Control):
|
||||
var occupant = occupant_panel_map.find_key(c)
|
||||
if occupant:
|
||||
occupant_panel_map[occupant].queue_free()
|
||||
occupant_panel_map.erase(occupant)
|
||||
|
||||
func launch_interfaces_for_occupant(character: PilotBall):
|
||||
var ui_container = character.get_ui_container()
|
||||
if not ui_container: return
|
||||
|
||||
var ui_window: UiWindow = UiWindowScene.instantiate()
|
||||
ui_container.add_child(ui_window)
|
||||
|
||||
ui_window.close_requested.connect(close_interface)
|
||||
ui_window.title = "Helm"
|
||||
|
||||
var frame: PanelFrame = PanelFrame.new()
|
||||
frame.build(panel_scenes, self)
|
||||
frame.databanks = active_shard_instances
|
||||
ui_window.set_content(frame)
|
||||
|
||||
ui_window.flip_button_pressed.connect(frame.toggle_wiring_mode)
|
||||
|
||||
# Store the panels created for this specific user
|
||||
occupant_panel_map[character] = ui_window
|
||||
|
||||
# --- Connect the new panels to the PERSISTENT shards ---
|
||||
_connect_internals(frame.installed_panels, active_shard_instances)
|
||||
|
||||
# --- WIRING LOGIC (Now connects temporary panels to persistent shards) ---
|
||||
|
||||
func _connect_internals(panel_instances: Array, shard_instances: Array):
|
||||
# This logic remains the same, but it's now called with the relevant instances
|
||||
# every time a user sits down.
|
||||
var lever_panel
|
||||
var button_panel
|
||||
var readout_screen
|
||||
var map_panel
|
||||
var lever_panel: ThrottleLeverPanel
|
||||
var button_panel: ButtonPanel
|
||||
var readout_screen: ReadoutScreenPanel
|
||||
var autopilot_output: ReadoutScreenPanel
|
||||
var map_panel: SensorPanel
|
||||
var sensor_shard: SensorSystemShard
|
||||
var helm_shard: HelmLogicShard
|
||||
var status_shard: ShipStatusShard
|
||||
@ -137,9 +154,11 @@ func _connect_internals(panel_instances: Array, shard_instances: Array):
|
||||
lever_panel = panel
|
||||
if panel is ButtonPanel:
|
||||
button_panel = panel
|
||||
if panel is ReadoutScreenPanel:
|
||||
if panel is ReadoutScreenPanel and not readout_screen:
|
||||
print("Panel is ReadoutScreen: %s" % panel)
|
||||
readout_screen = panel
|
||||
elif panel is ReadoutScreenPanel:
|
||||
autopilot_output = panel
|
||||
if panel is SensorPanel: # Look for the new map panel class
|
||||
map_panel = panel
|
||||
|
||||
@ -181,7 +200,12 @@ func _connect_internals(panel_instances: Array, shard_instances: Array):
|
||||
if readout_screen and status_shard:
|
||||
print("Wired: Status Shard -> Readout Screen")
|
||||
|
||||
status_shard.connect("status_updated", readout_screen.update_display)
|
||||
status_shard.status_updated.connect(readout_screen.update_display)
|
||||
|
||||
if autopilot_shard and autopilot_output:
|
||||
print("Wired: Autopilot Shard -> Autopilot Output")
|
||||
|
||||
autopilot_shard.fmt_out.connect(autopilot_output.update_display)
|
||||
|
||||
if map_panel and sensor_shard:
|
||||
# Connect the shard's "sensor_feed_updated" signal (blue wire)
|
||||
@ -192,13 +216,23 @@ func _connect_internals(panel_instances: Array, shard_instances: Array):
|
||||
if map_panel and nav_selection_shard:
|
||||
# Connect the shard's "sensor_feed_updated" signal (blue wire)
|
||||
# to the map's "update_sensor_feed" socket.
|
||||
map_panel.connect("body_selected_for_planning", nav_selection_shard.on_body_selected)
|
||||
map_panel.connect("body_selected_for_planning", nav_selection_shard.body_selected)
|
||||
print("Wired: Sensor Shard -> Map Panel (Sensor Feed)")
|
||||
|
||||
if nav_selection_shard and hohman_planner_shard:
|
||||
nav_selection_shard.target_selected.connect(hohman_planner_shard.on_target_updated)
|
||||
nav_selection_shard.target_selected.connect(hohman_planner_shard.target_updated)
|
||||
print("Wired: Nav Selection -> Maneuver Planner")
|
||||
|
||||
if hohman_planner_shard and autopilot_shard:
|
||||
hohman_planner_shard.maneuver_calculated.connect(autopilot_shard.on_maneuver_received)
|
||||
hohman_planner_shard.maneuver_calculated.connect(autopilot_shard.maneuver_received)
|
||||
print("Wired: Maneuver Planner -> Autopilot")
|
||||
|
||||
if autopilot_shard and helm_shard:
|
||||
helm_shard.thruster_calibrated.connect(autopilot_shard.set_thruster_calibration)
|
||||
|
||||
autopilot_shard.request_main_engine_thrust.connect(helm_shard.set_throttle_input)
|
||||
autopilot_shard.request_rotation_thrust.connect(helm_shard.set_rotation_input)
|
||||
autopilot_shard.request_rotation.connect(helm_shard.set_desired_rotation)
|
||||
autopilot_shard.request_attitude_hold.connect(helm_shard.set_attitude_hold)
|
||||
|
||||
print("Wired: Autopilot -> Helm")
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://2n42nstcj1n0"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://2reyxkr78ra0" path="res://scenes/ship/components/hardware/system_station.gd" id="1_8usqu"]
|
||||
[ext_resource type="Script" uid="uid://cgryue4aay4oa" path="res://scenes/ship/computer/panels/panel_world.gd" id="2_3288w"]
|
||||
[ext_resource type="Script" uid="uid://cadvugf4oqgvk" path="res://scenes/ship/computer/panels/frame/panel_frame.gd" id="3_p17qi"]
|
||||
|
||||
[sub_resource type="CircleShape2D" id="CircleShape2D_8usqu"]
|
||||
|
||||
@ -16,6 +16,9 @@ mass = 1.0
|
||||
shape = SubResource("CircleShape2D_8usqu")
|
||||
debug_color = Color(0, 0.551549, 0.918484, 0.42)
|
||||
|
||||
[node name="PanelFrame" type="Node" parent="."]
|
||||
script = ExtResource("2_3288w")
|
||||
metadata/_custom_type_script = "uid://cgryue4aay4oa"
|
||||
[node name="PanelFrame" type="Container" parent="."]
|
||||
visible = false
|
||||
offset_right = 1152.0
|
||||
offset_bottom = 576.0
|
||||
script = ExtResource("3_p17qi")
|
||||
metadata/_custom_type_script = "uid://cadvugf4oqgvk"
|
||||
|
||||
@ -48,16 +48,9 @@ func calculate_fuel_consumption(thrust_force: float, delta_time: float) -> float
|
||||
return mass_flow_rate * delta_time
|
||||
|
||||
# --- Public Methods ---
|
||||
func _on_body_entered(body: Node) -> void:
|
||||
# Check if the body we collided with is our own ship.
|
||||
if body is Spaceship:
|
||||
print("COLLISION WARNING: Thruster '%s' collided with the ship hull!" % self.name)
|
||||
else:
|
||||
print("Thruster '%s' collided with: %s" % [self.name, body.name])
|
||||
|
||||
# The controller calls this ONCE to activate the thruster.
|
||||
func turn_on():
|
||||
#print("Thruster: Recieved Turn On Signal")
|
||||
#print("THRUSTER: Recieved Turn On Signal")
|
||||
if enabled:
|
||||
is_firing = true
|
||||
|
||||
@ -66,7 +59,7 @@ func turn_on():
|
||||
|
||||
# The controller calls this ONCE to deactivate the thruster.
|
||||
func turn_off():
|
||||
#print("Thruster: Recieved Turn Off Signal")
|
||||
#print("THRUSTER: Recieved Turn Off Signal")
|
||||
is_firing = false
|
||||
|
||||
await get_tree().physics_frame
|
||||
|
||||
@ -1,11 +1,27 @@
|
||||
class_name BasePanel
|
||||
extends Control
|
||||
|
||||
|
||||
@export_range(1, 12, 1) var grid_width: int = 1
|
||||
@export_range(1, 8, 1) var grid_height: int = 2
|
||||
|
||||
var placed_in_col: int = 0
|
||||
var placed_in_row : int = 0
|
||||
|
||||
var _owning_station: SystemStation
|
||||
|
||||
const SocketScene = preload("res://scenes/ship/computer/wiring/socket.tscn")
|
||||
|
||||
# --- NEW: Wiring UI Nodes ---
|
||||
var inputs_container: VBoxContainer
|
||||
var outputs_container: VBoxContainer
|
||||
var socket_container: HSplitContainer
|
||||
var main_ui_content: Node # Reference to the panel's actual UI
|
||||
var all_sockets: Array[Socket] = []
|
||||
|
||||
func _get_minimum_size() -> Vector2:
|
||||
return Vector2(grid_width * Constants.UI_GRID_SIZE, grid_width * Constants.UI_GRID_SIZE)
|
||||
|
||||
## The SystemStation calls this function immediately after the panel is created.
|
||||
func initialize(station: SystemStation) -> void:
|
||||
_owning_station = station
|
||||
@ -15,3 +31,70 @@ func get_owning_station() -> SystemStation:
|
||||
if not is_instance_valid(_owning_station):
|
||||
push_warning("Owning station is not valid or has not been initialized for this panel.")
|
||||
return _owning_station
|
||||
|
||||
## Describes the signals this panel emits (e.g., "lever_pulled").
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return []
|
||||
|
||||
## Describes the functions this panel has to display data (e.g., "update_text").
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return []
|
||||
|
||||
# --- NEW: Function to toggle wiring mode ---
|
||||
func set_wiring_mode(is_wiring: bool):
|
||||
if is_wiring:
|
||||
# If we haven't created the wiring containers yet, do it now.
|
||||
if not is_instance_valid(socket_container):
|
||||
_setup_wiring_containers()
|
||||
|
||||
for child in get_children():
|
||||
child.hide()
|
||||
# Hide the main UI and show the sockets
|
||||
|
||||
socket_container.show()
|
||||
else:
|
||||
# Hide the sockets and show the main UI
|
||||
for child in get_children():
|
||||
child.show()
|
||||
|
||||
if is_instance_valid(socket_container):
|
||||
socket_container.hide()
|
||||
|
||||
func _setup_wiring_containers():
|
||||
# This function is called once to create the UI for the backside view.
|
||||
socket_container = HSplitContainer.new()
|
||||
|
||||
socket_container.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
socket_container.size_flags_vertical = SIZE_EXPAND_FILL
|
||||
add_child(socket_container)
|
||||
|
||||
outputs_container = VBoxContainer.new()
|
||||
outputs_container.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
socket_container.add_child(outputs_container)
|
||||
|
||||
inputs_container = VBoxContainer.new()
|
||||
inputs_container.size_flags_horizontal = SIZE_EXPAND_FILL
|
||||
socket_container.add_child(inputs_container)
|
||||
|
||||
# Keep a reference to the panel's main content. Assumes it's the first child.
|
||||
if get_child_count() > 1:
|
||||
main_ui_content = get_child(0)
|
||||
|
||||
_populate_sockets()
|
||||
|
||||
func _populate_sockets():
|
||||
all_sockets.clear()
|
||||
|
||||
# Populate Input Sockets
|
||||
for socket_name in get_input_sockets():
|
||||
var socket = SocketScene.instantiate()
|
||||
inputs_container.add_child(socket)
|
||||
socket.initialize(socket_name, Socket.SocketType.INPUT)
|
||||
all_sockets.append(socket)
|
||||
|
||||
# Populate Output Sockets
|
||||
for socket_name in get_output_sockets():
|
||||
var socket = SocketScene.instantiate()
|
||||
outputs_container.add_child(socket)
|
||||
socket.initialize(socket_name, Socket.SocketType.OUTPUT)
|
||||
all_sockets.append(socket)
|
||||
|
||||
@ -24,3 +24,13 @@ func _ready():
|
||||
CalibrateRcsBtn.pressed.connect(button_4_pressed.emit)
|
||||
CalculationBtn.pressed.connect(button_5_pressed.emit)
|
||||
AutopilotButton.pressed.connect(button_6_pressed.emit)
|
||||
|
||||
func get_output_sockets():
|
||||
return [
|
||||
"button_1_pressed",
|
||||
"button_2_pressed",
|
||||
"button_3_pressed",
|
||||
"button_4_pressed",
|
||||
"button_5_pressed",
|
||||
"button_6_pressed",
|
||||
]
|
||||
|
||||
@ -3,10 +3,11 @@
|
||||
[ext_resource type="Script" uid="uid://ojt1koy5h64m" path="res://scenes/ship/computer/UI/button_panel.gd" id="1_cwyso"]
|
||||
|
||||
[node name="ButtonPanel" type="VBoxContainer"]
|
||||
offset_right = 200.0
|
||||
offset_bottom = 200.0
|
||||
offset_right = 151.0
|
||||
offset_bottom = 206.0
|
||||
script = ExtResource("1_cwyso")
|
||||
grid_width = 2
|
||||
grid_height = 4
|
||||
|
||||
[node name="RCSPos" type="Button" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
@ -10,3 +10,6 @@ class_name ReadoutScreenPanel
|
||||
func update_display(text: String):
|
||||
if display:
|
||||
display.text = text
|
||||
|
||||
func get_input_sockets():
|
||||
return ["update_display"]
|
||||
|
||||
@ -3,10 +3,7 @@
|
||||
[ext_resource type="Script" uid="uid://laeom8fvlfkf" path="res://scenes/ship/computer/UI/readout_screen_panel.gd" id="1_w2pab"]
|
||||
|
||||
[node name="DisplayText" type="VBoxContainer"]
|
||||
offset_right = 200.0
|
||||
offset_bottom = 200.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
offset_right = 1.0
|
||||
script = ExtResource("1_w2pab")
|
||||
grid_width = 2
|
||||
|
||||
|
||||
@ -6,6 +6,8 @@ signal body_selected_for_planning(body: OrbitalBody2D)
|
||||
|
||||
@export var map_icon_scene: PackedScene
|
||||
|
||||
@onready var map_canvas: Control = %MapCanvas
|
||||
|
||||
const LABEL_CULLING_PIXEL_THRESHOLD = 65.0
|
||||
const ICON_CULLING_PIXEL_THRESHOLD = 40.0
|
||||
|
||||
@ -27,6 +29,13 @@ var follow_progress: float = 0.0:
|
||||
# We must redraw every time the progress changes.
|
||||
queue_redraw()
|
||||
|
||||
|
||||
func get_output_sockets():
|
||||
return ["body_selected_for_planning"]
|
||||
|
||||
func get_input_sockets():
|
||||
return ["update_sensor_feed"]
|
||||
|
||||
# This is now the primary input for the map. It receives the "sensor feed".
|
||||
func update_sensor_feed(all_bodies: Array[OrbitalBody2D]):
|
||||
# This function replaces the old _populate_map logic.
|
||||
@ -45,7 +54,7 @@ func update_sensor_feed(all_bodies: Array[OrbitalBody2D]):
|
||||
for body in bodies_in_feed:
|
||||
if not body in icon_map:
|
||||
var icon = map_icon_scene.instantiate() as MapIcon
|
||||
add_child(icon)
|
||||
map_canvas.add_child(icon)
|
||||
icon.initialize(body)
|
||||
icon_map[body] = icon
|
||||
icon.selected.connect(_on_map_icon_selected)
|
||||
@ -82,29 +91,30 @@ func draw_projected_orbits(bodies_to_project: Array[OrbitalBody2D]):
|
||||
var focal_body = bodies_to_project[0]
|
||||
|
||||
# 2. Call the projection function
|
||||
var paths = OrbitalMechanics.project_n_body_paths(bodies_to_project, 20, 10) # 500 steps, 10 min each, global space
|
||||
# 3. Draw the paths
|
||||
for body in paths:
|
||||
var path_points = paths[body]
|
||||
|
||||
var scaled_path_points = PackedVector2Array()
|
||||
|
||||
for point in path_points:
|
||||
# Ensure path is drawn relative to the main focal body (the star)
|
||||
var path_world_pos = point + focal_body.global_position
|
||||
var relative_pos = path_world_pos - focal_body.global_position
|
||||
var scaled_pos = (relative_pos * map_scale) + map_offset + map_center
|
||||
scaled_path_points.append(scaled_pos)
|
||||
|
||||
if scaled_path_points.size() > 1:
|
||||
draw_polyline(scaled_path_points, Color(Color.WHITE, 0.2), 1.0, true)
|
||||
#var paths = OrbitalMechanics.project_n_body_paths(bodies_to_project, 20, 10) # 500 steps, 10 min each, global space
|
||||
## 3. Draw the paths
|
||||
#for body in paths:
|
||||
#var path_points = paths[body]
|
||||
#
|
||||
#var scaled_path_points = PackedVector2Array()
|
||||
#
|
||||
#for point in path_points:
|
||||
## Ensure path is drawn relative to the main focal body (the star)
|
||||
#var path_world_pos = point + focal_body.global_position
|
||||
#var relative_pos = path_world_pos - focal_body.global_position
|
||||
#var scaled_pos = (relative_pos * map_scale) + map_offset + map_center
|
||||
#scaled_path_points.append(scaled_pos)
|
||||
#
|
||||
#if scaled_path_points.size() > 1:
|
||||
#draw_polyline(scaled_path_points, Color(Color.WHITE, 0.2), 1.0, true)
|
||||
|
||||
func _update_icon_positions():
|
||||
if not is_instance_valid(focal_body): return
|
||||
|
||||
var map_center = get_rect().size / 2.0
|
||||
var map_center = map_canvas.get_rect().size / 2.0
|
||||
|
||||
# --- MODIFIED: Continuous follow logic ---
|
||||
# TODO: Follow logic broke when map_canvas was introduced
|
||||
# --- Continuous follow logic ---
|
||||
if is_instance_valid(followed_body):
|
||||
# Calculate the ideal offset to center the followed body.
|
||||
var relative_target_pos = followed_body.global_position - focal_body.global_position
|
||||
@ -163,8 +173,8 @@ func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP or event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
var zoom_factor = 1.25 if event.button_index == MOUSE_BUTTON_WHEEL_UP else 1 / 1.25
|
||||
var mouse_pos = get_local_mouse_position()
|
||||
var map_center = get_rect().size / 2.0
|
||||
var mouse_pos = map_canvas.get_local_mouse_position()
|
||||
var map_center = map_canvas.get_rect().size / 2.0
|
||||
|
||||
var point_under_mouse_world = (mouse_pos - map_center - map_offset) / map_scale
|
||||
map_scale *= zoom_factor
|
||||
|
||||
@ -7,9 +7,14 @@
|
||||
clip_contents = true
|
||||
layout_mode = 3
|
||||
anchors_preset = 0
|
||||
offset_right = 600.0
|
||||
offset_bottom = 400.0
|
||||
mouse_filter = 1
|
||||
script = ExtResource("1_5yxry")
|
||||
map_icon_scene = ExtResource("2_kvnmq")
|
||||
grid_width = 4
|
||||
grid_width = 6
|
||||
grid_height = 4
|
||||
|
||||
[node name="MapCanvas" type="Control" parent="."]
|
||||
unique_name_in_owner = true
|
||||
anchors_preset = 0
|
||||
offset_right = 40.0
|
||||
offset_bottom = 40.0
|
||||
|
||||
@ -11,6 +11,11 @@ func _ready():
|
||||
# Connect the slider's signal to our custom signal
|
||||
lever_slider.value_changed.connect(func(value): emit_signal("lever_value_changed", value))
|
||||
|
||||
## Describes the signals this panel emits (e.g., "lever_pulled").
|
||||
func get_output_sockets():
|
||||
return ["lever_value_changed"]
|
||||
|
||||
|
||||
#func _process(delta):
|
||||
## Allow keyboard control as well
|
||||
#var input_value = Input.get_action_strength("thrust_forward") - Input.get_action_strength("thrust_backward")
|
||||
|
||||
@ -3,11 +3,10 @@
|
||||
[ext_resource type="Script" uid="uid://dho2ww3hmmsra" path="res://scenes/ship/computer/UI/throttle_lever_panel.gd" id="1_q6svd"]
|
||||
|
||||
[node name="ThrottleLeverPanel" type="VBoxContainer"]
|
||||
offset_right = 100.0
|
||||
offset_bottom = 200.0
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
offset_right = 63.0
|
||||
offset_bottom = 35.0
|
||||
script = ExtResource("1_q6svd")
|
||||
grid_height = 4
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
@ -2,13 +2,6 @@
|
||||
class_name ControlPanel
|
||||
extends Resource
|
||||
|
||||
enum LayoutAnchor {
|
||||
TOP_LEFT, TOP_RIGHT,
|
||||
BOTTOM_LEFT, BOTTOM_RIGHT,
|
||||
CENTER_TOP, CENTER_BOTTOM,
|
||||
CENTER_LEFT, CENTER_RIGHT
|
||||
}
|
||||
|
||||
## The UI scene for this panel (e.g., a lever, a screen).
|
||||
@export var ui_scene: PackedScene
|
||||
|
||||
|
||||
43
scenes/ship/computer/data_types/data_types.gd
Normal file
43
scenes/ship/computer/data_types/data_types.gd
Normal file
@ -0,0 +1,43 @@
|
||||
class_name DataTypes
|
||||
extends Node
|
||||
|
||||
# TODO: Add comments and export tooltips for these classes so players can understand what they hold
|
||||
|
||||
class ThrusterCalibration:
|
||||
var thruster_data: Dictionary[Thruster, ThrusterData]
|
||||
var max_pos_torque: float
|
||||
var max_neg_torque: float
|
||||
|
||||
class ThrusterData:
|
||||
enum ThrusterType {
|
||||
LINEAR,
|
||||
ROTATIONAL,
|
||||
UNCALIBRATED
|
||||
}
|
||||
var thruster_node: Thruster
|
||||
var thruster_type: ThrusterType = ThrusterType.UNCALIBRATED
|
||||
var measured_torque: float # The rotational force it provides
|
||||
var measured_thrust: float # The linear force it provides
|
||||
|
||||
class ImpulsiveBurnPlan:
|
||||
var delta_v_magnitude: float
|
||||
var wait_time: float = 0.0
|
||||
var burn_duration: float
|
||||
var desired_rotation_rad: float
|
||||
|
||||
class PathProjection:
|
||||
var body_ref: OrbitalBody2D
|
||||
var points: Array[PathPoint]
|
||||
|
||||
func _init(b: OrbitalBody2D):
|
||||
body_ref = b
|
||||
|
||||
class PathPoint:
|
||||
var time: float # Time in seconds from the start of the projection
|
||||
var position: Vector2
|
||||
var velocity: Vector2
|
||||
|
||||
func _init(t: float, p: Vector2, v: Vector2):
|
||||
time = t
|
||||
position = p
|
||||
velocity = v
|
||||
1
scenes/ship/computer/data_types/data_types.gd.uid
Normal file
1
scenes/ship/computer/data_types/data_types.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://jew2ur3plyy6
|
||||
@ -1,14 +1,16 @@
|
||||
@tool
|
||||
class_name Databank
|
||||
extends Resource
|
||||
extends Node
|
||||
|
||||
## The script containing the logic for this shard.
|
||||
@export var logic_script: Script
|
||||
var root_module: Module
|
||||
|
||||
# --- Initialization ---
|
||||
func initialize(ship_root: Module):
|
||||
self.root_module = ship_root
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Dictionary:
|
||||
return {}
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return []
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_signals() -> Dictionary:
|
||||
return {}
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return []
|
||||
|
||||
274
scenes/ship/computer/panels/frame/panel_frame.gd
Normal file
274
scenes/ship/computer/panels/frame/panel_frame.gd
Normal file
@ -0,0 +1,274 @@
|
||||
@tool
|
||||
extends Container
|
||||
class_name PanelFrame
|
||||
|
||||
# A column is defined as Constants.UI_GRID_SIZE pixels in width
|
||||
@export var columns = 12
|
||||
# A row is defined as Constants.UI_GRID_SIZE pixels in height
|
||||
@export var rows = 6
|
||||
|
||||
enum WiringState { IDLE, DRAGGING_WIRE }
|
||||
var current_state: WiringState = WiringState.IDLE
|
||||
var current_schematic: WiringSchematic
|
||||
var active_wire_points: Array[Vector2] = []
|
||||
var start_socket: Socket
|
||||
var end_socket: Socket
|
||||
var wiring_mode: bool = false
|
||||
|
||||
var databanks: Array[Databank]
|
||||
var databanks_container: GridContainer
|
||||
|
||||
# --- NO CHANGE HERE ---
|
||||
# This getter is a nice way to access only the BasePanel children.
|
||||
var installed_panels: Array[BasePanel]:
|
||||
get:
|
||||
installed_panels = []
|
||||
for child in get_children():
|
||||
if child is BasePanel:
|
||||
installed_panels.append(child as BasePanel)
|
||||
return installed_panels
|
||||
|
||||
func _init() -> void:
|
||||
size = Vector2(columns * Constants.UI_GRID_SIZE, rows * Constants.UI_GRID_SIZE)
|
||||
|
||||
# --- NEW FUNCTION ---
|
||||
# This is a crucial function for all Control nodes, especially containers.
|
||||
# It tells the layout system the smallest size this container can be.
|
||||
func _get_minimum_size() -> Vector2:
|
||||
# The minimum size is simply the grid dimensions multiplied by the pixel size.
|
||||
return Vector2(columns * Constants.UI_GRID_SIZE, rows * Constants.UI_GRID_SIZE)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_SORT_CHILDREN:
|
||||
_sort_children()
|
||||
|
||||
func build(panel_scenes: Array[PackedScene], station: SystemStation):
|
||||
# Instead of manually calling our placement function, we tell Godot
|
||||
# that the layout needs to be updated. Godot will then call
|
||||
# _notification(NOTIFICATION_SORT_CHILDREN) for us at the correct time.
|
||||
var col = 0
|
||||
var row = 0
|
||||
#print("STATION: Building panels using autolayout")
|
||||
|
||||
for panel_scene in panel_scenes:
|
||||
if not panel_scene: continue
|
||||
|
||||
var panel_instance = panel_scene.instantiate()
|
||||
if not panel_instance is BasePanel:
|
||||
panel_instance.queue_free()
|
||||
continue
|
||||
|
||||
var panel: BasePanel = panel_instance as BasePanel
|
||||
panel.initialize(station)
|
||||
|
||||
# Store the grid coordinates on the panel itself. The container will use
|
||||
# this information when it arranges its children.
|
||||
if panel.grid_height <= self.rows - row:
|
||||
panel.placed_in_col = col
|
||||
panel.placed_in_row = row
|
||||
add_child(panel)
|
||||
#print(" - panel %s placed at: Col %s, Row %s" % [panel, col, row])
|
||||
row += panel.grid_height
|
||||
else:
|
||||
var last_panel = get_children()[-1]
|
||||
col += last_panel.grid_width
|
||||
row = 0
|
||||
panel.placed_in_col = col
|
||||
panel.placed_in_row = row
|
||||
add_child(panel)
|
||||
#print(" - panel %s placed at: Col %s, Row %s" % [panel, col, row])
|
||||
row += panel.grid_height
|
||||
|
||||
queue_sort()
|
||||
|
||||
|
||||
# This is the core logic. It positions and sizes every child.
|
||||
func _sort_children():
|
||||
#print("PanelFrame Sorting children")
|
||||
for child in get_children():
|
||||
if child == databanks_container:
|
||||
print("Databanks container found %s" % child)
|
||||
fit_child_in_rect(child, Rect2(Vector2(0, rows * Constants.UI_GRID_SIZE), child.size))
|
||||
continue
|
||||
# Skip any nodes that aren't a BasePanel.
|
||||
if not child is BasePanel:
|
||||
continue
|
||||
|
||||
|
||||
var panel := child as BasePanel
|
||||
|
||||
# Calculate the desired position based on the panel's stored grid coordinates.
|
||||
var start_pos = Vector2(panel.placed_in_col * Constants.UI_GRID_SIZE, panel.placed_in_row * Constants.UI_GRID_SIZE)
|
||||
|
||||
# Calculate the desired size based on the panel's width and height in grid units.
|
||||
var panel_size = Vector2(panel.grid_width * Constants.UI_GRID_SIZE, panel.grid_height * Constants.UI_GRID_SIZE)
|
||||
|
||||
#print(" - %s, Pos %s Size %s" % [panel, start_pos, panel_size])
|
||||
|
||||
# This single function tells the container to position AND size the child
|
||||
# within the given rectangle. The Rect2's origin is the position.
|
||||
fit_child_in_rect(panel, Rect2(start_pos, panel_size))
|
||||
|
||||
# TODO: Expose grid to install panels
|
||||
|
||||
func toggle_wiring_mode():
|
||||
wiring_mode = !wiring_mode
|
||||
|
||||
for panel in installed_panels:
|
||||
panel.set_wiring_mode(wiring_mode)
|
||||
|
||||
if wiring_mode:
|
||||
_build_databanks(databanks)
|
||||
pass
|
||||
|
||||
if is_instance_valid(databanks_container):
|
||||
if wiring_mode: databanks_container.show()
|
||||
else: databanks_container.hide()
|
||||
|
||||
class InstalledDatabank:
|
||||
extends Control
|
||||
|
||||
var databank_ref: Databank
|
||||
var all_sockets: Array[Socket] = []
|
||||
var SocketScene: PackedScene = preload("res://scenes/ship/computer/wiring/socket.tscn")
|
||||
var inputs_container: VBoxContainer
|
||||
var outputs_container: VBoxContainer
|
||||
|
||||
func _populate_sockets():
|
||||
all_sockets.clear()
|
||||
|
||||
if not is_instance_valid(inputs_container):
|
||||
inputs_container = VBoxContainer.new()
|
||||
add_child(inputs_container)
|
||||
|
||||
if not is_instance_valid(outputs_container):
|
||||
outputs_container = VBoxContainer.new()
|
||||
add_child(outputs_container)
|
||||
|
||||
# Populate Input Sockets
|
||||
for socket_name in databank_ref.get_input_sockets():
|
||||
var socket = SocketScene.instantiate()
|
||||
inputs_container.add_child(socket)
|
||||
socket.initialize(socket_name, Socket.SocketType.INPUT)
|
||||
all_sockets.append(socket)
|
||||
|
||||
# Populate Output Sockets
|
||||
for socket_name in databank_ref.get_output_sockets():
|
||||
var socket = SocketScene.instantiate()
|
||||
outputs_container.add_child(socket)
|
||||
socket.initialize(socket_name, Socket.SocketType.OUTPUT)
|
||||
all_sockets.append(socket)
|
||||
|
||||
func _build_databanks(dbs_to_install: Array[Databank]):
|
||||
if not is_instance_valid(databanks_container):
|
||||
databanks_container = GridContainer.new()
|
||||
databanks_container.columns = columns
|
||||
databanks_container.add_theme_constant_override("h_separation", Constants.UI_GRID_SIZE * 3)
|
||||
databanks_container.add_theme_constant_override("v_separation", Constants.UI_GRID_SIZE + 16)
|
||||
add_child(databanks_container)
|
||||
|
||||
var installed_databanks = databanks_container.get_children()
|
||||
|
||||
for to_install in dbs_to_install:
|
||||
if installed_databanks.any(func(existing_db): return existing_db.databank_ref == to_install):
|
||||
continue
|
||||
|
||||
var installed_databank = InstalledDatabank.new()
|
||||
installed_databank.databank_ref = to_install
|
||||
databanks_container.add_child(installed_databank)
|
||||
installed_databank._populate_sockets()
|
||||
|
||||
func _gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton:
|
||||
# --- Start or End a Wire ---
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if event.is_pressed():
|
||||
|
||||
var socket = _get_socket_at_pos(event.position)
|
||||
if socket:
|
||||
current_state = WiringState.DRAGGING_WIRE
|
||||
if not start_socket:
|
||||
# start new wire
|
||||
start_socket = socket
|
||||
# Add start point to wire points
|
||||
active_wire_points.append(start_socket.icon.get_global_rect().get_center() - get_global_position())
|
||||
elif start_socket and socket.socket_type != start_socket.socket_type:
|
||||
end_socket = socket
|
||||
_save_new_connection()
|
||||
_reset_wiring_state()
|
||||
elif current_state == WiringState.DRAGGING_WIRE:
|
||||
# Add intermediate point
|
||||
active_wire_points.append(get_local_mouse_position())
|
||||
|
||||
elif event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
# Pop Last Point
|
||||
active_wire_points.remove_at(active_wire_points.size() - 1)
|
||||
|
||||
# Check if wire points are empty, then we remove the whole wire
|
||||
if active_wire_points.size() <= 0:
|
||||
_reset_wiring_state()
|
||||
|
||||
if event is InputEventMouseMotion and current_state == WiringState.DRAGGING_WIRE:
|
||||
queue_redraw()
|
||||
|
||||
func _draw():
|
||||
# 1. Draw all saved wires from the schematic.
|
||||
if current_schematic:
|
||||
for connection in current_schematic.connections:
|
||||
if connection.path_points.size() > 1:
|
||||
_draw_wire_path(connection.path_points, Color.GREEN)
|
||||
|
||||
# 2. Draw the active wire being dragged by the user.
|
||||
if current_state == WiringState.DRAGGING_WIRE:
|
||||
var live_path: Array[Vector2] = active_wire_points.duplicate()
|
||||
live_path.append(get_local_mouse_position())
|
||||
if live_path.size() > 1:
|
||||
_draw_wire_path(live_path, Color.YELLOW)
|
||||
|
||||
# --- NEW: Helper function to draw a multi-point path ---
|
||||
func _draw_wire_path(points: Array[Vector2], color: Color):
|
||||
for i in range(points.size() - 1):
|
||||
var p1 = points[i]
|
||||
var p2 = points[i+1]
|
||||
# var control_offset = Vector2(abs(p2.x - p1.x) * 0.5, 0)
|
||||
draw_line(p1, p2, color, 3.0)
|
||||
|
||||
func _save_new_connection():
|
||||
var new_connection = WireConnection.new()
|
||||
if start_socket.socket_type == end_socket.socket_type:
|
||||
push_error("Start socket and end socket of same type!")
|
||||
return
|
||||
|
||||
if start_socket.socket_type == Socket.SocketType.INPUT:
|
||||
new_connection.input_socket_name = start_socket.socket_name
|
||||
elif start_socket.socket_type == Socket.SocketType.OUTPUT:
|
||||
new_connection.output_socket_name = start_socket.socket_name
|
||||
|
||||
if end_socket.socket_type == Socket.SocketType.INPUT:
|
||||
new_connection.input_socket_name = end_socket.socket_name
|
||||
elif end_socket.socket_type == Socket.SocketType.OUTPUT:
|
||||
new_connection.output_socket_name = end_socket.socket_name
|
||||
|
||||
|
||||
var end_pos = end_socket.icon.get_global_rect().get_center() - get_global_position()
|
||||
active_wire_points.append(end_pos)
|
||||
new_connection.path_points = active_wire_points
|
||||
|
||||
if not current_schematic:
|
||||
current_schematic = WiringSchematic.new()
|
||||
current_schematic.connections.append(new_connection)
|
||||
print("Connection saved!")
|
||||
|
||||
func _reset_wiring_state():
|
||||
current_state = WiringState.IDLE
|
||||
start_socket = null
|
||||
end_socket = null
|
||||
active_wire_points.clear()
|
||||
queue_redraw()
|
||||
|
||||
func _get_socket_at_pos(global_pos: Vector2) -> Socket:
|
||||
for panel in installed_panels:
|
||||
for socket in panel.all_sockets:
|
||||
if is_instance_valid(socket) and socket.icon.get_global_rect().has_point(global_pos):
|
||||
return socket
|
||||
return null
|
||||
1
scenes/ship/computer/panels/frame/panel_frame.gd.uid
Normal file
1
scenes/ship/computer/panels/frame/panel_frame.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cadvugf4oqgvk
|
||||
@ -1,88 +0,0 @@
|
||||
# scripts/panel_world.gd
|
||||
class_name PanelFrame
|
||||
extends Node
|
||||
|
||||
# This PanelWorld owns a separate world just for its UI panels.
|
||||
var ui_world: World2D = World2D.new()
|
||||
|
||||
# The root node for all panels within the UI world.
|
||||
var panel_root: Control
|
||||
var grid_container: GridContainer
|
||||
|
||||
func _ready():
|
||||
# We need a SubViewport to host our separate world and keep it running.
|
||||
var viewport = SubViewport.new()
|
||||
viewport.world_2d = ui_world
|
||||
add_child(viewport)
|
||||
|
||||
# Create the root node for the panels inside the new world.
|
||||
panel_root = Control.new()
|
||||
panel_root.name = "PanelRoot"
|
||||
|
||||
# We add the panel_root to the scene tree via the viewport to make it active.
|
||||
viewport.add_child(panel_root)
|
||||
|
||||
## The station calls this to populate the world with its panels.
|
||||
func populate_panels(panel_scenes: Array[PackedScene]):
|
||||
if not is_instance_valid(panel_root): return
|
||||
|
||||
# Add the GridContainer to the panel_root to manage the layout.
|
||||
grid_container = GridContainer.new()
|
||||
panel_root.add_child(grid_container)
|
||||
|
||||
for panel_scene in panel_scenes:
|
||||
if not panel_scene: continue
|
||||
var panel_instance = panel_scene.instantiate()
|
||||
grid_container.add_child(panel_instance)
|
||||
# Any other setup for the panels would go here.
|
||||
|
||||
## The station calls this to get a view for a player.
|
||||
func create_window_view() -> Window:
|
||||
# Create a draggable Window for the player's UI.
|
||||
var window = Window.new()
|
||||
window.title = get_parent().name # Title the window after the station
|
||||
window.unresizable = true
|
||||
window.size = grid_container.size # Give it a default minimum size
|
||||
window.close_requested.connect(window.queue_free)
|
||||
|
||||
# Create the SubViewport setup to render the view.
|
||||
var vp_container = SubViewportContainer.new()
|
||||
window.add_child(vp_container)
|
||||
vp_container.stretch = true
|
||||
vp_container.anchors_preset = Control.LayoutPreset.PRESET_FULL_RECT
|
||||
vp_container.size = grid_container.size
|
||||
|
||||
var sub_viewport = SubViewport.new()
|
||||
sub_viewport.world_2d = ui_world # IMPORTANT: Point to our private UI world
|
||||
sub_viewport.size = Vector2(400,300)
|
||||
sub_viewport.transparent_bg = true
|
||||
vp_container.add_child(sub_viewport)
|
||||
|
||||
# Wait for the next idle frame. By this time, the GridContainer will have
|
||||
# calculated its size based on the panels added to it.
|
||||
await get_tree().process_frame
|
||||
|
||||
# Create a camera for this specific view.
|
||||
var camera = Camera2D.new()
|
||||
|
||||
# Center the camera on the panel layout.
|
||||
#camera.position = panel_root.size / 2
|
||||
# 2. Calculate the correct zoom to fit all the panels in the view.
|
||||
var bounds = panel_root.get_rect()
|
||||
var zoom_x = bounds.size.x / sub_viewport.size.x
|
||||
var zoom_y = bounds.size.y / sub_viewport.size.y
|
||||
# 2. Set the OFFSET to be the center of the panel grid.
|
||||
# This shifts the camera's VIEW to be centered on the panels.
|
||||
sub_viewport.add_child(camera)
|
||||
camera.offset = grid_container.size
|
||||
|
||||
# Center the camera on the grid of panels.
|
||||
#camera.position = panel_root.size / 2.0
|
||||
|
||||
# Use the larger zoom factor to ensure everything fits, and add a 10% margin.
|
||||
#camera.zoom = Vector2.ONE * max(zoom_x, zoom_y) * 1.1
|
||||
|
||||
# Forward input from the screen overlay to the world-space panel.
|
||||
vp_container.gui_input.connect(func(event): sub_viewport.push_input(event.duplicate()))
|
||||
|
||||
return window
|
||||
@ -1 +0,0 @@
|
||||
uid://cgryue4aay4oa
|
||||
@ -1,39 +1,50 @@
|
||||
# scenes/ship/computer/shards/autopilot_databank.gd
|
||||
extends Node
|
||||
extends Databank
|
||||
class_name AutopilotShard
|
||||
|
||||
signal execution_state_changed(is_executing: bool, status: String)
|
||||
signal fmt_out(text: String)
|
||||
signal request_attitude_hold(b: bool)
|
||||
signal request_rotation(r: float)
|
||||
signal request_rotation_thrust(r: float)
|
||||
signal request_main_engine_thrust(t: float)
|
||||
|
||||
# --- References ---
|
||||
var root_module: Module
|
||||
var helm_shard: HelmLogicShard # This will be wired up by the station
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["maneuver_received", "execute_plan", "set_thruster_calibration"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["execution_state_changed", "fmt_out", "request_attitude_hold", "request_rotation", "request_rotation_thrust", "request_main_engine_thrust"]
|
||||
|
||||
# --- State ---
|
||||
enum State { IDLE, WAITING_FOR_WINDOW, EXECUTING_BURN }
|
||||
var current_state: State = State.IDLE
|
||||
var current_plan: Array = []
|
||||
|
||||
func initialize(ship_root: Module):
|
||||
self.root_module = ship_root
|
||||
var current_plan: Array[DataTypes.ImpulsiveBurnPlan] = []
|
||||
var max_rot_time: float = 30.0
|
||||
var RCS_calibration: DataTypes.ThrusterCalibration
|
||||
var is_executing: bool = false
|
||||
var status: String = ""
|
||||
var current_timer: SceneTreeTimer
|
||||
|
||||
func _process(delta):
|
||||
if current_state == State.WAITING_FOR_WINDOW and not current_plan.is_empty():
|
||||
var next_burn = current_plan[0]
|
||||
next_burn.wait_time -= delta
|
||||
var fmt = ""
|
||||
var state_name = State.keys()[current_state]
|
||||
if current_timer and current_timer.time_left:
|
||||
var time_str = "%d:%02d" % [int(current_timer.time_left) / 60, int(current_timer.time_left) % 60]
|
||||
var interpolated_status = status % time_str
|
||||
fmt = "Autopilot: %s\n%s" % [state_name, interpolated_status]
|
||||
else:
|
||||
fmt = "Autopilot: %s\n%s" % [state_name, status]
|
||||
|
||||
var time_str = "%d:%02d" % [int(next_burn.wait_time) / 60, int(next_burn.wait_time) % 60]
|
||||
emit_signal("execution_state_changed", true, "Waiting for burn window: T- %s" % time_str)
|
||||
fmt_out.emit(fmt)
|
||||
|
||||
# TODO: Add logic to command rotation from Helm shard
|
||||
|
||||
if next_burn.wait_time <= 0:
|
||||
_execute_next_burn()
|
||||
|
||||
# INPUT SOCKET: Connected to the ManeuverPlanner's "maneuver_calculated" signal.
|
||||
func on_maneuver_received(plan: Array):
|
||||
func maneuver_received(plan: Array[DataTypes.ImpulsiveBurnPlan]):
|
||||
current_plan = plan
|
||||
print("AUTOPILOT: Maneuver plan received.")
|
||||
emit_signal("execution_state_changed", false, "Plan Received. Press Execute.")
|
||||
status = "Plan Received.\nPress Execute."
|
||||
# In a UI, this would enable the "Execute" button.
|
||||
|
||||
# UI ACTION: An "Execute" button on a panel would call this.
|
||||
@ -41,28 +52,119 @@ func execute_plan():
|
||||
if not current_plan.is_empty():
|
||||
current_state = State.WAITING_FOR_WINDOW
|
||||
print("AUTOPILOT: Executing plan. Waiting for first burn window.")
|
||||
|
||||
func _execute_next_burn():
|
||||
if current_plan.is_empty():
|
||||
current_state = State.IDLE
|
||||
emit_signal("execution_state_changed", false, "Maneuver complete.")
|
||||
return
|
||||
|
||||
for step in current_plan:
|
||||
status = "Performing Rotation: T- %f" % rad_to_deg(step.desired_rotation_rad)
|
||||
var time_elapsed: float = await _execute_autopilot_rotation(step)
|
||||
|
||||
current_timer = get_tree().create_timer(step.wait_time - time_elapsed)
|
||||
|
||||
status = "Waiting for burn window: T- %s"
|
||||
|
||||
await current_timer.timeout
|
||||
|
||||
await _execute_next_burn(step)
|
||||
|
||||
|
||||
func set_thruster_calibration(data: DataTypes.ThrusterCalibration):
|
||||
RCS_calibration = data
|
||||
|
||||
# --- PROCESSS FUNCTIONS: Functions being run to execute the steps of a planned transfer ---
|
||||
|
||||
func _execute_next_burn(step: DataTypes.ImpulsiveBurnPlan):
|
||||
current_state = State.EXECUTING_BURN
|
||||
var burn = current_plan.pop_front()
|
||||
|
||||
emit_signal("execution_state_changed", true, "Executing Burn 1...")
|
||||
print("AUTOPILOT: Commanding main engine burn for %.2f seconds." % burn.burn_duration)
|
||||
status = "Executing Main Engine Burn: %s"
|
||||
print("AUTOPILOT: Commanding main engine burn for %.2f seconds." % step.burn_duration)
|
||||
|
||||
# Command the helm to fire the main engine
|
||||
if is_instance_valid(helm_shard):
|
||||
helm_shard.set_throttle_input(1.0)
|
||||
await get_tree().create_timer(burn.burn_duration).timeout
|
||||
helm_shard.set_throttle_input(0.0)
|
||||
request_main_engine_thrust.emit(1.0)
|
||||
|
||||
current_timer = get_tree().create_timer(step.burn_duration)
|
||||
|
||||
await current_timer.timeout
|
||||
|
||||
request_main_engine_thrust.emit(0.0)
|
||||
|
||||
# Transition to the next state
|
||||
if not current_plan.is_empty():
|
||||
current_state = State.WAITING_FOR_WINDOW
|
||||
else:
|
||||
current_state = State.IDLE
|
||||
emit_signal("execution_state_changed", false, "Maneuver complete.")
|
||||
execution_state_changed.emit(false, "Maneuver complete.")
|
||||
|
||||
# --- AUTOPILOT "BANG-COAST-BANG" LOGIC (REFACTORED) ---
|
||||
func _execute_autopilot_rotation(step: DataTypes.ImpulsiveBurnPlan) -> float:
|
||||
var time_window = minf(step.wait_time, max_rot_time)
|
||||
var angle_to_turn = shortest_angle_between(root_module.rotation, step.desired_rotation_rad)
|
||||
|
||||
var init_time = Time.get_ticks_msec()
|
||||
|
||||
if abs(angle_to_turn) < 0.01:
|
||||
request_rotation.emit(step.desired_rotation_rad)
|
||||
request_attitude_hold.emit(true)
|
||||
|
||||
return 0.0
|
||||
|
||||
# --- Get the specific torque values for each phase ---
|
||||
var accel_torque = RCS_calibration.max_pos_torque if angle_to_turn > 0 else RCS_calibration.max_neg_torque
|
||||
var decel_torque = RCS_calibration.max_neg_torque if angle_to_turn > 0 else RCS_calibration.max_pos_torque
|
||||
|
||||
if accel_torque == 0 or decel_torque == 0:
|
||||
print("AUTOPILOT ERROR: Missing thrusters for a full rotation.")
|
||||
return 0.0
|
||||
|
||||
print(" - Performing rotation.")
|
||||
|
||||
# --- Asymmetrical Burn Calculation ---
|
||||
# This is a more complex kinematic problem. We solve for the peak velocity and individual times.
|
||||
var accel_angular_accel = accel_torque / root_module.inertia
|
||||
var decel_angular_accel = decel_torque / root_module.inertia
|
||||
|
||||
# Solve for peak angular velocity (ω_peak) and times (t1, t2)
|
||||
var peak_angular_velocity = (2 * angle_to_turn * accel_angular_accel * decel_angular_accel) / (accel_angular_accel + decel_angular_accel)
|
||||
peak_angular_velocity = sqrt(abs(peak_angular_velocity)) * sign(angle_to_turn)
|
||||
|
||||
var accel_burn_time = abs(peak_angular_velocity / accel_angular_accel)
|
||||
var decel_burn_time = abs(peak_angular_velocity / decel_angular_accel)
|
||||
|
||||
var total_maneuver_time = accel_burn_time + decel_burn_time
|
||||
|
||||
if total_maneuver_time > time_window:
|
||||
print("AUTOPILOT WARNING: Maneuver is impossible in the given time window. Performing max-power turn.")
|
||||
# Fallback to a simple 50/50 burn if time is too short.
|
||||
accel_burn_time = time_window / 2.0
|
||||
decel_burn_time = time_window / 2.0
|
||||
|
||||
# No coast time in this simplified model, but it could be added back with more complex math.
|
||||
|
||||
print(" - Asymmetrical Rotation Plan: Accel Burn %.2fs, Decel Burn %.2fs" % [accel_burn_time, decel_burn_time])
|
||||
|
||||
# --- Execute Maneuver ---
|
||||
|
||||
# ACCELERATION BURN
|
||||
request_rotation_thrust.emit(sign(angle_to_turn))
|
||||
await get_tree().create_timer(accel_burn_time).timeout
|
||||
|
||||
# DECELERATION BURN
|
||||
print(" - Rotation acceleration complete, executing deceleration burn.")
|
||||
request_rotation_thrust.emit(sign(-angle_to_turn))
|
||||
await get_tree().create_timer(decel_burn_time).timeout
|
||||
|
||||
print(" - Rotation de-acceleration complete, executing deceleration burn.")
|
||||
request_rotation.emit(step.desired_rotation_rad)
|
||||
request_attitude_hold.emit(true)
|
||||
|
||||
print("AUTOPILOT: Rotation maneuver complete.")
|
||||
|
||||
return init_time - Time.get_ticks_msec()
|
||||
|
||||
# --- HELPERS ---
|
||||
|
||||
# Calculates the shortest angle between two angles (in radians).
|
||||
# The result will be between -PI and +PI. The sign indicates the direction.
|
||||
func shortest_angle_between(from_angle: float, to_angle: float) -> float:
|
||||
var difference = fposmod(to_angle - from_angle, TAU)
|
||||
if difference > PI:
|
||||
return difference - TAU
|
||||
else:
|
||||
return difference
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://dwhpjwuobcqdu"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://ceqdi6jobefnc" path="res://scenes/ship/computer/shards/helm_autopilot_databank.gd" id="1_0abvf"]
|
||||
[ext_resource type="Script" path="res://scenes/ship/computer/shards/helm_autopilot_databank.gd" id="1_0abvf"]
|
||||
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_tpm1x"]
|
||||
|
||||
[resource]
|
||||
|
||||
@ -1,22 +1,26 @@
|
||||
extends Node
|
||||
|
||||
extends Databank
|
||||
class_name HelmLogicShard
|
||||
|
||||
# --- References ---
|
||||
var root_module: Module
|
||||
var thrusters: Array[Thruster] = []
|
||||
|
||||
# --- Ship Performance Data (calibrated) ---
|
||||
var max_positive_torque: float = 1.0
|
||||
var max_negative_torque: float = 1.0
|
||||
@onready var thrusters: Array[Thruster] = []
|
||||
|
||||
# --- PD Controller Constants ---
|
||||
@export var HOLD_KP: float = 8000.0 # Proportional gain
|
||||
@export var HOLD_KD: float = 1200.0 # Derivative gain
|
||||
|
||||
var target_rotation_rad: float = 0.0
|
||||
@onready var target_rotation_rad: float = 0.0
|
||||
var attitude_hold_enabled: bool = false
|
||||
|
||||
var thruster_calibration_data: DataTypes.ThrusterCalibration
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["shutdown_rcs", "calibrate_rcs_performance", "set_throttle_input", "set_rotation_input", "set_desired_rotation", "set_attitude_hold"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["thruster_calibrated"]
|
||||
|
||||
# The Station calls this after instantiating the shard.
|
||||
func initialize(ship_root: Module):
|
||||
self.root_module = ship_root
|
||||
@ -30,7 +34,6 @@ func initialize(ship_root: Module):
|
||||
# Default to holding the initial attitude.
|
||||
target_rotation_rad = root_module.rotation
|
||||
|
||||
|
||||
func _physics_process(_delta):
|
||||
if not is_instance_valid(root_module): return
|
||||
|
||||
@ -46,7 +49,7 @@ func set_rotation_input(value: float):
|
||||
if abs(value) > 0.1:
|
||||
# Manual input overrides attitude hold.
|
||||
attitude_hold_enabled = false
|
||||
var desired_torque = (max_positive_torque if value > 0 else max_negative_torque) * value
|
||||
var desired_torque = (calibration_data.max_pos_torque if value > 0 else calibration_data.max_neg_torque) * value
|
||||
apply_rotational_thrust(desired_torque)
|
||||
else:
|
||||
# When input stops, re-engage hold at the current rotation.
|
||||
@ -58,21 +61,37 @@ func set_rotation_input(value: float):
|
||||
## It takes a value from 0.0 to 1.0.
|
||||
# --- REFACTORED: This is the key change ---
|
||||
func set_throttle_input(value: float):
|
||||
print("THRUSTER CONTROLLER: Throttle input recieved: %f.1" % value)
|
||||
# This function now works with the simple on/off thrusters.
|
||||
for thruster in thrusters:
|
||||
if thruster.main_thruster:
|
||||
if not calibration_data:
|
||||
print("THRUSTER CONTROLLER: No Calibration Data Found")
|
||||
return
|
||||
for thruster in calibration_data.thruster_data:
|
||||
var thruster_data: DataTypes.ThrusterData = calibration_data.thruster_data[thruster]
|
||||
if thruster_data.thruster_type == DataTypes.ThrusterData.ThrusterType.LINEAR:
|
||||
print(" - Main thruster identified with thrust capacity: %f" % thruster_data.measured_thrust)
|
||||
if value > 0.1:
|
||||
print(" - Main Engine Activated")
|
||||
thruster.turn_on()
|
||||
else:
|
||||
print(" - Main Engine Shut Off")
|
||||
thruster.turn_off()
|
||||
|
||||
func set_desired_rotation(r: float):
|
||||
target_rotation_rad = r
|
||||
|
||||
func set_attitude_hold(hold: bool):
|
||||
attitude_hold_enabled = hold
|
||||
|
||||
# --- LOGIC (Migrated from ThrusterController.gd) ---
|
||||
|
||||
func _perform_manual_hold():
|
||||
var error = shortest_angle_between(root_module.rotation, target_rotation_rad)
|
||||
var desired_torque = (error * HOLD_KP) - (root_module.angular_velocity * HOLD_KD)
|
||||
|
||||
apply_rotational_thrust(desired_torque)
|
||||
if abs(error) > 0.001:
|
||||
var desired_torque = (error * HOLD_KP) - (root_module.angular_velocity * HOLD_KD)
|
||||
|
||||
apply_rotational_thrust(desired_torque)
|
||||
else: apply_rotational_thrust(0.0)
|
||||
|
||||
# --- REFACTORED: This is the other key change ---
|
||||
func apply_rotational_thrust(desired_torque: float):
|
||||
@ -80,15 +99,16 @@ func apply_rotational_thrust(desired_torque: float):
|
||||
return
|
||||
|
||||
# Iterate through all available RCS thrusters that have been calibrated
|
||||
for thruster in thruster_data_map:
|
||||
var thruster_data: ThrusterData = thruster_data_map[thruster]
|
||||
for thruster in calibration_data.thruster_data:
|
||||
var thruster_data: DataTypes.ThrusterData = calibration_data.thruster_data[thruster]
|
||||
|
||||
# If this thruster can help apply the desired torque, turn it on.
|
||||
# Otherwise, explicitly turn it off to ensure it's not firing incorrectly.
|
||||
if sign(thruster_data.measured_torque) == sign(desired_torque) and desired_torque != 0:
|
||||
thruster.turn_on()
|
||||
else:
|
||||
thruster.turn_off()
|
||||
if thruster_data.thruster_type == DataTypes.ThrusterData.ThrusterType.ROTATIONAL:
|
||||
# If this thruster can help apply the desired torque, turn it on.
|
||||
# Otherwise, explicitly turn it off to ensure it's not firing incorrectly.
|
||||
if sign(thruster_data.measured_torque) == sign(desired_torque) and desired_torque != 0:
|
||||
thruster.turn_on()
|
||||
else:
|
||||
thruster.turn_off()
|
||||
|
||||
func shutdown_rcs():
|
||||
for thruster in thrusters:
|
||||
@ -106,6 +126,7 @@ func _find_all_thrusters(node: Node) -> Array[Thruster]:
|
||||
return thrusters
|
||||
|
||||
|
||||
# Angle difference in rad
|
||||
func shortest_angle_between(from_angle: float, to_angle: float) -> float:
|
||||
var difference = fposmod(to_angle - from_angle, TAU)
|
||||
if difference > PI:
|
||||
@ -113,15 +134,11 @@ func shortest_angle_between(from_angle: float, to_angle: float) -> float:
|
||||
else:
|
||||
return difference
|
||||
|
||||
signal thruster_calibrated(data: DataTypes.ThrusterCalibration)
|
||||
|
||||
var thruster_data_map: Dictionary = {}
|
||||
var calibration_data: DataTypes.ThrusterCalibration
|
||||
|
||||
# --- CALIBRATION LOGIC (Migrated from ThrusterController.gd) ---
|
||||
|
||||
# Inner class to store calibrated data for each thruster
|
||||
class ThrusterData:
|
||||
var thruster_node: Thruster
|
||||
var measured_torque: float # The rotational force it provides
|
||||
|
||||
## Manages the calibration sequence for all non-main thrusters.
|
||||
func calibrate_rcs_performance():
|
||||
@ -129,7 +146,7 @@ func calibrate_rcs_performance():
|
||||
|
||||
if not is_instance_valid(root_module): return
|
||||
|
||||
# --- THE FIX: Disable attitude hold during calibration ---
|
||||
# --- Disable attitude hold during calibration ---
|
||||
var original_attitude_hold_state = attitude_hold_enabled
|
||||
attitude_hold_enabled = false
|
||||
shutdown_rcs() # Ensure all thrusters are off before we start
|
||||
@ -137,30 +154,27 @@ func calibrate_rcs_performance():
|
||||
|
||||
print("Helm Shard: Attitude hold protocol: %s" % ("enabled" if attitude_hold_enabled else "disabled"))
|
||||
|
||||
thruster_data_map.clear()
|
||||
calibration_data = DataTypes.ThrusterCalibration.new()
|
||||
|
||||
for thruster in thrusters:
|
||||
if thruster.main_thruster: continue # Skip main engines
|
||||
var data: DataTypes.ThrusterData = await _calibrate_single_thruster(thruster)
|
||||
calibration_data.thruster_data[thruster] = data
|
||||
|
||||
var data: ThrusterData = await _calibrate_single_thruster(thruster)
|
||||
thruster_data_map[thruster] = data
|
||||
print(" - Calibrated %s: Torque(%.3f)" % [thruster.name, data.measured_torque])
|
||||
|
||||
print(thruster_data_map)
|
||||
print(calibration_data)
|
||||
# Now that we have the data, calculate the ship's max torque values
|
||||
max_positive_torque = 0.0
|
||||
max_negative_torque = 0.0
|
||||
for data in thruster_data_map.values():
|
||||
calibration_data.max_pos_torque = 0.0
|
||||
calibration_data.max_neg_torque = 0.0
|
||||
for data in calibration_data.thruster_data.values():
|
||||
if data.measured_torque > 0:
|
||||
max_positive_torque += data.measured_torque
|
||||
calibration_data.max_pos_torque += data.measured_torque
|
||||
else:
|
||||
max_negative_torque += abs(data.measured_torque)
|
||||
calibration_data.max_neg_torque += abs(data.measured_torque)
|
||||
|
||||
print("RCS Calibration Complete: Max Pos Torque: %.2f, Max Neg Torque: %.2f" % [max_positive_torque, max_negative_torque])
|
||||
print("RCS Calibration Complete: Max Pos Torque: %.2f, Max Neg Torque: %.2f" % [calibration_data.max_pos_torque, calibration_data.max_neg_torque])
|
||||
|
||||
# Auto-tune the PD controller with the new values
|
||||
if max_positive_torque > 0 and max_negative_torque > 0:
|
||||
var average_max_torque = (max_positive_torque + max_negative_torque) / 2.0
|
||||
if calibration_data.max_pos_torque > 0 and calibration_data.max_neg_torque > 0:
|
||||
var average_max_torque = (calibration_data.max_pos_torque + calibration_data.max_neg_torque) / 2.0
|
||||
HOLD_KP = average_max_torque * 0.1
|
||||
HOLD_KD = HOLD_KP * 1 # You can tune this multiplier
|
||||
print("PD Controller Auto-Tuned: Kp set to %.2f, Kd set to %.2f" % [HOLD_KP, HOLD_KD])
|
||||
@ -168,13 +182,17 @@ func calibrate_rcs_performance():
|
||||
attitude_hold_enabled = original_attitude_hold_state
|
||||
print("Helm Shard: Calibration complete. Attitude hold is now %s." % ("enabled" if attitude_hold_enabled else "disabled"))
|
||||
|
||||
thruster_calibration_data = calibration_data
|
||||
thruster_calibrated.emit(calibration_data)
|
||||
|
||||
## Performs a test fire of a single thruster and measures the resulting change in angular velocity.
|
||||
func _calibrate_single_thruster(thruster: Thruster) -> ThrusterData:
|
||||
var data = ThrusterData.new()
|
||||
func _calibrate_single_thruster(thruster: Thruster) -> DataTypes.ThrusterData:
|
||||
var data = DataTypes.ThrusterData.new()
|
||||
data.thruster_node = thruster
|
||||
|
||||
# Prepare for test: save initial state
|
||||
var initial_angular_velocity = root_module.angular_velocity
|
||||
var initial_linear_velocity = root_module.linear_velocity
|
||||
|
||||
var test_burn_duration = 0.5 # A very short burst
|
||||
|
||||
@ -188,6 +206,10 @@ func _calibrate_single_thruster(thruster: Thruster) -> ThrusterData:
|
||||
|
||||
# --- Measure Results ---
|
||||
var delta_angular_velocity = root_module.angular_velocity - initial_angular_velocity
|
||||
var delta_linear_velocity = root_module.linear_velocity - initial_linear_velocity
|
||||
|
||||
data.measured_torque = 0.0
|
||||
data.measured_thrust = 0.0
|
||||
|
||||
# --- Calculate Performance ---
|
||||
# Torque = inertia * angular_acceleration (alpha = dw/dt)
|
||||
@ -196,7 +218,21 @@ func _calibrate_single_thruster(thruster: Thruster) -> ThrusterData:
|
||||
else:
|
||||
data.measured_torque = 0.0
|
||||
push_warning("Root module inertia is 0. Cannot calibrate torque.")
|
||||
|
||||
if root_module.mass > 0:
|
||||
data.measured_thrust = root_module.mass * (delta_linear_velocity.length() / test_burn_duration)
|
||||
else:
|
||||
data.measured_thrust = 0.0
|
||||
push_warning("Root module mass is 0. Cannot calibrate torque.")
|
||||
|
||||
if data.measured_thrust > abs(data.measured_torque):
|
||||
print(" - Calibrated %s: Linear(%.3f)" % [thruster.name, data.measured_thrust])
|
||||
data.thruster_type = DataTypes.ThrusterData.ThrusterType.LINEAR
|
||||
elif data.measured_thrust < abs(data.measured_torque):
|
||||
print(" - Calibrated %s: Torque(%.3f)" % [thruster.name, data.measured_torque])
|
||||
data.thruster_type = DataTypes.ThrusterData.ThrusterType.ROTATIONAL
|
||||
|
||||
|
||||
# --- Cleanup: Counter the spin from the test fire ---
|
||||
if abs(data.measured_torque) > 0.001:
|
||||
var counter_torque = -data.measured_torque
|
||||
@ -204,7 +240,7 @@ func _calibrate_single_thruster(thruster: Thruster) -> ThrusterData:
|
||||
|
||||
# Find a thruster that can apply the counter-torque
|
||||
for other_thruster in thrusters:
|
||||
var other_data = thruster_data_map.get(other_thruster)
|
||||
var other_data = calibration_data.thruster_data.get(other_thruster)
|
||||
if other_data and sign(other_data.measured_torque) == sign(counter_torque):
|
||||
other_thruster.turn_on()
|
||||
await get_tree().create_timer(abs(counter_burn_duration)).timeout
|
||||
@ -212,6 +248,5 @@ func _calibrate_single_thruster(thruster: Thruster) -> ThrusterData:
|
||||
break # Use the first one we find
|
||||
|
||||
await get_tree().physics_frame
|
||||
root_module.angular_velocity = 0 # Final reset for safety
|
||||
|
||||
return data
|
||||
|
||||
@ -1,16 +1,22 @@
|
||||
extends Node
|
||||
extends Databank
|
||||
|
||||
class_name ShipStatusShard
|
||||
|
||||
## This shard emits a signal with the formatted ship status text.
|
||||
signal status_updated(text: String)
|
||||
|
||||
var root_module: Module
|
||||
|
||||
# Called by the Station when it's created.
|
||||
func initialize(ship_root: Module):
|
||||
self.root_module = ship_root
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return []
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["status_updated"]
|
||||
|
||||
func _physics_process(delta):
|
||||
if not is_instance_valid(root_module):
|
||||
return
|
||||
@ -23,7 +29,7 @@ func _physics_process(delta):
|
||||
var status_text = """
|
||||
[font_size=24]Ship Status[/font_size]
|
||||
[font_size=18]Rotation: %.1f deg[/font_size]
|
||||
[font_size=18]Ang. Vel.: %.3f deg/s[/font_size]
|
||||
[font_size=18]Ang. Vel.: %.2f deg/s[/font_size]
|
||||
[font_size=18]Velocity: %.2f m/s[/font_size]
|
||||
""" % [rotation_deg, angular_vel_dps, linear_vel_mps]
|
||||
|
||||
|
||||
69
scenes/ship/computer/shards/nav_brachistochrone_planner.gd
Normal file
69
scenes/ship/computer/shards/nav_brachistochrone_planner.gd
Normal file
@ -0,0 +1,69 @@
|
||||
# space_simulation/scenes/ship/computer/shards/nav_brachistochrone_planner.gd
|
||||
extends Databank
|
||||
class_name BrachistochronePlannerShard
|
||||
|
||||
## Emitted when a maneuver plan has been successfully calculated.
|
||||
signal maneuver_calculated(plan: Array[DataTypes.ImpulsiveBurnPlan])
|
||||
|
||||
# --- References ---
|
||||
var target_body: OrbitalBody2D = null
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["target_updated", "calculate_hohmann_transfer"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["maneuver_calculated"]
|
||||
|
||||
# INPUT SOCKET: Connected to the NavSelectionShard's "target_selected" signal.
|
||||
func target_updated(new_target: OrbitalBody2D):
|
||||
print("BRACHISTOCHRONE PLANNER: Target received %s." % new_target.name)
|
||||
target_body = new_target
|
||||
|
||||
# TODO: All positions and velocities for calculating should be gathered from a sensor databank
|
||||
# UI ACTION: A panel button would call this function.
|
||||
func calculate_brachistochrone_transfer():
|
||||
if not is_instance_valid(root_module) or not is_instance_valid(target_body):
|
||||
print("BRACHISTOCHRONE PLANNER: Cannot calculate without ship and target.")
|
||||
return
|
||||
|
||||
# 1. Get total main engine thrust from all thruster components
|
||||
# TODO: This should be gathered from a calibration shard
|
||||
var main_engine_thrust = 0.0
|
||||
for component in root_module.get_components():
|
||||
if component is Thruster and component.main_thruster:
|
||||
main_engine_thrust += component.max_thrust
|
||||
|
||||
if main_engine_thrust == 0.0 or root_module.mass == 0.0:
|
||||
print("BRACHISTOCHRONE PLANNER: Ship has no main engine thrust or mass.")
|
||||
return
|
||||
|
||||
var acceleration = main_engine_thrust / root_module.mass
|
||||
var distance = root_module.global_position.distance_to(target_body.global_position)
|
||||
|
||||
# Using the kinematic equation: d = (1/2)at^2, solved for t: t = sqrt(2d/a)
|
||||
# Since we accelerate for half the distance and decelerate for the other half:
|
||||
var time_for_half_journey = sqrt(distance / acceleration)
|
||||
|
||||
# --- Assemble the plan as two ImpulsiveBurnPlan steps ---
|
||||
var plan: Array[DataTypes.ImpulsiveBurnPlan] = []
|
||||
|
||||
# --- Step 1: Acceleration Burn ---
|
||||
var accel_burn = DataTypes.ImpulsiveBurnPlan.new()
|
||||
accel_burn.wait_time = 0 # Start immediately
|
||||
accel_burn.burn_duration = time_for_half_journey
|
||||
# The desired rotation is the direction vector from ship to target
|
||||
accel_burn.desired_rotation_rad = root_module.global_position.direction_to(target_body.global_position).angle() + (PI / 2.0)
|
||||
plan.append(accel_burn)
|
||||
|
||||
# --- Step 2: Deceleration Burn (The flip is handled by the autopilot between steps) ---
|
||||
var decel_burn = DataTypes.ImpulsiveBurnPlan.new()
|
||||
decel_burn.wait_time = 0 # No coasting period
|
||||
decel_burn.burn_duration = time_for_half_journey
|
||||
# The desired rotation is opposite the first burn
|
||||
decel_burn.desired_rotation_rad = accel_burn.desired_rotation_rad + PI
|
||||
plan.append(decel_burn)
|
||||
|
||||
print("BRACHISTOCHRONE PLANNER: Plan calculated. Total time: %.2f s" % (time_for_half_journey * 2.0))
|
||||
maneuver_calculated.emit(plan)
|
||||
@ -0,0 +1 @@
|
||||
uid://ghluwjd5c5ul
|
||||
@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://bnyce8i208qby"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://ghluwjd5c5ul" path="res://scenes/ship/computer/shards/nav_brachistochrone_planner.gd" id="1_asajk"]
|
||||
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_xdqj8"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_xdqj8")
|
||||
logic_script = ExtResource("1_asajk")
|
||||
metadata/_custom_type_script = "uid://osk1l75vlikn"
|
||||
@ -1,32 +1,35 @@
|
||||
# scenes/ship/computer/shards/maneuver_planner_databank.gd
|
||||
extends Node
|
||||
extends Databank
|
||||
class_name HohmanPlannerShard
|
||||
|
||||
## Emitted when a maneuver plan has been successfully calculated.
|
||||
signal maneuver_calculated(plan: Array)
|
||||
signal maneuver_calculated(plan: Array[DataTypes.ImpulsiveBurnPlan])
|
||||
|
||||
# --- References ---
|
||||
var root_module: Module
|
||||
var selection_shard: NavSelectionShard
|
||||
var target_body: OrbitalBody2D = null
|
||||
|
||||
# --- Inner class to hold maneuver data ---
|
||||
class ImpulsiveBurn:
|
||||
var delta_v_magnitude: float
|
||||
var wait_time: float = 0.0
|
||||
var burn_duration: float
|
||||
var desired_rotation_rad: float
|
||||
# --- Configurations ---
|
||||
var boost_factor: float = 1.0
|
||||
|
||||
func initialize(ship_root: Module):
|
||||
self.root_module = ship_root
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["target_updated", "calculate_hohmann_transfer"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["maneuver_calculated"]
|
||||
|
||||
# INPUT SOCKET: Connected to the NavSelectionShard's "target_selected" signal.
|
||||
func on_target_updated(new_target: OrbitalBody2D):
|
||||
func target_updated(new_target: OrbitalBody2D):
|
||||
print("MANEUVER PLANNER: Target recieved %s." % new_target)
|
||||
target_body = new_target
|
||||
calculate_hohmann_transfer()
|
||||
|
||||
# In a UI, this would enable the "Calculate" button.
|
||||
|
||||
func set_boost_factor(value: float):
|
||||
boost_factor = value
|
||||
|
||||
# UI ACTION: A panel button would call this function.
|
||||
func calculate_hohmann_transfer():
|
||||
if not is_instance_valid(root_module) or not is_instance_valid(target_body):
|
||||
@ -36,11 +39,10 @@ func calculate_hohmann_transfer():
|
||||
var star = GameManager.current_star_system.get_star()
|
||||
if not is_instance_valid(star): return
|
||||
|
||||
var ship = root_module
|
||||
var mu = OrbitalMechanics.G * star.mass
|
||||
var r1 = ship.global_position.distance_to(star.global_position)
|
||||
var r1 = root_module.global_position.distance_to(star.global_position)
|
||||
var r2 = target_body.global_position.distance_to(star.global_position)
|
||||
var a_transfer = (r1 + r2) / 2.0
|
||||
var a_transfer = (r1 + r2) / 2.0 * boost_factor
|
||||
|
||||
var v_source_orbit = sqrt(mu / r1)
|
||||
var v_target_orbit = sqrt(mu / r2)
|
||||
@ -57,7 +59,7 @@ func calculate_hohmann_transfer():
|
||||
var travel_angle = ang_vel_target * time_of_flight
|
||||
var required_phase_angle = PI - travel_angle
|
||||
|
||||
var vec_to_ship = (ship.global_position - star.global_position).normalized()
|
||||
var vec_to_ship = (root_module.global_position - star.global_position).normalized()
|
||||
var vec_to_target = (target_body.global_position - star.global_position).normalized()
|
||||
var current_phase_angle = vec_to_ship.angle_to(vec_to_target)
|
||||
|
||||
@ -67,27 +69,82 @@ func calculate_hohmann_transfer():
|
||||
if relative_ang_vel == 0: return # Avoid division by zero
|
||||
var wait_time = abs(angle_to_wait / relative_ang_vel)
|
||||
|
||||
var main_engine_thrust = 0.0 # TODO: Need a way to get this from the Helm shard
|
||||
# TODO: Need a way to get this from a shared calibration databank shard
|
||||
var main_engine_thrust = 0.0
|
||||
for thruster in root_module.get_components():
|
||||
if thruster is Thruster and thruster.main_thruster:
|
||||
main_engine_thrust += thruster.max_thrust
|
||||
|
||||
if main_engine_thrust == 0: return
|
||||
|
||||
var acceleration = main_engine_thrust / ship.mass
|
||||
var burn_duration1 = delta_v1 / acceleration
|
||||
var burn_duration2 = delta_v2 / acceleration
|
||||
var acceleration = main_engine_thrust / root_module.mass
|
||||
|
||||
var plan = []
|
||||
var burn1 = ImpulsiveBurn.new()
|
||||
burn1.delta_v_magnitude = delta_v1
|
||||
# --- Use the absolute value of delta_v for burn duration ---
|
||||
var burn_duration1 = abs(delta_v1) / acceleration
|
||||
var burn_duration2 = abs(delta_v2) / acceleration
|
||||
|
||||
# --- NEW: Predict the ship's state at the time of the burn ---
|
||||
var predicted_state = _predict_state_after_coast(root_module, star, wait_time)
|
||||
var predicted_velocity_vec = predicted_state["velocity"]
|
||||
|
||||
var plan: Array[DataTypes.ImpulsiveBurnPlan] = []
|
||||
var burn1 = DataTypes.ImpulsiveBurnPlan.new()
|
||||
burn1.delta_v_magnitude = abs(delta_v1)
|
||||
burn1.wait_time = wait_time
|
||||
burn1.burn_duration = burn_duration1
|
||||
burn1.desired_rotation_rad = ship.linear_velocity.angle()
|
||||
|
||||
# --- Determine rotation based on the sign of delta_v ---
|
||||
# Prograde (speeding up) or retrograde (slowing down)
|
||||
var prograde_direction = predicted_velocity_vec.angle()
|
||||
burn1.desired_rotation_rad = prograde_direction if delta_v1 >= 0 else prograde_direction + PI
|
||||
plan.append(burn1)
|
||||
|
||||
var burn2 = ImpulsiveBurn.new()
|
||||
# ... (burn 2 setup would go here)
|
||||
var burn2 = DataTypes.ImpulsiveBurnPlan.new()
|
||||
burn2.delta_v_magnitude = delta_v2
|
||||
burn2.wait_time = time_of_flight - burn_duration1
|
||||
burn2.burn_duration = burn_duration2
|
||||
|
||||
# --- Determine rotation for the second burn ---
|
||||
var target_prograde_direction = (target_body.global_position - star.global_position).orthogonal().angle()
|
||||
burn2.desired_rotation_rad = target_prograde_direction if delta_v2 >= 0 else target_prograde_direction + PI
|
||||
plan.append(burn2)
|
||||
|
||||
print("Hohmann Plan:")
|
||||
print(" - Wait: %d s" % wait_time)
|
||||
print(" - Burn 1: %.1f m/s (%.1f s)" % [delta_v1, burn_duration1])
|
||||
print(" - Flight time: %d s" % time_of_flight)
|
||||
print(" - Burn 2: %.1f m/s (%.1f s)" % [delta_v2, burn_duration2])
|
||||
print("MANEUVER PLANNER: Hohmann transfer calculated. Emitting plan.")
|
||||
emit_signal("maneuver_calculated", plan)
|
||||
|
||||
maneuver_calculated.emit(plan)
|
||||
|
||||
# Simulates the ship's 2-body orbit around the star to predict its future state.
|
||||
func _predict_state_after_coast(body_to_trace: OrbitalBody2D, primary: OrbitalBody2D, time: float) -> Dictionary:
|
||||
# --- Simulation Parameters ---
|
||||
var time_step = 1.0 # Simulate in 1-second increments
|
||||
var num_steps = int(ceil(time / time_step))
|
||||
|
||||
# --- Initial State (relative to the primary) ---
|
||||
var ghost_relative_pos = body_to_trace.global_position - primary.global_position
|
||||
var ghost_relative_vel = body_to_trace.linear_velocity - primary.linear_velocity
|
||||
|
||||
var mu = OrbitalMechanics.G * primary.mass
|
||||
|
||||
for i in range(num_steps):
|
||||
# --- Physics Calculation ---
|
||||
var distance_sq = ghost_relative_pos.length_squared()
|
||||
if distance_sq < 1.0: break
|
||||
|
||||
var direction = -ghost_relative_pos.normalized()
|
||||
var force_magnitude = mu / distance_sq # Simplified F = mu*m/r^2 and a=F/m
|
||||
var acceleration = direction * force_magnitude
|
||||
|
||||
# --- Integration (Euler method) ---
|
||||
ghost_relative_vel += acceleration * time_step
|
||||
ghost_relative_pos += ghost_relative_vel * time_step
|
||||
|
||||
# --- Return the final state, converted back to global space ---
|
||||
return {
|
||||
"position": ghost_relative_pos + primary.global_position,
|
||||
"velocity": ghost_relative_vel + primary.linear_velocity
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://6jj1jd14cdlt"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://bghu5lhcbcfmh" path="res://scenes/ship/computer/shards/nav_hohman_planner.gd" id="1_attn3"]
|
||||
[ext_resource type="Script" path="res://scenes/ship/computer/shards/nav_hohman_planner.gd" id="1_attn3"]
|
||||
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_nleqa"]
|
||||
|
||||
[resource]
|
||||
|
||||
79
scenes/ship/computer/shards/nav_intercept_solver.gd
Normal file
79
scenes/ship/computer/shards/nav_intercept_solver.gd
Normal file
@ -0,0 +1,79 @@
|
||||
# space_simulation/scenes/ship/computer/shards/nav_intercept_solver.gd
|
||||
extends Databank
|
||||
class_name InterceptSolverShard
|
||||
|
||||
signal solution_found(plan: Array[DataTypes.ImpulsiveBurnPlan])
|
||||
signal solution_impossible
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["project_n_body_paths"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["solution_found", "solution_impossible"]
|
||||
|
||||
# INPUT SOCKET: Planners will call this with a projected path.
|
||||
func solve_rendezvous_plan(
|
||||
target_path: Array[DataTypes.PathPoint],
|
||||
maneuver_type: String # e.g., "brachistochrone" or "hohmann"
|
||||
):
|
||||
if not is_instance_valid(root_module) or target_path.is_empty():
|
||||
emit_signal("solution_impossible")
|
||||
return
|
||||
|
||||
var rendezvous_point = find_earliest_rendezvous(target_path)
|
||||
|
||||
if not rendezvous_point:
|
||||
emit_signal("solution_impossible")
|
||||
return
|
||||
|
||||
# Once we have the target point (time, pos, vel), we can generate
|
||||
# the specific burn plan based on the requested type.
|
||||
var plan: Array[DataTypes.ImpulsiveBurnPlan]
|
||||
match maneuver_type:
|
||||
"brachistochrone":
|
||||
plan = _generate_brachistochrone_plan(rendezvous_point)
|
||||
# "hohmann" would be more complex, as it has constraints
|
||||
_:
|
||||
print("Unknown maneuver type for solver.")
|
||||
emit_signal("solution_impossible")
|
||||
return
|
||||
|
||||
emit_signal("solution_found", plan)
|
||||
|
||||
|
||||
# This is the core solver logic.
|
||||
func find_earliest_rendezvous(target_path: Array[DataTypes.PathPoint]) -> DataTypes.PathPoint:
|
||||
# For each point in the target's future path...
|
||||
for point in target_path:
|
||||
# 1. Calculate the required change in position (displacement).
|
||||
var delta_p = point.position - root_module.global_position
|
||||
|
||||
# 2. Calculate the required change in velocity.
|
||||
var delta_v = point.velocity - root_module.linear_velocity
|
||||
|
||||
# 3. Using kinematics (d = v_initial*t + 0.5at^2), find the constant
|
||||
# acceleration 'a' required to satisfy both delta_p and delta_v over
|
||||
# the time 'point.time'.
|
||||
# a = 2 * (delta_p - root_module.linear_velocity * point.time) / (point.time * point.time)
|
||||
var required_acceleration_vector = 2.0 * (delta_p - root_module.linear_velocity * point.time) / (point.time * point.time)
|
||||
|
||||
# 4. Check if the magnitude of this required acceleration is something our ship can actually do.
|
||||
var max_accel = root_module.main_engine_thrust / root_module.mass # Assumes we need a get_main_engine_thrust() helper
|
||||
|
||||
if required_acceleration_vector.length() <= max_accel:
|
||||
# This is the first point in time we can reach. This is our solution.
|
||||
return point
|
||||
|
||||
# If we get through the whole path and can't reach any of them, it's impossible.
|
||||
return null
|
||||
|
||||
|
||||
func _generate_brachistochrone_plan(rendezvous_point: DataTypes.PathPoint) -> Array[DataTypes.ImpulsiveBurnPlan]:
|
||||
# This function would now use the data from the solved rendezvous_point
|
||||
# to create the two-burn Brachistochrone plan, similar to before.
|
||||
# The key difference is that all calculations are now based on a confirmed possible intercept.
|
||||
var plan: Array[DataTypes.ImpulsiveBurnPlan] = []
|
||||
# ... logic to build the plan ...
|
||||
return plan
|
||||
1
scenes/ship/computer/shards/nav_intercept_solver.gd.uid
Normal file
1
scenes/ship/computer/shards/nav_intercept_solver.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dsbn7ushwqrko
|
||||
87
scenes/ship/computer/shards/nav_projection_shard.gd
Normal file
87
scenes/ship/computer/shards/nav_projection_shard.gd
Normal file
@ -0,0 +1,87 @@
|
||||
# space_simulation/scenes/ship/computer/shards/nav_path_projection.gd
|
||||
extends Databank
|
||||
class_name PathProjectionShard
|
||||
|
||||
## Emitted after a requested path has been calculated.
|
||||
signal projected_system_bus(paths: Array[DataTypes.PathPoint])
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["project_n_body_paths"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["projected_system_bus"]
|
||||
|
||||
## Projects the future paths of an array of bodies interacting with each other.
|
||||
## Returns a dictionary mapping each body to its calculated PackedVector2Array path.
|
||||
func project_n_body_paths(
|
||||
bodies_to_trace: Array[OrbitalBody2D],
|
||||
num_steps: int,
|
||||
time_step: float
|
||||
):
|
||||
|
||||
# --- Step 1: Create a "ghost state" for each body ---
|
||||
# A ghost state is just a simple dictionary holding the physics properties.
|
||||
var ghost_states = []
|
||||
for body in bodies_to_trace:
|
||||
ghost_states.append({
|
||||
"body_ref": body,
|
||||
"mass": body.mass,
|
||||
"position": body.global_position,
|
||||
"velocity": body.linear_velocity # Velocity is always in the same space
|
||||
})
|
||||
|
||||
# --- Step 2: Prepare the results dictionary ---
|
||||
var paths: Dictionary = {}
|
||||
for state in ghost_states:
|
||||
paths[state.body_ref] = []
|
||||
|
||||
|
||||
# --- Step 3: Run the ghost simulation ---
|
||||
for i in range(num_steps):
|
||||
# Create a list to hold the forces for this time step
|
||||
var forces_for_step = {}
|
||||
for state in ghost_states:
|
||||
forces_for_step[state.body_ref] = Vector2.ZERO
|
||||
|
||||
# a) Calculate all gravitational forces between the ghosts
|
||||
for j in range(ghost_states.size()):
|
||||
var state_a = ghost_states[j]
|
||||
for k in range(j + 1, ghost_states.size()):
|
||||
var state_b = ghost_states[k]
|
||||
|
||||
# Calculate force between the two ghost states2:
|
||||
var distance_sq = state_a.position.distance_squared_to(state_b.position)
|
||||
if distance_sq < 1.0: return Vector2.ZERO
|
||||
|
||||
var force_magnitude = (OrbitalMechanics.G * state_a.mass * state_b.mass) / distance_sq
|
||||
var direction = state_a.position.direction_to(state_b.position)
|
||||
var force_vector = direction * force_magnitude
|
||||
|
||||
# Store the forces to be applied
|
||||
forces_for_step[state_a.body_ref] += force_vector
|
||||
forces_for_step[state_b.body_ref] -= force_vector
|
||||
|
||||
# b) Integrate forces for each ghost to find its next position
|
||||
for state in ghost_states:
|
||||
if state.mass > 0:
|
||||
var acceleration = forces_for_step[state.body_ref] / state.mass
|
||||
state.velocity += acceleration * time_step
|
||||
state.position += state.velocity * time_step
|
||||
|
||||
|
||||
|
||||
# c) Record the new position in the path
|
||||
paths[state.body_ref].append(DataTypes.PathPoint.new(i * time_step, state.position, state.velocity))
|
||||
|
||||
# --- Step 4: Prepare the results dictionary ---
|
||||
var projections: Array[DataTypes.PathProjection] = []
|
||||
for state in ghost_states:
|
||||
var projection: DataTypes.PathProjection = DataTypes.PathProjection.new(state.body_ref)
|
||||
|
||||
projection.points = paths[state.body_ref]
|
||||
|
||||
projections.append(projection)
|
||||
|
||||
projected_system_bus.emit(paths)
|
||||
1
scenes/ship/computer/shards/nav_projection_shard.gd.uid
Normal file
1
scenes/ship/computer/shards/nav_projection_shard.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://0f6v6iu3o5qo
|
||||
9
scenes/ship/computer/shards/nav_projection_shard.tres
Normal file
9
scenes/ship/computer/shards/nav_projection_shard.tres
Normal file
@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://d4e5f6g7h8jaj"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ship/computer/shards/nav_path_projection.gd" id="1_proj"]
|
||||
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="2_data"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_data")
|
||||
logic_script = ExtResource("1_proj")
|
||||
metadata/_custom_type_script = "uid://osk1l75vlikn"
|
||||
@ -1,5 +1,5 @@
|
||||
# scenes/ship/computer/shards/nav_selection_databank.gd
|
||||
extends Node
|
||||
extends Databank
|
||||
class_name NavSelectionShard
|
||||
|
||||
## Emitted whenever a new navigation target is selected from the map.
|
||||
@ -7,8 +7,16 @@ signal target_selected(body: OrbitalBody2D)
|
||||
|
||||
var selected_body: OrbitalBody2D = null
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["body_selected"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["target_selected"]
|
||||
|
||||
# INPUT SOCKET: This function is connected to the SensorPanel's "body_selected" signal.
|
||||
func on_body_selected(body: OrbitalBody2D):
|
||||
func body_selected(body: OrbitalBody2D):
|
||||
if is_instance_valid(body) and body != selected_body:
|
||||
print("NAV SELECTION: New target acquired - ", body.name)
|
||||
selected_body = body
|
||||
|
||||
@ -1,20 +1,34 @@
|
||||
extends Node
|
||||
|
||||
extends Databank
|
||||
class_name SensorSystemShard
|
||||
|
||||
## This shard emits all trackable bodies as a "sensor feed" every frame.
|
||||
signal sensor_feed_updated(bodies: Array[OrbitalBody2D])
|
||||
|
||||
func _physics_process(delta):
|
||||
# In a more advanced game, this shard might have its own power requirements
|
||||
# or could be affected by radiation, etc. For now, it just gets all bodies.
|
||||
@export_group("Projection Settings")
|
||||
@export var projection_steps: int = 500
|
||||
@export var time_per_step: float = 60.0 # Project at 1-minute intervals
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return []
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["sensor_feed_updated"]
|
||||
|
||||
# We use _process instead of _physics_process to avoid slowing down the physics thread.
|
||||
# This calculation can happen on a separate frame if needed.
|
||||
func _process(_delta: float):
|
||||
var star_system = GameManager.current_star_system
|
||||
|
||||
var tracked_bodies: Array[OrbitalBody2D] = [star_system.get_star()]
|
||||
|
||||
for planetary_system in star_system.get_planetary_systems():
|
||||
tracked_bodies.append_array( planetary_system.get_internal_attractors())
|
||||
|
||||
if not is_instance_valid(star_system):
|
||||
return
|
||||
|
||||
# Gather all bodies that need to be included in the simulation.
|
||||
var tracked_bodies: Array[OrbitalBody2D] = []
|
||||
tracked_bodies.append(star_system.get_star())
|
||||
tracked_bodies.append_array(star_system.get_planetary_systems())
|
||||
tracked_bodies.append_array(star_system.get_orbital_bodies())
|
||||
|
||||
if tracked_bodies.is_empty():
|
||||
return
|
||||
|
||||
sensor_feed_updated.emit(tracked_bodies)
|
||||
|
||||
30
scenes/ship/computer/wiring/socket.gd
Normal file
30
scenes/ship/computer/wiring/socket.gd
Normal file
@ -0,0 +1,30 @@
|
||||
# Socket.gd
|
||||
extends Control
|
||||
class_name Socket
|
||||
|
||||
enum SocketType { INPUT, OUTPUT }
|
||||
|
||||
enum SocketDataTypes {
|
||||
FLOAT, INT, STRING, EXEC
|
||||
}
|
||||
|
||||
@onready var icon: ColorRect = $Icon
|
||||
@onready var label: Label = $Label
|
||||
|
||||
var socket_name: String
|
||||
var socket_type: SocketType
|
||||
|
||||
# Called by the parent component block to set up the socket.
|
||||
func initialize(s_name: String, s_type: SocketType):
|
||||
socket_name = s_name
|
||||
socket_type = s_type
|
||||
|
||||
icon.tooltip_text = socket_name
|
||||
|
||||
if socket_type == SocketType.INPUT:
|
||||
icon.color = Color.DODGER_BLUE
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_LEFT
|
||||
else: # OUTPUT
|
||||
icon.color = Color.ORANGE
|
||||
# For output sockets, we can right-align the text to make it look neat.
|
||||
label.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
|
||||
1
scenes/ship/computer/wiring/socket.gd.uid
Normal file
1
scenes/ship/computer/wiring/socket.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://d3d6tgy757f43
|
||||
17
scenes/ship/computer/wiring/socket.tscn
Normal file
17
scenes/ship/computer/wiring/socket.tscn
Normal file
@ -0,0 +1,17 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://8glctqud5ioq"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d3d6tgy757f43" path="res://scenes/ship/computer/wiring/socket.gd" id="1_ubhg7"]
|
||||
|
||||
[node name="Socket" type="HBoxContainer"]
|
||||
offset_right = 5.0
|
||||
offset_bottom = 23.0
|
||||
script = ExtResource("1_ubhg7")
|
||||
|
||||
[node name="Icon" type="ColorRect" parent="."]
|
||||
custom_minimum_size = Vector2(48, 48)
|
||||
layout_mode = 2
|
||||
mouse_filter = 1
|
||||
color = Color(0.2, 0.235294, 0.796078, 1)
|
||||
|
||||
[node name="Label" type="Label" parent="."]
|
||||
layout_mode = 2
|
||||
19
scenes/ship/computer/wiring/wire_connection.gd
Normal file
19
scenes/ship/computer/wiring/wire_connection.gd
Normal file
@ -0,0 +1,19 @@
|
||||
# space_simulation/scenes/ship/computer/wiring/wire_connection.gd
|
||||
@tool
|
||||
class_name WireConnection
|
||||
extends Resource
|
||||
|
||||
## The resource of the component where the wire starts (e.g., a Databank).
|
||||
@export var output_component_resource: Resource
|
||||
|
||||
## The name of the output signal/socket.
|
||||
@export var output_socket_name: String
|
||||
|
||||
## The resource of the component where the wire ends (e.g., a ControlPanel).
|
||||
@export var input_component_resource: Resource
|
||||
|
||||
## The name of the input function/socket.
|
||||
@export var input_socket_name: String
|
||||
|
||||
## The array of points (in local coordinates of the wiring panel) that define the wire's path.
|
||||
@export var path_points: PackedVector2Array = []
|
||||
1
scenes/ship/computer/wiring/wire_connection.gd.uid
Normal file
1
scenes/ship/computer/wiring/wire_connection.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dmmb3tmpqr4ur
|
||||
7
scenes/ship/computer/wiring/wiring_schematic.gd
Normal file
7
scenes/ship/computer/wiring/wiring_schematic.gd
Normal file
@ -0,0 +1,7 @@
|
||||
# space_simulation/scenes/ship/computer/wiring/wiring_schematic.gd
|
||||
@tool
|
||||
class_name WiringSchematic
|
||||
extends Resource
|
||||
|
||||
## An array of all the individual wire connections in this schematic.
|
||||
@export var connections: Array[WireConnection] = []
|
||||
1
scenes/ship/computer/wiring/wiring_schematic.gd.uid
Normal file
1
scenes/ship/computer/wiring/wiring_schematic.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bf6py4usdvjmk
|
||||
@ -1,18 +0,0 @@
|
||||
class_name ShipSignalBus
|
||||
extends Node
|
||||
|
||||
# --- Navigation & Maneuvering Events ---
|
||||
# Emitted when a maneuver plan is calculated.
|
||||
signal maneuver_planned(plan)
|
||||
|
||||
# Emitted to command the start of a timed rotation.
|
||||
signal timed_rotation_commanded(target_rotation_rad, time_window)
|
||||
|
||||
# Emitted to command the start of a timed main engine burn.
|
||||
signal timed_burn_commanded(duration)
|
||||
|
||||
# --- Thruster & Ship Status Events ---
|
||||
# Emitted when the main engine starts or stops firing.
|
||||
signal main_engine_state_changed(is_firing: bool, total_thrust: float)
|
||||
# Emitted when RCS thrusters are fired.
|
||||
signal rcs_state_changed(is_firing: bool, torque: float)
|
||||
@ -1 +0,0 @@
|
||||
uid://w1546qtaupd2
|
||||
@ -1,85 +0,0 @@
|
||||
# Spaceship.gd
|
||||
class_name Spaceship
|
||||
extends OrbitalBody2D
|
||||
|
||||
@export var ship_name: String = "Stardust Drifter"
|
||||
@export var dry_mass: float = 1000.0 # Mass of the ship without fuel/cargo (in kg)
|
||||
@export var hull_integrity: float = 100.0
|
||||
|
||||
@onready var camera: Camera2D = $Camera2D
|
||||
|
||||
@export_category("Camera")
|
||||
@export var zoom_speed: float = 1.0
|
||||
var current_time_scale: float = Engine.time_scale
|
||||
|
||||
# --- Node References to Modular Systems ---
|
||||
@onready var signal_bus: ShipSignalBus = $SignalBus
|
||||
@onready var thruster_controller: ThrusterController = $ThrusterController
|
||||
@onready var fuel_system = $FuelSystem
|
||||
@onready var life_support = $LifeSupport
|
||||
@onready var navigation_computer = $NavigationComputer
|
||||
# @onready var weapon_system = $WeaponSystem
|
||||
# @onready var power_grid = $PowerGrid
|
||||
|
||||
func _ready() -> void:
|
||||
super()
|
||||
GameManager.register_ship(self)
|
||||
|
||||
# Give the navigation computer a reference to this ship
|
||||
if navigation_computer:
|
||||
navigation_computer.ship = self
|
||||
|
||||
camera.make_current()
|
||||
|
||||
# This function will now handle all non-UI input for the player-controlled ship.
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
# --- Map Toggling ---
|
||||
if event.is_action_pressed("ui_map_mode"):
|
||||
# Instead of showing/hiding a node directly, we broadcast our intent.
|
||||
# The NavigationComputer will be listening for this global signal.
|
||||
SignalBus.emit_signal("map_mode_toggled")
|
||||
|
||||
# --- Time Scale Controls ---
|
||||
if event.is_action_pressed("time_increase"):
|
||||
var new_value = min(current_time_scale * 1.2, 1000)
|
||||
current_time_scale = clamp(new_value, 0.5, 1000)
|
||||
Engine.time_scale = current_time_scale
|
||||
|
||||
elif event.is_action_pressed("time_decrease"):
|
||||
var new_value = max(current_time_scale * 0.833, 0.1)
|
||||
current_time_scale = clamp(new_value, 0.5, 1000)
|
||||
Engine.time_scale = current_time_scale
|
||||
|
||||
elif event.is_action_pressed("time_reset"):
|
||||
Engine.time_scale = 1.0
|
||||
|
||||
# --- Public API for Ship Management ---
|
||||
# Call this to take damage. Damage can have a position for breach effects.
|
||||
func take_damage(amount: float, damage_position: Vector2):
|
||||
hull_integrity -= amount
|
||||
print("%s hull integrity at %.1f%%" % [ship_name, hull_integrity])
|
||||
|
||||
if hull_integrity <= 0:
|
||||
destroy_ship()
|
||||
else:
|
||||
# Check if the hit caused a hull breach
|
||||
life_support.check_for_breach(damage_position)
|
||||
|
||||
func destroy_ship():
|
||||
print("%s has been destroyed!" % ship_name)
|
||||
queue_free()
|
||||
|
||||
# --- Signal Handlers ---
|
||||
#func _on_fuel_mass_changed():
|
||||
# Update the ship's total mass when fuel is consumed or added
|
||||
#update_total_mass()
|
||||
|
||||
func _on_hull_breach(breach_position: Vector2, force_vector: Vector2):
|
||||
pass
|
||||
# A hull breach applies a continuous force at a specific point
|
||||
# For simplicity, we can apply it as a central force and torque here
|
||||
#var force = force_vector * 100 # Scale the force
|
||||
#
|
||||
## Calculate torque: Torque = r x F (cross product of position vector and force)
|
||||
#var position_relative_to_center = breach_position - self.global_position
|
||||
#var torque = position_relative_to_center.cross(force)
|
||||
@ -1 +0,0 @@
|
||||
uid://dyqbk4lcx3mhq
|
||||
@ -1,63 +0,0 @@
|
||||
[gd_scene load_steps=9 format=3 uid="uid://dlck1lyrn1xvp"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://dyqbk4lcx3mhq" path="res://scenes/ship/spaceship.gd" id="1_ae4p7"]
|
||||
[ext_resource type="Script" uid="uid://c0bx113ifxyh8" path="res://scenes/ship/systems/thruster_controller.gd" id="2_xs8u7"]
|
||||
[ext_resource type="PackedScene" uid="uid://c77wxeb7gpplw" path="res://scenes/modules/test_module.tscn" id="2_y58gg"]
|
||||
[ext_resource type="Script" uid="uid://dx3uerblskj5r" path="res://scenes/ship/systems/fuel_system.gd" id="3_xs8u7"]
|
||||
[ext_resource type="Script" uid="uid://buyp6t5cppitw" path="res://scenes/ship/systems/life_support.gd" id="4_v0rat"]
|
||||
[ext_resource type="PackedScene" uid="uid://cxpjm8tp3l1j7" path="res://scenes/ship/systems/navigation_computer.tscn" id="5_6nyhl"]
|
||||
[ext_resource type="Script" uid="uid://w1546qtaupd2" path="res://scenes/ship/ship_signal_bus.gd" id="7_yl4tl"]
|
||||
[ext_resource type="PackedScene" uid="uid://c0bb77rmyatr0" path="res://scenes/ship/components/hardware/thruster.tscn" id="8_oedjh"]
|
||||
|
||||
[node name="Spaceship" type="Node2D"]
|
||||
script = ExtResource("1_ae4p7")
|
||||
base_mass = 2000.0
|
||||
inertia = 500.0
|
||||
metadata/_custom_type_script = "uid://0isnsk356que"
|
||||
|
||||
[node name="Module" parent="." instance=ExtResource("2_y58gg")]
|
||||
|
||||
[node name="FuelSystem" type="Node" parent="."]
|
||||
script = ExtResource("3_xs8u7")
|
||||
|
||||
[node name="LifeSupport" type="Node" parent="."]
|
||||
script = ExtResource("4_v0rat")
|
||||
|
||||
[node name="NavigationComputer" parent="." instance=ExtResource("5_6nyhl")]
|
||||
|
||||
[node name="Camera2D" type="Camera2D" parent="."]
|
||||
|
||||
[node name="ThrusterController" type="Node2D" parent="."]
|
||||
script = ExtResource("2_xs8u7")
|
||||
|
||||
[node name="RCS1" parent="." instance=ExtResource("8_oedjh")]
|
||||
position = Vector2(-125, 200)
|
||||
rotation = 1.5708
|
||||
main_thruster = false
|
||||
inertia = 0.0
|
||||
|
||||
[node name="RCS2" parent="." instance=ExtResource("8_oedjh")]
|
||||
position = Vector2(-125, -200)
|
||||
rotation = 1.5708
|
||||
main_thruster = false
|
||||
inertia = 0.0
|
||||
|
||||
[node name="RCS3" parent="." instance=ExtResource("8_oedjh")]
|
||||
position = Vector2(125, -200)
|
||||
rotation = -1.5708
|
||||
main_thruster = false
|
||||
inertia = 0.0
|
||||
|
||||
[node name="RCS4" parent="." instance=ExtResource("8_oedjh")]
|
||||
position = Vector2(125, 200)
|
||||
rotation = -1.5708
|
||||
main_thruster = false
|
||||
inertia = 0.0
|
||||
|
||||
[node name="MainEngine" parent="." instance=ExtResource("8_oedjh")]
|
||||
position = Vector2(-1, 226)
|
||||
max_thrust = 100.0
|
||||
inertia = 0.0
|
||||
|
||||
[node name="SignalBus" type="Node" parent="."]
|
||||
script = ExtResource("7_yl4tl")
|
||||
@ -1,184 +0,0 @@
|
||||
# space_simulation/scripts/map_controller.gd
|
||||
class_name MapController
|
||||
extends Control
|
||||
|
||||
signal body_selected_for_planning(body: OrbitalBody2D)
|
||||
|
||||
@export var map_icon_scene: PackedScene
|
||||
|
||||
const LABEL_CULLING_PIXEL_THRESHOLD = 65.0
|
||||
const ICON_CULLING_PIXEL_THRESHOLD = 40.0
|
||||
|
||||
var map_scale: float = 0.001
|
||||
var map_offset: Vector2 = Vector2.ZERO
|
||||
var focal_body: OrbitalBody2D
|
||||
|
||||
var icon_map: Dictionary = {}
|
||||
|
||||
var followed_body: OrbitalBody2D = null
|
||||
var map_tween: Tween
|
||||
|
||||
# The starting point for our lerp animation.
|
||||
var follow_start_offset: Vector2
|
||||
# The progress of the follow animation (0.0 to 1.0), animated by a tween.
|
||||
var follow_progress: float = 0.0:
|
||||
set(value):
|
||||
follow_progress = value
|
||||
# We must redraw every time the progress changes.
|
||||
queue_redraw()
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().physics_frame
|
||||
var star_system = GameManager.current_star_system
|
||||
if is_instance_valid(star_system):
|
||||
focal_body = star_system.get_system_data().star
|
||||
_populate_map()
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
_update_icon_positions()
|
||||
|
||||
func _draw() -> void:
|
||||
var map_center = get_rect().size / 2.0
|
||||
|
||||
var system_data = GameManager.get_system_data()
|
||||
if system_data and system_data.belts:
|
||||
for belt in system_data.belts:
|
||||
var radius = belt.centered_radius * map_scale
|
||||
draw_circle(map_center + map_offset, radius, Color(Color.WHITE, 0.1), false)
|
||||
|
||||
for body in icon_map:
|
||||
if body is Asteroid: continue
|
||||
var icon = icon_map[body]
|
||||
if not icon.visible: continue
|
||||
|
||||
var path_points = []
|
||||
if body is CelestialBody: path_points = OrbitalMechanics._calculate_relative_orbital_path(body)
|
||||
elif body is OrbitalBody2D: path_points = OrbitalMechanics._calculate_n_body_orbital_path(body)
|
||||
else: continue
|
||||
var scaled_path_points = PackedVector2Array()
|
||||
|
||||
for point in path_points:
|
||||
# Ensure path is drawn relative to the main focal body (the star)
|
||||
var path_world_pos = point + focal_body.global_position
|
||||
var relative_pos = path_world_pos - focal_body.global_position
|
||||
var scaled_pos = (relative_pos * map_scale) + map_offset + map_center
|
||||
scaled_path_points.append(scaled_pos)
|
||||
|
||||
if scaled_path_points.size() > 1:
|
||||
draw_polyline(scaled_path_points, Color(Color.WHITE, 0.2), 1.0, true)
|
||||
|
||||
func _populate_map():
|
||||
for child in get_children():
|
||||
child.queue_free()
|
||||
icon_map.clear()
|
||||
|
||||
var all_bodies = GameManager.get_all_trackable_bodies()
|
||||
for body in all_bodies:
|
||||
if not is_instance_valid(body): continue
|
||||
var icon = map_icon_scene.instantiate() as MapIcon
|
||||
add_child(icon)
|
||||
icon.initialize(body)
|
||||
icon_map[body] = icon
|
||||
icon.selected.connect(_on_map_icon_selected)
|
||||
icon.follow_requested.connect(_on_follow_requested)
|
||||
|
||||
func _update_icon_positions():
|
||||
if not is_instance_valid(focal_body): return
|
||||
|
||||
var map_center = get_rect().size / 2.0
|
||||
|
||||
# --- MODIFIED: Continuous follow logic ---
|
||||
if is_instance_valid(followed_body):
|
||||
# Calculate the ideal offset to center the followed body.
|
||||
var relative_target_pos = followed_body.global_position - focal_body.global_position
|
||||
var target_offset = -relative_target_pos * map_scale
|
||||
|
||||
# During the initial pan, interpolate from the start to the target.
|
||||
# When follow_progress reaches 1.0, this just becomes target_offset.
|
||||
map_offset = follow_start_offset.lerp(target_offset, follow_progress)
|
||||
|
||||
# It will now use the dynamically updated map_offset.
|
||||
var icon_data_for_frame = []
|
||||
for body in icon_map:
|
||||
var icon = icon_map[body]
|
||||
|
||||
icon.visible = true
|
||||
icon.name_label.visible = true
|
||||
|
||||
var relative_pos = body.global_position - focal_body.global_position
|
||||
var final_screen_pos = (relative_pos * map_scale) + map_offset + map_center
|
||||
|
||||
icon_data_for_frame.append({
|
||||
"screen_pos": final_screen_pos,
|
||||
"body": body,
|
||||
"icon": icon
|
||||
})
|
||||
|
||||
icon.position = final_screen_pos - (icon.size / 2)
|
||||
|
||||
for i in range(icon_data_for_frame.size()):
|
||||
var data_a = icon_data_for_frame[i]
|
||||
|
||||
if not data_a.icon.visible:
|
||||
continue
|
||||
|
||||
for j in range(i + 1, icon_data_for_frame.size()):
|
||||
var data_b = icon_data_for_frame[j]
|
||||
if not data_b.icon.visible: continue
|
||||
|
||||
var distance = data_a.screen_pos.distance_to(data_b.screen_pos)
|
||||
|
||||
if distance < ICON_CULLING_PIXEL_THRESHOLD:
|
||||
if data_a.body.mass > data_b.body.mass:
|
||||
data_b.icon.visible = false
|
||||
else:
|
||||
data_a.icon.visible = false
|
||||
elif distance < LABEL_CULLING_PIXEL_THRESHOLD:
|
||||
if data_a.body.mass > data_b.body.mass:
|
||||
data_b.icon.name_label.visible = false
|
||||
else:
|
||||
data_a.icon.name_label.visible = false
|
||||
|
||||
# Request a redraw at the end of the update
|
||||
queue_redraw()
|
||||
|
||||
func _gui_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseButton:
|
||||
if event.button_index == MOUSE_BUTTON_WHEEL_UP or event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
var zoom_factor = 1.25 if event.button_index == MOUSE_BUTTON_WHEEL_UP else 1 / 1.25
|
||||
var mouse_pos = get_local_mouse_position()
|
||||
var map_center = get_rect().size / 2.0
|
||||
|
||||
var point_under_mouse_world = (mouse_pos - map_center - map_offset) / map_scale
|
||||
map_scale *= zoom_factor
|
||||
var point_under_mouse_new_screen = (point_under_mouse_world * map_scale) + map_center + map_offset
|
||||
map_offset += mouse_pos - point_under_mouse_new_screen
|
||||
|
||||
if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_LEFT:
|
||||
# --- MODIFIED: Break the lock and reset progress ---
|
||||
if is_instance_valid(followed_body):
|
||||
print("Map lock disengaged by manual pan.")
|
||||
followed_body = null
|
||||
follow_progress = 0.0 # Reset progress
|
||||
if map_tween:
|
||||
map_tween.kill()
|
||||
|
||||
map_offset += event.relative
|
||||
|
||||
func _on_map_icon_selected(body: OrbitalBody2D):
|
||||
emit_signal("body_selected_for_planning", body)
|
||||
|
||||
func _on_follow_requested(body: OrbitalBody2D):
|
||||
print("Map view locking on to: ", body.name)
|
||||
follow_progress = 0.0
|
||||
followed_body = body
|
||||
|
||||
if map_tween:
|
||||
map_tween.kill()
|
||||
|
||||
# Store the offset at the exact moment the follow begins.
|
||||
follow_start_offset = map_offset
|
||||
|
||||
# --- REVISED: We now tween the 'follow_progress' property instead of 'map_offset' ---
|
||||
map_tween = create_tween().set_trans(Tween.TRANS_CUBIC).set_ease(Tween.EASE_IN_OUT)
|
||||
map_tween.tween_property(self, "follow_progress", 1.0, 1.5)
|
||||
@ -1 +0,0 @@
|
||||
uid://b74hxlsox8ldo
|
||||
@ -1,402 +0,0 @@
|
||||
# NavigationComputer.gd
|
||||
extends Node
|
||||
|
||||
@onready var ship: Spaceship = %Spaceship
|
||||
var ship_signal_bus: ShipSignalBus
|
||||
|
||||
# --- Node References ---
|
||||
@onready var navigation_ui: CanvasLayer = %NavigationUI
|
||||
@onready var map_controller: MapController = %MapController
|
||||
@onready var target_label: Label = %TargetLabel
|
||||
@onready var info_label: Label = %InfoLabel
|
||||
@onready var ship_status_label: Label = %ShipStatusLabel
|
||||
@onready var torque_indicator: ColorRect = %TorqueIndicator
|
||||
@onready var sub_viewport: SubViewport = %SubViewport
|
||||
@onready var ship_view_camera: Camera2D = %ShipViewCamera # Add this reference
|
||||
|
||||
# Buttons for different maneuvers
|
||||
@onready var plan_hohmann_button: Button = %PlanHohmannButton
|
||||
@onready var plan_fast_button: Button = %PlanFastButton
|
||||
@onready var plan_torchship_button: Button = %PlanTorchshipButton
|
||||
@onready var plan_gravity_assist_button: Button = %PlanGravityAssistButton
|
||||
@onready var execute_plan_button: Button = %ExecutePlanButton
|
||||
|
||||
# How many seconds before a burn we should start orienting the ship.
|
||||
const PRE_BURN_ORIENTATION_SECONDS = 30.0 # Give a larger window for the new logic
|
||||
const ROTATION_SAFETY_BUFFER = 10.0 # Seconds to ensure rotation finishes before burn
|
||||
|
||||
# A flag to make sure we only send the signal once per maneuver
|
||||
var rotation_plan_sent = false
|
||||
|
||||
# --- State Management ---
|
||||
enum State { IDLE, PLANNING, WAITING, EXECUTING }
|
||||
var current_state: State = State.IDLE
|
||||
|
||||
# --- Navigation Data ---
|
||||
var source_body: CelestialBody
|
||||
var target_body: CelestialBody
|
||||
|
||||
var current_plan # Can be an array of ImpulsiveBurn or a ContinuousBurnPlan
|
||||
|
||||
# --- Inner classes to hold maneuver data ---
|
||||
class ImpulsiveBurn:
|
||||
var delta_v_magnitude: float
|
||||
var wait_time: float = 0.0
|
||||
var burn_duration: float
|
||||
var desired_rotation_rad: float # The world rotation the ship needs to be in
|
||||
|
||||
class ContinuousBurnPlan:
|
||||
var total_travel_time: float
|
||||
var acceleration_time: float
|
||||
var initial_burn_direction: Vector2 # The world-space direction vector for the burn
|
||||
|
||||
func _ready() -> void:
|
||||
# tell our SubViewport to render the same world as main viewport.
|
||||
if is_instance_valid(sub_viewport):
|
||||
print("NAV COMP: Sub viewport found")
|
||||
sub_viewport.world_2d = get_viewport().world_2d
|
||||
else:
|
||||
print("NAV COMP: Sub viewport not found")
|
||||
|
||||
# Connect to the global signal from the SignalBus
|
||||
SignalBus.map_mode_toggled.connect(on_map_mode_toggled)
|
||||
|
||||
if is_instance_valid(ship):
|
||||
ship_signal_bus = ship.signal_bus
|
||||
|
||||
# Ensure the UI starts hidden
|
||||
if navigation_ui:
|
||||
navigation_ui.hide()
|
||||
|
||||
# Connect UI signals
|
||||
map_controller.body_selected_for_planning.connect(_on_target_selected)
|
||||
plan_hohmann_button.pressed.connect(_on_plan_hohmann_pressed)
|
||||
plan_fast_button.pressed.connect(_on_plan_fast_pressed)
|
||||
plan_torchship_button.pressed.connect(_on_plan_torchship_pressed)
|
||||
plan_gravity_assist_button.pressed.connect(_on_plan_gravity_assist_pressed)
|
||||
execute_plan_button.pressed.connect(_on_execute_plan_pressed)
|
||||
|
||||
ship_status_label.text = ""
|
||||
|
||||
update_ui()
|
||||
|
||||
# Add a background to the UIm
|
||||
_setup_background()
|
||||
|
||||
|
||||
func _setup_background():
|
||||
# --- FIX #1: Add a black, opaque background ---
|
||||
var bg = ColorRect.new()
|
||||
bg.color = Color.BLACK
|
||||
# Add it as the very first child so it's behind everything else.
|
||||
navigation_ui.add_child(bg)
|
||||
navigation_ui.move_child(bg, 0)
|
||||
# Make the background cover the entire screen.
|
||||
bg.anchor_right = 1.0
|
||||
bg.anchor_bottom = 1.0
|
||||
|
||||
# This function is called whenever any node in the game emits the "map_mode_toggled" signal.
|
||||
func on_map_mode_toggled():
|
||||
if navigation_ui:
|
||||
# Toggle the visibility of the UI screen
|
||||
navigation_ui.visible = not navigation_ui.visible
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# This ensures the viewport camera always mirrors the ship's main camera.
|
||||
if is_instance_valid(ship) and is_instance_valid(ship.camera):
|
||||
if is_instance_valid(ship_view_camera):
|
||||
ship_view_camera.global_transform = ship.camera.global_transform
|
||||
ship_view_camera.zoom = ship.camera.zoom
|
||||
|
||||
_update_ship_status_label()
|
||||
|
||||
if current_state == State.PLANNING and current_plan:
|
||||
if current_plan is Array and not current_plan.is_empty():
|
||||
var first_burn = current_plan[0]
|
||||
# The plan is not locked in, but we can see the window approaching.
|
||||
first_burn.wait_time -= delta
|
||||
var time_str = _format_seconds_to_mmss(first_burn.wait_time)
|
||||
info_label.text = "Optimal window in: %s.\nPress Execute to lock in plan." % time_str
|
||||
if first_burn.wait_time < 0:
|
||||
# If the window is missed during planning, mark the plan as stale.
|
||||
info_label.text = "Transfer window missed. Please plan a new maneuver."
|
||||
current_plan = null
|
||||
update_ui()
|
||||
|
||||
if current_state == State.WAITING and current_plan:
|
||||
if current_plan is Array and not current_plan.is_empty():
|
||||
var next_burn: ImpulsiveBurn = current_plan[0]
|
||||
next_burn.wait_time -= delta
|
||||
var time_str = _format_seconds_to_mmss(next_burn.wait_time)
|
||||
info_label.text = "Time to burn: %s" % time_str
|
||||
|
||||
# --- NEW: Emit the rotation plan ---
|
||||
# When we are inside the orientation window, and haven't sent the plan yet...
|
||||
if not rotation_plan_sent:
|
||||
# The time allowed for rotation is the time we have left, minus a safety buffer.
|
||||
var time_for_rotation = next_burn.wait_time - ROTATION_SAFETY_BUFFER
|
||||
|
||||
# Tell the thruster controller to handle it.
|
||||
ship.signal_bus.emit_signal("timed_rotation_commanded", next_burn.desired_rotation_rad, time_for_rotation)
|
||||
|
||||
rotation_plan_sent = true # Mark the plan as sent
|
||||
|
||||
if next_burn.wait_time <= 0:
|
||||
_execute_maneuver()
|
||||
|
||||
# The EXECUTING state would be handled by a dedicated autopilot script/node
|
||||
|
||||
func update_ui():
|
||||
execute_plan_button.disabled = (current_plan == null or current_state != State.PLANNING)
|
||||
|
||||
if current_state == State.PLANNING:
|
||||
# Show available plans based on engine type
|
||||
|
||||
# TODO change UI to show recommendations based on thruster type
|
||||
#if installed_engine is ChemicalThruster:
|
||||
plan_hohmann_button.show()
|
||||
plan_fast_button.show()
|
||||
plan_gravity_assist_button.show()
|
||||
#elif installed_engine is IonDrive:
|
||||
plan_torchship_button.show()
|
||||
|
||||
# --- Signal Handlers ---
|
||||
|
||||
func _on_target_selected(body: CelestialBody):
|
||||
if current_state == State.IDLE or current_state == State.PLANNING:
|
||||
target_body = body
|
||||
|
||||
# Assume ship is orbiting the target's primary (e.g., the star)
|
||||
current_plan = null
|
||||
current_state = State.PLANNING
|
||||
target_label.text = "Target: %s." % target_body.name
|
||||
info_label.text = "Select a maneuver."
|
||||
update_ui()
|
||||
|
||||
func _on_execute_plan_pressed():
|
||||
if current_plan:
|
||||
current_state = State.WAITING
|
||||
update_ui()
|
||||
|
||||
# --- Planning Function Calls ---
|
||||
func _on_plan_hohmann_pressed():
|
||||
current_plan = _calculate_hohmann_transfer(source_body, target_body)
|
||||
# TODO: map_controller.draw_planned_trajectory(...)
|
||||
update_ui()
|
||||
|
||||
func _on_plan_fast_pressed():
|
||||
# For simplicity, a "fast" transfer is a Hohmann with a 25% larger transfer orbit
|
||||
current_plan = _calculate_hohmann_transfer(source_body, target_body, 1.25)
|
||||
# TODO: map_controller.draw_planned_trajectory(...)
|
||||
update_ui()
|
||||
|
||||
func _on_plan_torchship_pressed():
|
||||
current_plan = _calculate_brachistochrone_transfer()
|
||||
# TODO: map_controller.draw_planned_trajectory(...)
|
||||
update_ui()
|
||||
|
||||
func _on_plan_gravity_assist_pressed():
|
||||
info_label.text = "Gravity Assist calculation is highly complex and not yet implemented."
|
||||
print("Placeholder for Gravity Assist logic.")
|
||||
|
||||
# --- Calculation and Execution ---
|
||||
func _calculate_hohmann_transfer(source_planet: CelestialBody, target_planet: CelestialBody, transfer_boost_factor: float = 1.0) -> Array:
|
||||
# Get the central star safely from the GameManager.
|
||||
#var ship_current_primary = ship.primary
|
||||
var star = GameManager.get_system_data().star
|
||||
if not is_instance_valid(star):
|
||||
print("Hohmann Error: Could not find star in GameManager.")
|
||||
return []
|
||||
|
||||
# This maneuver requires the ship and target to orbit the same star.
|
||||
if not target_planet:
|
||||
info_label.text = "Invalid Transfer: No target object for intersect."
|
||||
return []
|
||||
|
||||
# mu (μ): The Standard Gravitational Parameter of the star. It's G * M, a constant that simplifies calculations.
|
||||
var mu = OrbitalMechanics.G * star.mass
|
||||
|
||||
# r1: The ship's current orbital radius (distance from the star).
|
||||
var r1 = ship.global_position.distance_to(star.global_position)
|
||||
# r2: The target planet's orbital radius.
|
||||
var r2 = target_planet.global_position.distance_to(star.global_position)
|
||||
|
||||
# --- Hohmann Transfer Calculations ---
|
||||
|
||||
# v_source_orbit: The ship's current circular orbital speed.
|
||||
var v_source_orbit = sqrt(mu / r1)
|
||||
# v_target_orbit: The target planet's circular orbital speed.
|
||||
var v_target_orbit = sqrt(mu / r2)
|
||||
|
||||
# a_transfer: The semi-major axis (average radius) of the elliptical transfer orbit.
|
||||
var a_transfer = (r1 + r2) / 2.0 * transfer_boost_factor
|
||||
|
||||
# v_transfer_periapsis: The required speed at the start of the transfer (periapsis) to get onto the ellipse.
|
||||
var v_transfer_periapsis = sqrt(mu * ((2.0 / r1) - (1.0 / a_transfer)))
|
||||
# v_transfer_apoapsis: The speed the ship will have when it arrives at the end of the transfer (apoapsis).
|
||||
var v_transfer_apoapsis = sqrt(mu * ((2.0 / r2) - (1.0 / a_transfer)))
|
||||
|
||||
# delta_v1: The first burn. The change in speed needed to go from the source orbit to the transfer orbit.
|
||||
var delta_v1 = v_transfer_periapsis - v_source_orbit
|
||||
# delta_v2: The second burn. The change in speed needed to go from the transfer orbit to the target orbit.
|
||||
var delta_v2 = v_target_orbit - v_transfer_apoapsis
|
||||
|
||||
# time_of_flight: Half the period of the elliptical transfer orbit (Kepler's 3rd Law).
|
||||
var time_of_flight = PI * sqrt(pow(a_transfer, 3) / mu)
|
||||
|
||||
# --- Launch Window (Phase Angle) Calculations ---
|
||||
|
||||
# ang_vel_target: The angular speed of the target planet (in radians per second).
|
||||
var ang_vel_target = sqrt(mu / pow(r2, 3))
|
||||
# travel_angle: The angle the target planet will travel through during the ship's flight time.
|
||||
var travel_angle = ang_vel_target * time_of_flight
|
||||
# required_phase_angle: The starting angle needed between the ship and target for a successful intercept.
|
||||
var required_phase_angle = PI - travel_angle
|
||||
|
||||
# vec_to_ship/target: Direction vectors from the star to the ship and target.
|
||||
var vec_to_ship = (ship.global_position - star.global_position).normalized()
|
||||
var vec_to_target = (target_planet.global_position - star.global_position).normalized()
|
||||
# current_phase_angle: The angle between the ship and target right now.
|
||||
var current_phase_angle = vec_to_ship.angle_to(vec_to_target)
|
||||
|
||||
# ang_vel_ship: The ship's current angular speed.
|
||||
var ang_vel_ship = sqrt(mu / pow(r1, 3))
|
||||
# relative_ang_vel: How quickly the ship is catching up to (or falling behind) the target.
|
||||
var relative_ang_vel = ang_vel_ship - ang_vel_target
|
||||
# angle_to_wait: The angular distance the ship needs to "wait" for alignment.
|
||||
var angle_to_wait = current_phase_angle - required_phase_angle
|
||||
# wait_time: The final calculated time in seconds to wait for the optimal launch window.
|
||||
var wait_time = abs(angle_to_wait / relative_ang_vel)
|
||||
|
||||
# --- Final Plan Assembly ---
|
||||
|
||||
# Calculate burn durations
|
||||
var acceleration = ship.thruster_controller.main_engine_max_thrust() / ship.mass
|
||||
var burn_duration1 = delta_v1 / acceleration
|
||||
var burn_duration2 = delta_v2 / acceleration
|
||||
|
||||
var plan = []
|
||||
var burn1 = ImpulsiveBurn.new()
|
||||
burn1.delta_v_magnitude = delta_v1
|
||||
burn1.wait_time = wait_time
|
||||
burn1.burn_duration = burn_duration1
|
||||
# The desired rotation is the angle of the ship's prograde (tangential) velocity vector.
|
||||
burn1.desired_rotation_rad = ship.linear_velocity.angle()
|
||||
plan.append(burn1)
|
||||
|
||||
var burn2 = ImpulsiveBurn.new()
|
||||
burn2.delta_v_magnitude = delta_v2
|
||||
burn2.wait_time = time_of_flight - burn_duration1
|
||||
burn2.burn_duration = burn_duration2
|
||||
# The desired rotation for burn 2 is the tangential direction at the target orbit.
|
||||
burn2.desired_rotation_rad = (target_planet.global_position - star.global_position).orthogonal().angle()
|
||||
plan.append(burn2)
|
||||
|
||||
info_label.text = "Hohmann Plan:\nWait: %d s\nBurn 1: %.1f m/s (%.1f s)" % [wait_time, delta_v1, burn_duration1]
|
||||
return plan
|
||||
|
||||
func _calculate_brachistochrone_transfer() -> ContinuousBurnPlan:
|
||||
var distance = ship.global_position.distance_to(target_body.global_position)
|
||||
var acceleration = ship.thruster_controller.main_engine_max_thrust() / ship.mass
|
||||
if acceleration == 0: return null
|
||||
|
||||
# d = 1/2 * a * t^2 => t = sqrt(2d/a). We do this twice (accel/decel).
|
||||
var time_for_half_journey = sqrt(distance / acceleration)
|
||||
|
||||
var plan = ContinuousBurnPlan.new()
|
||||
plan.total_travel_time = 2 * time_for_half_journey
|
||||
plan.acceleration_time = time_for_half_journey
|
||||
plan.required_acceleration = acceleration
|
||||
|
||||
info_label.text = "Torchship Trajectory Calculated.\nTravel Time: %d s" % plan.total_travel_time
|
||||
return plan
|
||||
|
||||
func _execute_maneuver():
|
||||
if current_state != State.WAITING or not current_plan: return
|
||||
|
||||
current_state = State.EXECUTING
|
||||
var burn: ImpulsiveBurn = current_plan.pop_front()
|
||||
|
||||
# Tell the controller to start burning. Orientation is already handled.
|
||||
ship.signal_bus.emit_signal("timed_burn_commanded", burn.burn_duration)
|
||||
|
||||
# Set up for the next leg of the journey or finish
|
||||
if not current_plan.is_empty():
|
||||
# The next "wait_time" is the coasting period between burns.
|
||||
current_state = State.WAITING
|
||||
|
||||
# Reset the flag here, preparing the system for the *next* burn's rotation command.
|
||||
rotation_plan_sent = false
|
||||
else:
|
||||
current_state = State.IDLE
|
||||
current_plan = null
|
||||
update_ui()
|
||||
|
||||
var _previous_angular_velocity: float = 0.0
|
||||
var actual_applied_torque: float = 0.0
|
||||
|
||||
func _update_ship_status_label():
|
||||
if not is_instance_valid(ship):
|
||||
ship_status_label.text = "NO SHIP DATA"
|
||||
return
|
||||
|
||||
# Build an array of strings for each line of the display
|
||||
var status_lines = []
|
||||
|
||||
var assume_torque = ship.thruster_controller.current_applied_torque
|
||||
|
||||
# Get rotation data from the ship
|
||||
var rotation_deg = rad_to_deg(ship.rotation)
|
||||
var current_angular_velocity = ship.angular_velocity
|
||||
var angular_vel_dps = rad_to_deg(ship.angular_velocity)
|
||||
var delta_omega = ship.angular_velocity - _previous_angular_velocity
|
||||
var delta = get_physics_process_delta_time()
|
||||
|
||||
if delta > 0:
|
||||
var angular_acceleration = delta_omega / delta
|
||||
#actual_applied_torque = ship.accumulated_torque # ship.inertia * angular_acceleration
|
||||
# Update the indicator color based on the comparison.
|
||||
|
||||
_previous_angular_velocity = current_angular_velocity
|
||||
|
||||
# Get the sign of each torque value.
|
||||
var calc_sign = sign(assume_torque)
|
||||
var actual_sign = sign(actual_applied_torque)
|
||||
|
||||
if calc_sign != 0 and calc_sign == actual_sign:
|
||||
# Success: We are commanding a turn, and the physics engine agrees.
|
||||
torque_indicator.color = Color.GREEN
|
||||
elif calc_sign != 0 and actual_sign == 0:
|
||||
# Mismatch: We are commanding a turn, but the physics engine reports no torque.
|
||||
# This is the flicker you are seeing.
|
||||
torque_indicator.color = Color.RED
|
||||
else:
|
||||
# Idle: No torque is being commanded, and none is being applied.
|
||||
torque_indicator.color = Color.DARK_GRAY
|
||||
|
||||
var sensor_torque_sign = "+" if signf(actual_applied_torque) > 0.0 else "-" if signf(actual_applied_torque) < 0.0 else "0"
|
||||
|
||||
|
||||
status_lines.append("Rotation: %.1f deg" % rotation_deg)
|
||||
status_lines.append("Ang. Vel.: %.5f deg/s" % angular_vel_dps)
|
||||
status_lines.append("Assumed Torque: %.2f N·m" % assume_torque)
|
||||
status_lines.append("Sensed Torque: %.2f N·m" % actual_applied_torque)
|
||||
|
||||
# Get burn data from the thruster controller
|
||||
var burn_time = ship.thruster_controller.current_burn_time_remaining
|
||||
var thrust_force = ship.thruster_controller.current_thrust_force
|
||||
status_lines.append("Burn Time: %.1f s" % burn_time)
|
||||
status_lines.append("Thrust: %.5f N" % thrust_force)
|
||||
|
||||
# Join the lines with a newline character and update the label
|
||||
ship_status_label.text = "\n".join(status_lines)
|
||||
|
||||
# Helper function to format a float of seconds into a M:SS string
|
||||
func _format_seconds_to_mmss(seconds_float: float) -> String:
|
||||
if seconds_float < 0:
|
||||
seconds_float = 0
|
||||
var total_seconds = int(seconds_float)
|
||||
var minutes = total_seconds / 60
|
||||
var seconds = total_seconds % 60
|
||||
# "%02d" formats the seconds with a leading zero if needed (e.g., 05)
|
||||
return "%d:%02d" % [minutes, seconds]
|
||||
@ -1 +0,0 @@
|
||||
uid://cq2sgw12uj4jl
|
||||
@ -1,103 +0,0 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://cxpjm8tp3l1j7"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cq2sgw12uj4jl" path="res://scenes/ship/systems/navigation_computer.gd" id="1_ys00n"]
|
||||
[ext_resource type="Script" uid="uid://b74hxlsox8ldo" path="res://scenes/ship/systems/map_controller.gd" id="2_ys00n"]
|
||||
[ext_resource type="PackedScene" uid="uid://c2imrmgjthfdm" path="res://scenes/UI/MapIcon.tscn" id="3_378us"]
|
||||
|
||||
[node name="NavigationComputer" type="Node"]
|
||||
script = ExtResource("1_ys00n")
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
|
||||
[node name="NavigationUI" type="CanvasLayer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
|
||||
[node name="NavigationScreen" type="MarginContainer" parent="NavigationUI"]
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="NavigationUI/NavigationScreen"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer" parent="NavigationUI/NavigationScreen/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(250, 0)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
|
||||
[node name="TargetLabel" type="Label" parent="NavigationUI/NavigationScreen/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
clip_text = true
|
||||
|
||||
[node name="InfoLabel" type="Label" parent="NavigationUI/NavigationScreen/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TorqueIndicator" type="ColorRect" parent="NavigationUI/NavigationScreen/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(20, 20)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="PlanHohmannButton" type="Button" parent="NavigationUI/NavigationScreen/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Hohmann Transfer"
|
||||
|
||||
[node name="PlanFastButton" type="Button" parent="NavigationUI/NavigationScreen/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Fast Impulse"
|
||||
|
||||
[node name="PlanTorchshipButton" type="Button" parent="NavigationUI/NavigationScreen/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Torchship Burn"
|
||||
|
||||
[node name="PlanGravityAssistButton" type="Button" parent="NavigationUI/NavigationScreen/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Grav Assist"
|
||||
|
||||
[node name="ExecutePlanButton" type="Button" parent="NavigationUI/NavigationScreen/HBoxContainer/VBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Launch"
|
||||
|
||||
[node name="MapController" type="Control" parent="NavigationUI/NavigationScreen/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
clip_contents = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_stretch_ratio = 3.0
|
||||
script = ExtResource("2_ys00n")
|
||||
map_icon_scene = ExtResource("3_378us")
|
||||
|
||||
[node name="RightHandPanel" type="VBoxContainer" parent="NavigationUI/NavigationScreen/HBoxContainer"]
|
||||
custom_minimum_size = Vector2(0, 300)
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 4
|
||||
|
||||
[node name="ShipStatusLabel" type="Label" parent="NavigationUI/NavigationScreen/HBoxContainer/RightHandPanel"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
|
||||
[node name="MarginContainer" type="MarginContainer" parent="NavigationUI/NavigationScreen/HBoxContainer/RightHandPanel"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
||||
[node name="SubViewportContainer" type="SubViewportContainer" parent="NavigationUI/NavigationScreen/HBoxContainer/RightHandPanel"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="SubViewport" type="SubViewport" parent="NavigationUI/NavigationScreen/HBoxContainer/RightHandPanel/SubViewportContainer"]
|
||||
unique_name_in_owner = true
|
||||
transparent_bg = true
|
||||
handle_input_locally = false
|
||||
size = Vector2i(320, 180)
|
||||
render_target_update_mode = 4
|
||||
|
||||
[node name="ShipViewCamera" type="Camera2D" parent="NavigationUI/NavigationScreen/HBoxContainer/RightHandPanel/SubViewportContainer/SubViewport"]
|
||||
unique_name_in_owner = true
|
||||
@ -1,371 +0,0 @@
|
||||
# ThrusterController.gd
|
||||
class_name ThrusterController
|
||||
extends Node2D
|
||||
|
||||
# --- References ---
|
||||
@onready var ship: Spaceship = get_parent()
|
||||
var ship_signal_bus: ShipSignalBus
|
||||
|
||||
@onready var main_engines: Array[Thruster] = []
|
||||
@onready var rcs_thrusters: Array[Thruster] = []
|
||||
|
||||
# --- Public Status Variables ---
|
||||
var current_burn_time_remaining: float = 0.0
|
||||
var current_thrust_force: float = 0.0
|
||||
var current_applied_torque: float = 0.0
|
||||
|
||||
# --- Autopilot & Manual Control State ---
|
||||
enum RotationMode { NONE, AUTOPILOT_TURN, MANUAL_HOLD }
|
||||
var rotation_mode: RotationMode = RotationMode.NONE
|
||||
const ATTITUDE_TOLERANCE_RAD = 0.005 # ~1 degree. If error is > this, we correct.
|
||||
|
||||
|
||||
# --- NEW: Inner class to store calibrated data for each thruster ---
|
||||
class ThrusterData:
|
||||
var thruster_node: Thruster
|
||||
var measured_force_vector: Vector2 # The linear push it provides
|
||||
var measured_torque: float # The rotational force it provides
|
||||
var primary_axis: String # e.g., "Linear", "Yaw", "Surge"
|
||||
|
||||
# --- NEW: A dictionary to map thruster nodes to their performance data ---
|
||||
var thruster_data_map: Dictionary[Thruster, ThrusterData] = {}
|
||||
|
||||
var target_rotation_rad: float = 0.0
|
||||
var is_burning: bool = false
|
||||
|
||||
# --- Ship Performance Data (calibrated) ---
|
||||
var max_positive_torque: float = 10000.0 # Default value, will be calibrated
|
||||
var max_negative_torque: float = 10000.0 # Default value, will be calibrated
|
||||
|
||||
# --- Manual Hold Constants (PD Controller) ---
|
||||
@export var HOLD_KP: float = 8000.0 # Proportional gain for stability
|
||||
@export var HOLD_KD: float = 1200.0 # Derivative gain for damping
|
||||
|
||||
# --- Crew Comfort Levels ---
|
||||
const COMFORTABLE_ANGULAR_VELOCITY = 0.5 # radians/sec
|
||||
|
||||
func _ready() -> void:
|
||||
await get_tree().process_frame
|
||||
|
||||
if is_instance_valid(ship):
|
||||
ship_signal_bus = ship.signal_bus
|
||||
ship_signal_bus.timed_rotation_commanded.connect(_on_autopilot_rotation_commanded)
|
||||
ship_signal_bus.timed_burn_commanded.connect(_on_autopilot_main_engine_burn_commanded)
|
||||
|
||||
# Register thrusters
|
||||
var thruster_nodes = get_tree().get_nodes_in_group("ship_thrusters")
|
||||
for thruster: Thruster in thruster_nodes:
|
||||
if thruster.get_parent() == ship:
|
||||
if thruster.main_thruster: main_engines.append(thruster)
|
||||
else: rcs_thrusters.append(thruster)
|
||||
|
||||
await get_tree().physics_frame
|
||||
|
||||
# Calibrate thrusters on startup
|
||||
await calibrate_rcs_performance()
|
||||
command_hold_attitude(0.0)
|
||||
|
||||
|
||||
func _physics_process(_delta: float) -> void:
|
||||
match rotation_mode:
|
||||
RotationMode.MANUAL_HOLD:
|
||||
#if _is_attitude_misaligned() or true:
|
||||
_perform_manual_hold()
|
||||
#if ship: print("AUTOPILOT: Holding attitude.")
|
||||
|
||||
# --- PUBLIC API ---
|
||||
|
||||
# The Navigation Computer calls this.
|
||||
func _on_autopilot_rotation_commanded(new_target_rot: float, time_window: float):
|
||||
rotation_mode = RotationMode.AUTOPILOT_TURN
|
||||
# Start the asynchronous autopilot maneuver
|
||||
_execute_autopilot_rotation(new_target_rot, time_window)
|
||||
|
||||
func _on_autopilot_main_engine_burn_commanded(duration: float):
|
||||
while _is_attitude_misaligned():
|
||||
await get_tree().physics_frame
|
||||
|
||||
_fire_main_engine(duration)
|
||||
|
||||
# The player/manual input would call this.
|
||||
func command_hold_attitude(rotation_rad: float):
|
||||
if rotation_mode != RotationMode.MANUAL_HOLD:
|
||||
print("AUTOPILOT: Engaging manual attitude hold at %.2f deg." % rad_to_deg(rotation_rad))
|
||||
rotation_mode = RotationMode.MANUAL_HOLD
|
||||
target_rotation_rad = rotation_rad
|
||||
|
||||
func command_stop_rotation():
|
||||
if rotation_mode == RotationMode.MANUAL_HOLD:
|
||||
command_hold_attitude(ship.rotation)
|
||||
|
||||
func command_disable_rcs():
|
||||
rotation_mode = RotationMode.NONE
|
||||
shutdown_rcs()
|
||||
print("AUTOPILOT: RCS disabled.")
|
||||
|
||||
# --- Helper for Misalignment Check ---
|
||||
func _is_attitude_misaligned() -> bool:
|
||||
var error = shortest_angle_between(ship.rotation, target_rotation_rad)
|
||||
return abs(error) > ATTITUDE_TOLERANCE_RAD
|
||||
|
||||
func calibrate_rcs_performance() -> void:
|
||||
print("AUTOPILOT: Beginning full RCS calibration protocol...")
|
||||
thruster_data_map.clear()
|
||||
|
||||
# Create a temporary copy as the original arrays may change
|
||||
var all_thrusters_to_test = main_engines + rcs_thrusters
|
||||
|
||||
for thruster in all_thrusters_to_test:
|
||||
var data: ThrusterData = await _calibrate_single_thruster(thruster)
|
||||
thruster_data_map[thruster] = data
|
||||
print(" - Calibrated %s: Force(%.3f, %.3f), Torque(%.3f)" % [thruster.name, data.measured_force_vector.x, data.measured_force_vector.y, data.measured_torque])
|
||||
|
||||
# Now that we have the data, we can update the ship's max torque values
|
||||
max_positive_torque = 0
|
||||
max_negative_torque = 0
|
||||
for data in thruster_data_map.values():
|
||||
if data.measured_torque > 0:
|
||||
max_positive_torque += data.measured_torque
|
||||
else:
|
||||
max_negative_torque += abs(data.measured_torque)
|
||||
|
||||
print("RCS Calibration Complete: Max Pos Torque: %.2f, Max Neg Torque: %.2f" % [max_positive_torque, max_negative_torque])
|
||||
|
||||
# --- 3. Cleanup: Perform a final burn to stop the ship ---
|
||||
# We use the newly calibrated torque to calculate the stop burn time.
|
||||
await _reset_ship_rotation()
|
||||
|
||||
shutdown_rcs()
|
||||
|
||||
# Auto-tune the PD controller with the new values
|
||||
var average_max_torque = (max_positive_torque + max_negative_torque) / 2.0
|
||||
HOLD_KP = average_max_torque * 0.1
|
||||
HOLD_KD = HOLD_KP * 1
|
||||
print("PD Controller Auto-Tuned: Kp set to %.4f, Kd set to %.4f" % [HOLD_KP, HOLD_KD])
|
||||
|
||||
|
||||
func _calibrate_single_thruster(thruster: Thruster) -> ThrusterData:
|
||||
var data = ThrusterData.new()
|
||||
data.thruster_node = thruster
|
||||
|
||||
# Prepare for test: save state, disable damping
|
||||
var initial_velocity = ship.linear_velocity
|
||||
var initial_angular_velocity = ship.angular_velocity
|
||||
|
||||
var test_burn_duration = 0.5 if thruster.main_thruster else 0.2 # A very short burst
|
||||
|
||||
# --- Perform Test Fire ---
|
||||
thruster.turn_on()
|
||||
await get_tree().create_timer(test_burn_duration).timeout
|
||||
thruster.turn_off()
|
||||
|
||||
# Let the physics engine settle for one frame
|
||||
await get_tree().physics_frame
|
||||
|
||||
# --- Measure Results ---
|
||||
var delta_velocity = ship.linear_velocity - initial_velocity
|
||||
var delta_angular_velocity = ship.angular_velocity - initial_angular_velocity
|
||||
|
||||
# --- Calculate Performance ---
|
||||
# Force = mass * acceleration (a = dv/dt)
|
||||
data.measured_force_vector = ship.mass * (delta_velocity / test_burn_duration)
|
||||
# Torque = inertia * angular_acceleration (alpha = dw/dt)
|
||||
data.measured_torque = ship.inertia * (delta_angular_velocity / test_burn_duration)
|
||||
|
||||
return data
|
||||
|
||||
# Resets the ship's rotation and angular velocity to zero.
|
||||
func _reset_ship_rotation() -> void:
|
||||
# Use the calibrated torque to calculate the final stop burn time.
|
||||
if abs(ship.angular_velocity) > 0.01:
|
||||
var stop_torque_dir = -sign(ship.angular_velocity)
|
||||
var stop_torque_mag = max_positive_torque if stop_torque_dir > 0 else max_negative_torque
|
||||
var stop_burn_time = abs((ship.inertia * ship.angular_velocity) / stop_torque_mag)
|
||||
|
||||
var stop_timer = get_tree().create_timer(stop_burn_time)
|
||||
while stop_timer.get_time_left() > 0.0:
|
||||
apply_rotational_thrust(stop_torque_dir * stop_torque_mag)
|
||||
await get_tree().physics_frame
|
||||
|
||||
shutdown_rcs()
|
||||
print("AUTOPILOT: Ship rotation and velocity reset.")
|
||||
|
||||
# --- AUTOPILOT "BANG-COAST-BANG" LOGIC (REFACTORED) ---
|
||||
func _execute_autopilot_rotation(new_target_rot: float, time_window: float):
|
||||
var angle_to_turn = shortest_angle_between(ship.rotation, new_target_rot)
|
||||
|
||||
if abs(angle_to_turn) < 0.01:
|
||||
command_hold_attitude(new_target_rot)
|
||||
return
|
||||
|
||||
# --- NEW: Get the specific torque values for each phase ---
|
||||
var accel_torque = max_positive_torque if angle_to_turn > 0 else max_negative_torque
|
||||
var decel_torque = max_negative_torque if angle_to_turn > 0 else max_positive_torque
|
||||
|
||||
if accel_torque == 0 or decel_torque == 0:
|
||||
print("AUTOPILOT ERROR: Missing thrusters for a full rotation.")
|
||||
return
|
||||
|
||||
# --- NEW: Asymmetrical Burn Calculation ---
|
||||
# This is a more complex kinematic problem. We solve for the peak velocity and individual times.
|
||||
var accel_angular_accel = accel_torque / ship.inertia
|
||||
var decel_angular_accel = decel_torque / ship.inertia
|
||||
|
||||
# Solve for peak angular velocity (ω_peak) and times (t1, t2)
|
||||
var peak_angular_velocity = (2 * angle_to_turn * accel_angular_accel * decel_angular_accel) / (accel_angular_accel + decel_angular_accel)
|
||||
peak_angular_velocity = sqrt(abs(peak_angular_velocity)) * sign(angle_to_turn)
|
||||
|
||||
var accel_burn_time = abs(peak_angular_velocity / accel_angular_accel)
|
||||
var decel_burn_time = abs(peak_angular_velocity / decel_angular_accel)
|
||||
|
||||
var total_maneuver_time = accel_burn_time + decel_burn_time
|
||||
|
||||
if total_maneuver_time > time_window:
|
||||
print("AUTOPILOT WARNING: Maneuver is impossible in the given time window. Performing max-power turn.")
|
||||
# Fallback to a simple 50/50 burn if time is too short.
|
||||
accel_burn_time = time_window / 2.0
|
||||
decel_burn_time = time_window / 2.0
|
||||
|
||||
# No coast time in this simplified model, but it could be added back with more complex math.
|
||||
|
||||
print("AUTOPILOT: Asymmetrical Plan: Accel Burn %.2fs, Decel Burn %.2fs" % [accel_burn_time, decel_burn_time])
|
||||
|
||||
# --- Execute Maneuver ---
|
||||
|
||||
# ACCELERATION BURN
|
||||
var accel_timer = get_tree().create_timer(accel_burn_time)
|
||||
while accel_timer.get_time_left() > 0.0:
|
||||
apply_rotational_thrust(accel_torque * sign(angle_to_turn))
|
||||
await get_tree().physics_frame
|
||||
|
||||
# DECELERATION BURN
|
||||
print("AUTOPILOT: Acceleration complete, executing deceleration burn.")
|
||||
var decel_timer = get_tree().create_timer(decel_burn_time)
|
||||
while decel_timer.get_time_left() > 0.0:
|
||||
apply_rotational_thrust(-accel_torque * sign(angle_to_turn)) # Apply opposite torque
|
||||
await get_tree().physics_frame
|
||||
|
||||
shutdown_rcs()
|
||||
|
||||
print("AUTOPILOT: Rotation maneuver complete.")
|
||||
command_hold_attitude(new_target_rot)
|
||||
|
||||
|
||||
# --- MANUAL HOLD & STABILIZATION LOGIC (REFACTORED) ---
|
||||
func _perform_manual_hold():
|
||||
if not is_instance_valid(ship):
|
||||
rotation_mode = RotationMode.NONE # Safety check
|
||||
return
|
||||
|
||||
# 1. Calculate the error (how far we have to go).
|
||||
var error = shortest_angle_between(ship.rotation, target_rotation_rad)
|
||||
|
||||
# 2. Calculate the required torque using the PD formula.
|
||||
# This value will be positive, negative, or zero depending on our state.
|
||||
var desired_torque = (error * HOLD_KP) - (ship.angular_velocity * HOLD_KD)
|
||||
|
||||
# 3. Continuously command the thrusters based on the calculated torque.
|
||||
# This function will turn thrusters ON if they match the desired_torque's sign
|
||||
# and OFF if they do not. It must be called every frame to work correctly.
|
||||
if not _is_attitude_misaligned() and abs(ship.angular_velocity) < 0.01 and abs(desired_torque) < 0.0001:
|
||||
ship.angular_velocity = 0.0
|
||||
ship.rotation = target_rotation_rad
|
||||
|
||||
shutdown_rcs()
|
||||
else:
|
||||
apply_rotational_thrust(desired_torque)
|
||||
|
||||
|
||||
# --- WORKER FUNCTIONS ---
|
||||
|
||||
func apply_rotational_thrust(desired_torque: float):
|
||||
if not is_instance_valid(ship):
|
||||
return
|
||||
|
||||
var delta_torque = 0.0
|
||||
|
||||
# Iterate through all available RCS thrusters
|
||||
for thruster in rcs_thrusters:
|
||||
# ... (your existing calculation for produced_torque is correct)
|
||||
var thruster_local_pos = thruster.position
|
||||
var thruster_data: ThrusterData = thruster_data_map.get(thruster)
|
||||
|
||||
# If this thruster can help, turn it on. Otherwise, explicitly turn it off.
|
||||
if sign(thruster_data.measured_torque) == sign(desired_torque) and desired_torque != 0:
|
||||
thruster.turn_on()
|
||||
delta_torque += thruster_data.measured_torque
|
||||
|
||||
else:
|
||||
thruster.turn_off()
|
||||
|
||||
current_applied_torque = delta_torque
|
||||
|
||||
## Applies forces from the correct thrusters to achieve a desired torque.
|
||||
#func apply_rotational_thrust(desired_torque: float):
|
||||
#if not is_instance_valid(ship):
|
||||
#return
|
||||
#
|
||||
##print("AUTOPILOT: Applying rotational thrust %f" % desired_torque)
|
||||
## Iterate through all available RCS thrusters
|
||||
#var delta_torque = 0.0
|
||||
#for thruster in rcs_thrusters:
|
||||
## 1. Get the thruster's position relative to the ship's center. This is its local position.
|
||||
#var thruster_local_pos = thruster.position
|
||||
#
|
||||
## 2. Calculate the force this thruster produces, also in LOCAL space.
|
||||
#var force_local_vec = thruster.thrust_direction * thruster.max_thrust
|
||||
#
|
||||
## 3. Calculate the torque in LOCAL space. This is now a valid calculation.
|
||||
#var produced_torque = thruster_local_pos.cross(force_local_vec)
|
||||
## 4. Decide whether to fire the thruster. This check will now work correctly.
|
||||
#if sign(produced_torque) == sign(desired_torque):
|
||||
#thruster.turn_on()
|
||||
#delta_torque += produced_torque
|
||||
#
|
||||
#else:
|
||||
#thruster.turn_off()
|
||||
#
|
||||
#current_applied_torque = delta_torque
|
||||
|
||||
|
||||
func shutdown_rcs():
|
||||
for thruster in rcs_thrusters: thruster.turn_off()
|
||||
current_applied_torque = 0.0
|
||||
|
||||
# Calculates the shortest angle between two angles (in radians).
|
||||
# The result will be between -PI and +PI. The sign indicates the direction.
|
||||
func shortest_angle_between(from_angle: float, to_angle: float) -> float:
|
||||
var difference = fposmod(to_angle - from_angle, TAU)
|
||||
if difference > PI:
|
||||
return difference - TAU
|
||||
else:
|
||||
return difference
|
||||
|
||||
# Calculates the total torque available from all thrusters for a given direction.
|
||||
func _get_total_possible_torque(direction: int) -> float:
|
||||
var total_torque: float = 0.0
|
||||
for thruster in rcs_thrusters:
|
||||
var r = thruster.position
|
||||
var force_local_vec = thruster.thrust_direction * thruster.max_thrust
|
||||
var produced_torque = r.cross(force_local_vec)
|
||||
if sign(produced_torque) == direction:
|
||||
total_torque += abs(produced_torque)
|
||||
return total_torque
|
||||
|
||||
# Fires all main engines for a specified duration.
|
||||
func _fire_main_engine(duration: float):
|
||||
print("AUTOPILOT: Firing main engine for %.2f seconds." % duration)
|
||||
|
||||
for engine in main_engines:
|
||||
engine.turn_on()
|
||||
|
||||
await get_tree().create_timer(duration).timeout
|
||||
|
||||
for engine in main_engines:
|
||||
engine.turn_off()
|
||||
|
||||
is_burning = false
|
||||
print("AUTOPILOT: Main engine burn complete.")
|
||||
|
||||
func main_engine_max_thrust():
|
||||
return main_engines.reduce(func(thrust, engine : Thruster): return thrust + engine.max_thrust, 0.0)
|
||||
@ -1 +0,0 @@
|
||||
uid://c0bx113ifxyh8
|
||||
163
scenes/tests/3d/character_pawn_3d.gd
Normal file
163
scenes/tests/3d/character_pawn_3d.gd
Normal file
@ -0,0 +1,163 @@
|
||||
# CharacterPawn.gd
|
||||
extends CharacterBody3D
|
||||
class_name CharacterPawn3D
|
||||
|
||||
## Core Parameters
|
||||
@export var collision_energy_loss: float = 0.3
|
||||
@export var base_inertia: float = 1.0 # Pawn's inertia without suit
|
||||
|
||||
## Input Buffers
|
||||
var _move_input: Vector2 = Vector2.ZERO
|
||||
var _roll_input: float = 0.0
|
||||
var _vertical_input: float = 0.0
|
||||
var _interact_input: PlayerController3D.KeyInput = PlayerController3D.KeyInput.new(false, false, false)
|
||||
var _l_click_input: PlayerController3D.KeyInput = PlayerController3D.KeyInput.new(false, false, false)
|
||||
var _r_click_input: PlayerController3D.KeyInput = PlayerController3D.KeyInput.new(false, false, false)
|
||||
var _pitch_yaw_input: Vector2 = Vector2.ZERO
|
||||
|
||||
## Rotation Variables
|
||||
@onready var camera_anchor: Marker3D = $CameraAnchor
|
||||
@onready var camera_pivot: Node3D = $CameraPivot
|
||||
@onready var camera: Camera3D = $CameraPivot/SpringArm/Camera3D
|
||||
@export_range(0.1, PI / 2.0) var max_yaw_rad: float = deg_to_rad(80.0)
|
||||
@export_range(-PI / 2.0 + 0.01, 0) var min_pitch_rad: float = deg_to_rad(-75.0)
|
||||
@export_range(0, PI / 2.0 - 0.01) var max_pitch_rad: float = deg_to_rad(60.0)
|
||||
@export var head_turn_lerp_speed: float = 15.0
|
||||
|
||||
## Movement Components
|
||||
@onready var eva_suit_component: EVAMovementComponent = $EVAMovementComponent
|
||||
@onready var zero_g_movemement_component: ZeroGMovementComponent = $ZeroGMovementComponent
|
||||
|
||||
## Physics State (Managed by Pawn)
|
||||
var angular_velocity: Vector3 = Vector3.ZERO
|
||||
@export var angular_damping: float = 0.95 # Base damping
|
||||
|
||||
## Other State Variables
|
||||
var current_gravity: Vector3 = Vector3.ZERO # TODO: Implement gravity detection
|
||||
var overlapping_ladder_area: Area3D = null
|
||||
@onready var grip_detector: Area3D = $GripDetector
|
||||
|
||||
# Constants for State Checks
|
||||
const WALKABLE_GRAVITY_THRESHOLD: float = 1.0
|
||||
|
||||
func _ready():
|
||||
# find movement components
|
||||
if eva_suit_component: print("Found EVA Suit Controller")
|
||||
if zero_g_movemement_component: print("Found Zero-G Movement Controller")
|
||||
|
||||
# Connect grip detector signals
|
||||
if grip_detector and zero_g_movemement_component:
|
||||
print("GripDetector Area3D node found")
|
||||
grip_detector.area_entered.connect(zero_g_movemement_component.on_grip_area_entered)
|
||||
grip_detector.area_exited.connect(zero_g_movemement_component.on_grip_area_exited)
|
||||
else:
|
||||
printerr("GripDetector Area3D node not found on CharacterPawn!")
|
||||
|
||||
if is_multiplayer_authority():
|
||||
camera.make_current()
|
||||
camera.process_mode = Node.PROCESS_MODE_ALWAYS
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
camera_pivot.global_transform = camera_anchor.get_global_transform_interpolated()
|
||||
|
||||
|
||||
func _physics_process(delta: float):
|
||||
# 1. Apply Mouse Rotation (Universal head look)
|
||||
_apply_mouse_rotation()
|
||||
|
||||
if zero_g_movemement_component: # Fallback to ZeroG controller (for initiating reach)
|
||||
zero_g_movemement_component.process_movement(delta, _move_input, _vertical_input, _roll_input, _l_click_input, _r_click_input)
|
||||
|
||||
# 3. Integrate Angular Velocity (Universal)
|
||||
_integrate_angular_velocity(delta)
|
||||
|
||||
# 4. Apply Linear Velocity & Collision (Universal)
|
||||
# Use move_and_slide for states affected by gravity/floor or zero-g collisions
|
||||
move_and_slide()
|
||||
|
||||
# Check for collision response AFTER move_and_slide
|
||||
var collision_count = get_slide_collision_count()
|
||||
if collision_count > 0:
|
||||
var collision = get_slide_collision(collision_count - 1) # Get last collision
|
||||
# Delegate or handle basic bounce
|
||||
if eva_suit_component:
|
||||
eva_suit_component.handle_collision(collision, collision_energy_loss)
|
||||
else:
|
||||
_handle_basic_collision(collision)
|
||||
|
||||
# 5. Reset Inputs
|
||||
_reset_inputs()
|
||||
|
||||
# --- Universal Rotation ---
|
||||
func _apply_mouse_rotation():
|
||||
if _pitch_yaw_input != Vector2.ZERO:
|
||||
camera_anchor.rotate_y(-_pitch_yaw_input.x)
|
||||
|
||||
# Apply Pitch LOCALLY to pivot
|
||||
camera_anchor.rotate_object_local(Vector3.RIGHT, _pitch_yaw_input.y)
|
||||
camera_anchor.rotation.x = clamp(camera_anchor.rotation.x, min_pitch_rad, max_pitch_rad)
|
||||
|
||||
_pitch_yaw_input = Vector2.ZERO
|
||||
|
||||
camera_anchor.rotation.z = 0.0
|
||||
|
||||
# --- Universal Integration & Collision ---
|
||||
func _integrate_angular_velocity(delta: float):
|
||||
# (Same integration logic as before using Basis or Quaternions)
|
||||
if angular_velocity.length_squared() > 0.0001:
|
||||
rotate(angular_velocity.normalized(), angular_velocity.length() * delta)
|
||||
|
||||
# Prevent drift if velocity is very small
|
||||
if angular_velocity.length_squared() < 0.0001:
|
||||
angular_velocity = Vector3.ZERO
|
||||
|
||||
func _handle_basic_collision(collision: KinematicCollision3D):
|
||||
var surface_normal = collision.get_normal()
|
||||
velocity = velocity.bounce(surface_normal)
|
||||
velocity *= (1.0 - collision_energy_loss * 0.5)
|
||||
|
||||
# --- Public Helper for Controllers ---
|
||||
# Applies torque affecting angular velocity
|
||||
func add_torque(torque_global: Vector3, delta: float):
|
||||
# Calculate effective inertia (base + suit multiplier if applicable)
|
||||
var effective_inertia = base_inertia * (eva_suit_component.inertia_multiplier if eva_suit_component else 1.0)
|
||||
if effective_inertia <= 0: effective_inertia = 1.0 # Safety prevent division by zero
|
||||
# Apply change directly to angular velocity using the global torque
|
||||
|
||||
angular_velocity += (torque_global / effective_inertia) * delta
|
||||
|
||||
# --- Movement Implementations (Keep non-EVA ones here) ---
|
||||
func _apply_walking_movement(_delta: float): pass # TODO
|
||||
func _apply_ladder_floating_drag(delta: float):
|
||||
velocity = velocity.lerp(Vector3.ZERO, delta * 2.0);
|
||||
angular_velocity = angular_velocity.lerp(Vector3.ZERO, delta * 2.0)
|
||||
func _apply_ladder_movement(_delta: float): pass # TODO
|
||||
|
||||
# --- Input Setters/Resets (Add vertical to set_movement_input) ---
|
||||
func set_movement_input(move: Vector2, roll: float, vertical: float): _move_input = move; _roll_input = roll; _vertical_input = vertical
|
||||
func set_interaction_input(interact_input: PlayerController3D.KeyInput): _interact_input = interact_input
|
||||
func set_rotation_input(pitch_yaw_input: Vector2): _pitch_yaw_input += pitch_yaw_input
|
||||
func set_click_input(l_action: PlayerController3D.KeyInput, r_action: PlayerController3D.KeyInput):
|
||||
_l_click_input = l_action
|
||||
_r_click_input = r_action
|
||||
func _reset_inputs():
|
||||
_move_input = Vector2.ZERO
|
||||
_roll_input = 0.0
|
||||
_vertical_input = 0.0
|
||||
_interact_input = PlayerController3D.KeyInput.new(false, false, false)
|
||||
_l_click_input = PlayerController3D.KeyInput.new(false, false, false)
|
||||
_r_click_input = PlayerController3D.KeyInput.new(false, false, false)
|
||||
_pitch_yaw_input = Vector2.ZERO # Keep _r_held
|
||||
|
||||
# --- Helper Functions ---
|
||||
func _on_ladder_area_entered(area: Area3D): if area.is_in_group("Ladders"): overlapping_ladder_area = area
|
||||
func _on_ladder_area_exited(area: Area3D): if area == overlapping_ladder_area: overlapping_ladder_area = null
|
||||
func _reset_head_yaw(delta: float):
|
||||
# Smoothly apply the reset target to the actual pivot rotation
|
||||
camera_anchor.rotation.y = lerpf(camera_anchor.rotation.y, 0.0, delta * head_turn_lerp_speed)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
match what:
|
||||
NOTIFICATION_ENTER_TREE:
|
||||
set_multiplayer_authority(int(name))
|
||||
1
scenes/tests/3d/character_pawn_3d.gd.uid
Normal file
1
scenes/tests/3d/character_pawn_3d.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cdmmiixa75f3x
|
||||
66
scenes/tests/3d/character_pawn_3d.tscn
Normal file
66
scenes/tests/3d/character_pawn_3d.tscn
Normal file
@ -0,0 +1,66 @@
|
||||
[gd_scene load_steps=9 format=3 uid="uid://7yc6a07xoccy"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cdmmiixa75f3x" path="res://scenes/tests/3d/character_pawn_3d.gd" id="1_4frsu"]
|
||||
[ext_resource type="PackedScene" uid="uid://bm1rbv4tuppbc" path="res://eva_suit_controller.tscn" id="3_gnddn"]
|
||||
[ext_resource type="Script" uid="uid://y3vo40i16ek3" path="res://scenes/tests/3d/zero_g_movement_component.gd" id="4_8jhjh"]
|
||||
[ext_resource type="PackedScene" uid="uid://ba3ijdstp2bvt" path="res://scenes/tests/3d/player_controller_3d.tscn" id="4_bcy3l"]
|
||||
|
||||
[sub_resource type="CapsuleShape3D" id="CapsuleShape3D_6vm80"]
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_6vm80"]
|
||||
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_gnddn"]
|
||||
radius = 1.0
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_gnddn"]
|
||||
properties/0/path = NodePath(".:position")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
properties/1/path = NodePath(".:rotation")
|
||||
properties/1/spawn = true
|
||||
properties/1/replication_mode = 1
|
||||
properties/2/path = NodePath("CameraPivot:rotation")
|
||||
properties/2/spawn = true
|
||||
properties/2/replication_mode = 2
|
||||
|
||||
[node name="CharacterPawn3D" type="CharacterBody3D"]
|
||||
script = ExtResource("1_4frsu")
|
||||
metadata/_custom_type_script = "uid://cdmmiixa75f3x"
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
|
||||
shape = SubResource("CapsuleShape3D_6vm80")
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("CapsuleMesh_6vm80")
|
||||
|
||||
[node name="CameraAnchor" type="Marker3D" parent="."]
|
||||
|
||||
[node name="CameraPivot" type="Node3D" parent="."]
|
||||
physics_interpolation_mode = 1
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.75, 0)
|
||||
top_level = true
|
||||
|
||||
[node name="SpringArm" type="SpringArm3D" parent="CameraPivot"]
|
||||
spring_length = 3.0
|
||||
|
||||
[node name="Camera3D" type="Camera3D" parent="CameraPivot/SpringArm"]
|
||||
|
||||
[node name="GripDetector" type="Area3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -1)
|
||||
collision_layer = 0
|
||||
collision_mask = 32768
|
||||
monitorable = false
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="GripDetector"]
|
||||
shape = SubResource("SphereShape3D_gnddn")
|
||||
|
||||
[node name="ZeroGMovementComponent" type="Node3D" parent="."]
|
||||
script = ExtResource("4_8jhjh")
|
||||
metadata/_custom_type_script = "uid://y3vo40i16ek3"
|
||||
|
||||
[node name="EVAMovementComponent" parent="." instance=ExtResource("3_gnddn")]
|
||||
|
||||
[node name="MultiplayerSynchronizer" type="MultiplayerSynchronizer" parent="."]
|
||||
replication_config = SubResource("SceneReplicationConfig_gnddn")
|
||||
|
||||
[node name="PlayerController3d" parent="." instance=ExtResource("4_bcy3l")]
|
||||
204
scenes/tests/3d/eva_movement_component.gd
Normal file
204
scenes/tests/3d/eva_movement_component.gd
Normal file
@ -0,0 +1,204 @@
|
||||
# eva_suit_controller.gd
|
||||
extends Node # Or Node3D if thrusters need specific positions later
|
||||
class_name EVAMovementComponent
|
||||
|
||||
## References (Set automatically in _ready)
|
||||
var pawn: CharacterPawn3D
|
||||
|
||||
## EVA Parameters (Moved from ZeroGPawn)
|
||||
@export var orientation_speed: float = 2.0 # Used for orienting body to camera
|
||||
@export var move_speed: float = 2.0
|
||||
@export var roll_torque: float = 2.5
|
||||
@export var angular_damping: float = 0.95 # Base damping applied by pawn, suit might add more?
|
||||
@export var inertia_multiplier: float = 1.0 # How much the suit adds to pawn's base inertia (placeholder)
|
||||
@export var stabilization_kp: float = 5.0
|
||||
@export var stabilization_kd: float = 1.0
|
||||
|
||||
## State
|
||||
var stabilization_target: Node3D = null
|
||||
var stabilization_enabled: bool = false
|
||||
|
||||
func _ready():
|
||||
pawn = get_parent() as CharacterPawn3D
|
||||
if not pawn:
|
||||
printerr("EVAMovementComponent must be a child of a CharacterBody3D pawn.")
|
||||
return
|
||||
# Make sure the paths match your CharacterPawn scene structure
|
||||
# if camera_anchor:
|
||||
# camera = camera_anchor.get_node_or_null("SpringArm/Camera3D") # Adjusted path for SpringArm
|
||||
|
||||
# if not camera_anchor or not camera:
|
||||
# printerr("EVAMovementComponent could not find CameraPivot/SpringArm/Camera3D on pawn.")
|
||||
|
||||
# --- Standardized Movement API ---
|
||||
|
||||
## Called by Pawn's _physics_process when in FLOATING state with suit equipped
|
||||
func process_movement(delta: float, move_input: Vector2, vertical_input: float, roll_input: float, orienting_input: PlayerController3D.KeyInput):
|
||||
var orienting = orienting_input.held
|
||||
if not is_instance_valid(pawn): return
|
||||
if orienting:
|
||||
_orient_pawn(delta)
|
||||
|
||||
# Check if stabilization is active and handle it first
|
||||
if stabilization_enabled and is_instance_valid(stabilization_target):
|
||||
_apply_stabilization_torques(delta)
|
||||
else:
|
||||
# Apply regular movement/torque only if not stabilizing
|
||||
_apply_floating_movement(delta, move_input, vertical_input, roll_input)
|
||||
|
||||
func apply_thrusters(pawn: CharacterPawn3D, delta: float, move_input: Vector2, vertical_input: float, roll_input: float):
|
||||
if not is_instance_valid(pawn): return
|
||||
|
||||
# Apply Linear Velocity
|
||||
var pawn_forward = -pawn.global_basis.z
|
||||
var pawn_right = pawn.global_basis.x
|
||||
var pawn_up = pawn.global_basis.y
|
||||
var move_dir_horizontal = (pawn_forward * move_input.y + pawn_right * move_input.x)
|
||||
var move_dir_vertical = pawn_up * vertical_input
|
||||
var combined_move_dir = move_dir_horizontal + move_dir_vertical
|
||||
|
||||
if combined_move_dir != Vector3.ZERO:
|
||||
pawn.velocity += combined_move_dir.normalized() * move_speed * delta
|
||||
|
||||
# Apply Roll Torque
|
||||
var roll_torque_global = -pawn.global_basis.z * (roll_input) * roll_torque # Sign fixed
|
||||
pawn.add_torque(roll_torque_global, delta)
|
||||
|
||||
## Called by Pawn to handle collision response in FLOATING state
|
||||
func handle_collision(collision: KinematicCollision3D, collision_energy_loss: float):
|
||||
if not is_instance_valid(pawn): return
|
||||
var surface_normal = collision.get_normal()
|
||||
var reflected_velocity = pawn.velocity.bounce(surface_normal)
|
||||
reflected_velocity *= (1.0 - collision_energy_loss)
|
||||
pawn.velocity = reflected_velocity # Update pawn's velocity directly
|
||||
|
||||
## Called by Pawn when entering FLOATING state with suit
|
||||
func on_enter_state():
|
||||
print("EVA Suit Engaged")
|
||||
# Any specific setup needed when activating the suit
|
||||
|
||||
## Called by Pawn when exiting FLOATING state with suit
|
||||
func on_exit_state():
|
||||
print("EVA Suit Disengaged")
|
||||
# Any cleanup needed when deactivating the suit (e.g., stop thruster effects)
|
||||
|
||||
# --- Internal Logic ---
|
||||
|
||||
func _apply_floating_movement(delta: float, move_input: Vector2, vertical_input: float, roll_input: float):
|
||||
# Apply Linear Velocity
|
||||
var pawn_forward = -pawn.global_basis.z
|
||||
var pawn_right = pawn.global_basis.x # Use pawn's right for consistency
|
||||
var pawn_up = pawn.global_basis.y
|
||||
var move_dir_horizontal = (pawn_forward * move_input.y + pawn_right * move_input.x)
|
||||
var move_dir_vertical = pawn_up * vertical_input
|
||||
var combined_move_dir = move_dir_horizontal + move_dir_vertical
|
||||
|
||||
if combined_move_dir != Vector3.ZERO:
|
||||
pawn.velocity += combined_move_dir.normalized() * move_speed * delta
|
||||
# --- Apply Roll Torque ---
|
||||
# Calculate torque magnitude based on input
|
||||
var roll_torque_vector = pawn.transform.basis.z * (-roll_input) * roll_torque
|
||||
|
||||
# Apply the global torque vector using the pawn's helper function
|
||||
pawn.add_torque(roll_torque_vector, delta)
|
||||
|
||||
|
||||
# --- Auto-Orientation Logic ---
|
||||
func _orient_pawn(delta: float):
|
||||
# 1. Determine Target Orientation Basis
|
||||
var initial_cam_basis = pawn.camera_anchor.global_basis
|
||||
var target_forward = -pawn.camera_anchor.global_basis.z # Look where camera looks
|
||||
var target_up = Vector3.UP # Default up initially
|
||||
|
||||
# --- THE FIX: Adjust how target_up is calculated ---
|
||||
# Calculate velocity components relative to camera orientation
|
||||
var _forward_velocity_component = pawn.velocity.dot(target_forward)
|
||||
var _right_velocity_component = pawn.velocity.dot(pawn.camera_anchor.global_basis.x)
|
||||
|
||||
# Only apply strong "feet trailing" if significant forward/backward movement dominates
|
||||
# and we are actually moving.
|
||||
#if abs(forward_velocity_component) > abs(right_velocity_component) * 0.5 and velocity.length_squared() > 0.1:
|
||||
#target_up = -velocity.normalized()
|
||||
## Orthogonalize to prevent basis skew
|
||||
#var target_right = target_up.cross(target_forward).normalized()
|
||||
## If vectors are parallel, cross product is zero. Fallback needed.
|
||||
#if target_right.is_zero_approx():
|
||||
#target_up = transform.basis.y # Fallback to current up
|
||||
#else:
|
||||
#target_up = target_forward.cross(target_right).normalized()
|
||||
#else:
|
||||
## If primarily strafing or stationary relative to forward,
|
||||
## maintain the current body's roll orientation (its local Y-axis).
|
||||
target_up = pawn.transform.basis.y
|
||||
|
||||
# Create the target basis
|
||||
var target_basis = Basis.looking_at(target_forward, target_up)
|
||||
|
||||
# Optional Pitch Offset (Experimental):
|
||||
# Apply the desired 70-degree pitch relative to the forward direction
|
||||
# var target_pitch_rad = deg_to_rad(target_body_pitch_degrees)
|
||||
# target_basis = target_basis.rotated(target_basis.x, target_pitch_rad) # Rotate around the target right vector
|
||||
|
||||
# 2. Smoothly Interpolate Towards Target Basis
|
||||
var current_basis = pawn.global_basis
|
||||
var new_basis = current_basis.slerp(target_basis, delta * orientation_speed).get_rotation_quaternion()
|
||||
|
||||
# Store the body's yaw *before* applying the new basis
|
||||
var _old_body_yaw = current_basis.get_euler().y
|
||||
var _old_body_pitch = current_basis.get_euler().x
|
||||
|
||||
# 3. Apply the new orientation
|
||||
pawn.global_basis = new_basis
|
||||
|
||||
# 4. Reset camera pivot to rotation to what it was before we rotated the parent
|
||||
pawn.camera_anchor.global_basis = initial_cam_basis
|
||||
|
||||
# --- Add new function placeholder ---
|
||||
# TODO: Implement Rotation Stabilization Logic
|
||||
func _apply_stabilization_torques(_delta: float):
|
||||
if not is_instance_valid(stabilization_target):
|
||||
stabilization_enabled = false
|
||||
return
|
||||
|
||||
# TODO: Get the angular velocity from suit readings
|
||||
var angular_velocity = Vector3.ZERO
|
||||
# 1. Calculate Relative Angular Velocity:
|
||||
# - Get the target's angular velocity (if it rotates) or assume zero.
|
||||
# - Find the difference between our angular_velocity and the target's.
|
||||
var relative_angular_velocity = angular_velocity # - target_angular_velocity (if applicable)
|
||||
|
||||
# 2. Calculate Target Orientation (Optional - for full orientation lock):
|
||||
# - Determine the desired orientation relative to the target (e.g., face towards it).
|
||||
# - Calculate the rotational error (e.g., using Quaternions or angle differences).
|
||||
var rotational_error = Vector3.ZERO # Placeholder for difference from desired orientation
|
||||
|
||||
# 3. Calculate Corrective Torque (PD Controller):
|
||||
# - Proportional Term (based on orientation error): P = rotational_error * stabilization_kp
|
||||
# - Derivative Term (based on relative spin): D = relative_angular_velocity * stabilization_kd
|
||||
# - Required Torque = -(P + D) # Negative to counteract error/spin
|
||||
var required_torque = -(rotational_error * stabilization_kp + relative_angular_velocity * stabilization_kd)
|
||||
|
||||
print("Applying stabilization torque: ", required_torque)
|
||||
# 4. Convert Required Torque into Thruster Actions:
|
||||
# - This is the complex part. Need to know thruster positions, directions, and forces.
|
||||
# - Determine which thrusters (likely RCS on the jetpack) can produce the required_torque.
|
||||
# - Calculate firing times/intensities for those thrusters.
|
||||
# - Apply the forces/torques (similar to how _apply_floating_movement applies roll torque).
|
||||
# Example (highly simplified, assumes direct torque application possible):
|
||||
|
||||
# angular_velocity += (required_torque / inertia) * delta
|
||||
|
||||
# --- Add methods for enabling/disabling stabilization, setting target etc. ---
|
||||
func set_stabilization_enabled(enable: bool):
|
||||
if enable and is_instance_valid(stabilization_target):
|
||||
stabilization_enabled = true
|
||||
print("EVA Suit Stabilization Enabled")
|
||||
else:
|
||||
stabilization_enabled = false
|
||||
print("EVA Suit Stabilization Disabled")
|
||||
|
||||
func set_stabilization_target(target: Node3D):
|
||||
stabilization_target = target
|
||||
|
||||
func toggle_stabilization():
|
||||
set_stabilization_enabled(not stabilization_enabled)
|
||||
1
scenes/tests/3d/eva_movement_component.gd.uid
Normal file
1
scenes/tests/3d/eva_movement_component.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://d4jka2etva22s
|
||||
70
scenes/tests/3d/grips/grip_area_3d.gd
Normal file
70
scenes/tests/3d/grips/grip_area_3d.gd
Normal file
@ -0,0 +1,70 @@
|
||||
extends Area3D
|
||||
class_name GripArea3D
|
||||
|
||||
## Signal emitted when a pawn successfully grabs this grip.
|
||||
signal grabbed(pawn: CharacterPawn3D)
|
||||
## Signal emitted when a pawn releases this grip.
|
||||
signal released(pawn: CharacterPawn3D)
|
||||
|
||||
## Enum to differentiate grip types. Add more as needed.
|
||||
enum GripType { POINT, BAR, LADDER_RUNG, SEAT_HANDLE }
|
||||
|
||||
## The type of this specific grip. Should be set by inheriting classes.
|
||||
@export var grip_type: GripType
|
||||
|
||||
## Can more than one pawn grab this simultaneously? (Usually false)
|
||||
@export var allow_multiple_occupants: bool = false
|
||||
|
||||
## Tracks the pawn currently holding this grip.
|
||||
var occupant: CharacterPawn3D = null
|
||||
|
||||
# --- Virtual Methods (to be overridden by subclasses) ---
|
||||
|
||||
## Calculates the ideal global transform (position and orientation)
|
||||
## the pawn should aim for when grabbing *this specific* grip.
|
||||
## The pawn_global_pos helps determine the closest point on extended grips like bars.
|
||||
func get_grip_transform(_pawn_global_pos: Vector3) -> Transform3D:
|
||||
# Default implementation: return the grip's own global transform.
|
||||
# Subclasses (like BarGrip) MUST override this for correct behavior.
|
||||
return global_transform
|
||||
|
||||
## Determines the direction the pawn should push off from this grip.
|
||||
## Usually the opposite of the surface normal or grip's forward direction.
|
||||
func get_push_off_normal() -> Vector3:
|
||||
# Default: Push directly away along the grip's local -Z axis (assuming Z points into the wall)
|
||||
return -global_transform.basis.z.normalized()
|
||||
|
||||
# --- Public API ---
|
||||
|
||||
## Check if a specific pawn *can* grab this grip right now.
|
||||
func can_grab(pawn: CharacterPawn3D) -> bool:
|
||||
if not is_instance_valid(pawn):
|
||||
return false
|
||||
if occupant != null and not allow_multiple_occupants:
|
||||
return false # Already occupied
|
||||
# Add distance checks or other conditions if needed
|
||||
return true
|
||||
|
||||
## Called by the pawn to initiate grabbing.
|
||||
func grab(pawn: CharacterPawn3D):
|
||||
if can_grab(pawn):
|
||||
occupant = pawn
|
||||
emit_signal("grabbed", pawn)
|
||||
# Disable collision? Monitor input? Subclasses might override.
|
||||
print(pawn.name, " grabbed ", name)
|
||||
return true
|
||||
return false
|
||||
|
||||
## Called by the pawn to release the grip.
|
||||
func release(pawn: CharacterPawn3D):
|
||||
if occupant == pawn:
|
||||
var released_pawn = occupant
|
||||
occupant = null
|
||||
emit_signal("released", released_pawn)
|
||||
print(pawn.name, " released ", name)
|
||||
# Re-enable collision?
|
||||
return true
|
||||
return false
|
||||
|
||||
func is_occupied() -> bool:
|
||||
return occupant != null
|
||||
1
scenes/tests/3d/grips/grip_area_3d.gd.uid
Normal file
1
scenes/tests/3d/grips/grip_area_3d.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cnt6griexrsaa
|
||||
64
scenes/tests/3d/grips/single_handhold.tscn
Normal file
64
scenes/tests/3d/grips/single_handhold.tscn
Normal file
@ -0,0 +1,64 @@
|
||||
[gd_scene load_steps=6 format=3 uid="uid://5noqmp8b267n"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://cnt6griexrsaa" path="res://scenes/tests/3d/grips/grip_area_3d.gd" id="1_c81dj"]
|
||||
|
||||
[sub_resource type="CylinderMesh" id="CylinderMesh_c81dj"]
|
||||
top_radius = 0.02
|
||||
bottom_radius = 0.02
|
||||
height = 0.2
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_c81dj"]
|
||||
height = 0.2
|
||||
radius = 0.02
|
||||
|
||||
[sub_resource type="CylinderMesh" id="CylinderMesh_hnq38"]
|
||||
top_radius = 0.01
|
||||
bottom_radius = 0.01
|
||||
height = 0.1
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_hnq38"]
|
||||
height = 0.1
|
||||
radius = 0.01
|
||||
|
||||
[node name="SingleHandhold" type="Node3D"]
|
||||
|
||||
[node name="Grip" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -0.1)
|
||||
collision_layer = 32769
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Grip"]
|
||||
mesh = SubResource("CylinderMesh_c81dj")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Grip"]
|
||||
shape = SubResource("CylinderShape3D_c81dj")
|
||||
|
||||
[node name="Spoke" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, 0.0801313, -0.05)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Spoke"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.01, 0)
|
||||
mesh = SubResource("CylinderMesh_hnq38")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="Spoke"]
|
||||
shape = SubResource("CylinderShape3D_hnq38")
|
||||
|
||||
[node name="Spoke2" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, -4.37114e-08, 1, 0, -1, -4.37114e-08, 0, -0.08, -0.05)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Spoke2"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.01, 0)
|
||||
mesh = SubResource("CylinderMesh_hnq38")
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="Spoke2"]
|
||||
shape = SubResource("CylinderShape3D_hnq38")
|
||||
|
||||
[node name="GripArea3D" type="Area3D" parent="."]
|
||||
transform = Transform3D(4.37114e-08, -1, -2.62268e-07, -1, -4.37114e-08, -7.64275e-15, 0, 2.62268e-07, -1, 0, 0, -0.1)
|
||||
collision_layer = 32768
|
||||
collision_mask = 0
|
||||
monitoring = false
|
||||
script = ExtResource("1_c81dj")
|
||||
metadata/_custom_type_script = "uid://cnt6griexrsaa"
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="GripArea3D"]
|
||||
shape = SubResource("CylinderShape3D_c81dj")
|
||||
93
scenes/tests/3d/player_controller_3d.gd
Normal file
93
scenes/tests/3d/player_controller_3d.gd
Normal file
@ -0,0 +1,93 @@
|
||||
# PlayerController3D.gd
|
||||
extends Node
|
||||
class_name PlayerController3D
|
||||
|
||||
@onready var possessed_pawn: CharacterPawn3D = get_parent()
|
||||
|
||||
# --- Mouse Sensitivity ---
|
||||
@export var mouse_sensitivity: float = 0.002 # Radians per pixel motion
|
||||
var _mouse_motion_input: Vector2 = Vector2.ZERO
|
||||
|
||||
class KeyInput:
|
||||
var pressed: bool = false
|
||||
var held: bool = false
|
||||
var released: bool = false
|
||||
|
||||
func _init(_p: bool, _h: bool, _r: bool):
|
||||
pressed = _p
|
||||
held = _h
|
||||
released = _r
|
||||
|
||||
func _unhandled_input(event: InputEvent):
|
||||
if not is_multiplayer_authority() or not is_instance_valid(possessed_pawn):
|
||||
# print("Peer ID: %s, Node Authority: %s" % [multiplayer.get_unique_id(), get_multiplayer_authority()])
|
||||
return
|
||||
|
||||
# Handle mouse motion input directly here
|
||||
if event is InputEventMouseMotion and Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
|
||||
_mouse_motion_input += Vector2(event.relative.x, -event.relative.y)
|
||||
|
||||
func _physics_process(_delta):
|
||||
if not is_multiplayer_authority() or not is_instance_valid(possessed_pawn):
|
||||
return
|
||||
|
||||
if _mouse_motion_input != Vector2.ZERO:
|
||||
# Calculate yaw and pitch based on mouse movement
|
||||
var sensitivity_modified_mouse_input = Vector2(_mouse_motion_input.x, _mouse_motion_input.y) * mouse_sensitivity
|
||||
|
||||
# Send rotation input via RPC immediately
|
||||
server_process_rotation_input.rpc_id(multiplayer.get_unique_id(), sensitivity_modified_mouse_input)
|
||||
|
||||
# Reset the buffer
|
||||
_mouse_motion_input = Vector2.ZERO
|
||||
|
||||
|
||||
var move_vec = Input.get_vector("move_left_3d", "move_right_3d", "move_backward_3d", "move_forward_3d")
|
||||
var roll_input = Input.get_action_strength("roll_right_3d") - Input.get_action_strength("roll_left_3d")
|
||||
var vertical_input = Input.get_action_strength("move_up_3d") - Input.get_action_strength("move_down_3d")
|
||||
var interact_input = KeyInput.new(Input.is_action_just_pressed("spacebar_3d"), Input.is_action_pressed("spacebar_3d"), Input.is_action_just_released("spacebar_3d"))
|
||||
|
||||
var l_input = KeyInput.new(Input.is_action_just_pressed("left_click"), Input.is_action_pressed("left_click"), Input.is_action_just_released("left_click"))
|
||||
var r_input = KeyInput.new(Input.is_action_just_pressed("right_click"), Input.is_action_pressed("right_click"), Input.is_action_just_released("right_click"))
|
||||
|
||||
server_process_movement_input.rpc_id(multiplayer.get_unique_id(), move_vec, roll_input, vertical_input)
|
||||
server_process_interaction_input.rpc_id(multiplayer.get_unique_id(), interact_input)
|
||||
server_process_clicks.rpc_id(multiplayer.get_unique_id(), l_input, r_input)
|
||||
|
||||
@rpc("authority", "call_local")
|
||||
func server_process_movement_input(move: Vector2, roll: float, vertical: float):
|
||||
if is_instance_valid(possessed_pawn):
|
||||
possessed_pawn.set_movement_input(move, roll, vertical)
|
||||
|
||||
@rpc("authority", "call_local")
|
||||
func server_process_interaction_input(interact_input: KeyInput):
|
||||
if is_instance_valid(possessed_pawn):
|
||||
possessed_pawn.set_interaction_input(interact_input)
|
||||
|
||||
@rpc("authority", "call_local")
|
||||
func server_process_rotation_input(input: Vector2):
|
||||
if is_instance_valid(possessed_pawn):
|
||||
possessed_pawn.set_rotation_input(input)
|
||||
|
||||
@rpc("authority", "call_local")
|
||||
func server_process_clicks(l_action: KeyInput, r_action: KeyInput):
|
||||
if is_instance_valid(possessed_pawn):
|
||||
possessed_pawn.set_click_input(l_action, r_action)
|
||||
|
||||
func possess(pawn_to_control: CharacterPawn3D):
|
||||
possessed_pawn = pawn_to_control
|
||||
|
||||
#print("PlayerController3D %d possessed: %s" % [multiplayer.get_unique_id(), possessed_pawn.name])
|
||||
|
||||
# Optional: Release mouse when losing focus
|
||||
func _notification(what):
|
||||
match what:
|
||||
NOTIFICATION_WM_WINDOW_FOCUS_OUT:
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
|
||||
NOTIFICATION_WM_WINDOW_FOCUS_IN:
|
||||
Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
|
||||
NOTIFICATION_EXIT_TREE:
|
||||
print("PlayerController %s exited tree" % multiplayer.get_unique_id())
|
||||
NOTIFICATION_ENTER_TREE:
|
||||
print("PlayerController %s entered tree" % multiplayer.get_unique_id())
|
||||
|
||||
1
scenes/tests/3d/player_controller_3d.gd.uid
Normal file
1
scenes/tests/3d/player_controller_3d.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://vjfk3xnapfti
|
||||
6
scenes/tests/3d/player_controller_3d.tscn
Normal file
6
scenes/tests/3d/player_controller_3d.tscn
Normal file
@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://ba3ijdstp2bvt"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://vjfk3xnapfti" path="res://scenes/tests/3d/player_controller_3d.gd" id="1_elh6f"]
|
||||
|
||||
[node name="PlayerController3d" type="Node"]
|
||||
script = ExtResource("1_elh6f")
|
||||
163
scenes/tests/3d/zero_g_3d_test.tscn
Normal file
163
scenes/tests/3d/zero_g_3d_test.tscn
Normal file
@ -0,0 +1,163 @@
|
||||
[gd_scene load_steps=7 format=3 uid="uid://ddfsn0rtdnfda"]
|
||||
|
||||
[ext_resource type="PackedScene" uid="uid://5noqmp8b267n" path="res://scenes/tests/3d/grips/single_handhold.tscn" id="1_jlvj7"]
|
||||
[ext_resource type="PackedScene" uid="uid://dvpy3urgtm62n" path="res://scenes/ship/components/hardware/spawner.tscn" id="2_jlvj7"]
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_kateb"]
|
||||
size = Vector3(50, 1, 50)
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_25xtv"]
|
||||
size = Vector3(50, 1, 50)
|
||||
|
||||
[sub_resource type="CylinderMesh" id="CylinderMesh_nvgim"]
|
||||
top_radius = 4.0
|
||||
bottom_radius = 4.0
|
||||
height = 50.0
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_nvgim"]
|
||||
height = 50.0
|
||||
radius = 4.0
|
||||
|
||||
[node name="ZeroG3DTest" type="Node3D"]
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="."]
|
||||
|
||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="."]
|
||||
transform = Transform3D(0.743413, -0.535317, 0.400964, 0, 0.599499, 0.800376, -0.668832, -0.59501, 0.445675, 0, 0, 0)
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -25, 0)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D"]
|
||||
mesh = SubResource("BoxMesh_kateb")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D"]
|
||||
shape = SubResource("BoxShape3D_25xtv")
|
||||
|
||||
[node name="StaticBody3D2" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 25, 0)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D2"]
|
||||
mesh = SubResource("BoxMesh_kateb")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D2"]
|
||||
shape = SubResource("BoxShape3D_25xtv")
|
||||
|
||||
[node name="StaticBody3D3" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 25, 0, 0)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D3"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.050821304, 0.029743195, -0.014732361)
|
||||
mesh = SubResource("BoxMesh_kateb")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D3"]
|
||||
shape = SubResource("BoxShape3D_25xtv")
|
||||
|
||||
[node name="StaticBody3D4" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, -25, 0, 0)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D4"]
|
||||
mesh = SubResource("BoxMesh_kateb")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D4"]
|
||||
shape = SubResource("BoxShape3D_25xtv")
|
||||
|
||||
[node name="StaticBody3D5" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(1.91069e-15, 4.37114e-08, 1, 1, -4.37114e-08, 0, 4.37114e-08, 1, -4.37114e-08, 0, 0, 25)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D5"]
|
||||
mesh = SubResource("BoxMesh_kateb")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D5"]
|
||||
shape = SubResource("BoxShape3D_25xtv")
|
||||
|
||||
[node name="StaticBody3D6" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(1.91069e-15, 4.37114e-08, 1, 1, -4.37114e-08, 0, 4.37114e-08, 1, -4.37114e-08, 0, 0, -25)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D6"]
|
||||
mesh = SubResource("BoxMesh_kateb")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D6"]
|
||||
shape = SubResource("BoxShape3D_25xtv")
|
||||
|
||||
[node name="SpotLight3D" type="SpotLight3D" parent="."]
|
||||
transform = Transform3D(-0.769143, -0.409482, -0.490656, 0, 0.767758, -0.64074, 0.639077, -0.492821, -0.590516, -0.470994, -0.615063, -0.56685)
|
||||
spot_range = 50.0
|
||||
spot_angle = 61.19
|
||||
|
||||
[node name="StaticBody3D7" type="StaticBody3D" parent="."]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D7"]
|
||||
mesh = SubResource("CylinderMesh_nvgim")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D7"]
|
||||
shape = SubResource("CylinderShape3D_nvgim")
|
||||
|
||||
[node name="StaticBody3D8" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(-4.37114e-08, -1, 0, 1, -4.37114e-08, 0, 0, 0, 1, 0, 0, 0)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D8"]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.016562464, 0.05784607, -0.0040130615)
|
||||
mesh = SubResource("CylinderMesh_nvgim")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D8"]
|
||||
transform = Transform3D(-0.999991, 0.00434579, 0, -0.00434579, -0.999991, 0, 0, 0, 1, 0, 0, 0)
|
||||
shape = SubResource("CylinderShape3D_nvgim")
|
||||
|
||||
[node name="SingleHandhold" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.0698468, -16.9997, -4)
|
||||
|
||||
[node name="SingleHandhold8" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.0698468, -16.9997, -4)
|
||||
|
||||
[node name="SingleHandhold3" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.065501, -15.9997, -4)
|
||||
|
||||
[node name="SingleHandhold4" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.0611551, -14.9997, -4)
|
||||
|
||||
[node name="SingleHandhold5" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.0611551, -14.9997, -4)
|
||||
|
||||
[node name="SingleHandhold6" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.0568093, -13.9997, -4)
|
||||
|
||||
[node name="SingleHandhold7" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.0524635, -12.9997, -4)
|
||||
|
||||
[node name="SingleHandhold9" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.10026761, -23.99963, -4)
|
||||
|
||||
[node name="SingleHandhold10" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.10026761, -23.99963, -4)
|
||||
|
||||
[node name="SingleHandhold11" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.09592181, -22.99963, -4)
|
||||
|
||||
[node name="SingleHandhold12" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.091575906, -21.99963, -4)
|
||||
|
||||
[node name="SingleHandhold13" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.091575906, -21.99963, -4)
|
||||
|
||||
[node name="SingleHandhold14" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.08723011, -20.99963, -4)
|
||||
|
||||
[node name="SingleHandhold15" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(0.00434583, 0.99999, 0, -0.99999, 0.00434583, 0, 0, 0, 1, 0.08288431, -19.99963, -4)
|
||||
|
||||
[node name="SingleHandhold2" parent="StaticBody3D8" instance=ExtResource("1_jlvj7")]
|
||||
transform = Transform3D(-0.00434583, 0.99999, -3.79925e-10, 0.99999, 0.00434583, 8.7421995e-08, 8.742269e-08, -3.123381e-18, -1, 0.0698468, -16.9997, 4)
|
||||
|
||||
[node name="StaticBody3D9" type="StaticBody3D" parent="."]
|
||||
transform = Transform3D(0.00434584, 4.37103e-08, 0.999991, 0.999991, 0, -0.00434584, 0, 1, -4.37114e-08, 0, 0, 0)
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="StaticBody3D9"]
|
||||
mesh = SubResource("CylinderMesh_nvgim")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D9"]
|
||||
transform = Transform3D(4.33065e-08, 0.00434584, -0.999991, 4.38977e-08, -0.999991, -0.00434584, -1, -4.35214e-08, -4.37722e-08, 0, 0, 0)
|
||||
shape = SubResource("CylinderShape3D_nvgim")
|
||||
|
||||
[node name="Spawner" parent="." instance=ExtResource("2_jlvj7")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 12.309784, 0, -12.84836)
|
||||
484
scenes/tests/3d/zero_g_movement_component.gd
Normal file
484
scenes/tests/3d/zero_g_movement_component.gd
Normal file
@ -0,0 +1,484 @@
|
||||
# zero_g_move_controller.gd
|
||||
extends Node
|
||||
class_name ZeroGMovementComponent
|
||||
|
||||
## References
|
||||
var pawn: CharacterPawn3D
|
||||
var camera_pivot: Node3D
|
||||
|
||||
## State & Parameters
|
||||
var current_grip: GripArea3D = null # Use GripArea3D type hint
|
||||
var nearby_grips: Array[GripArea3D] = []
|
||||
|
||||
# --- Reach Parameters ---
|
||||
@export var reach_speed: float = 10.0 # Speed pawn moves towards grip
|
||||
@export var reach_orient_speed: float = 10.0 # Speed pawn orients to grip
|
||||
|
||||
# --- Grip damping parameters ---
|
||||
@export var gripping_linear_damping: float = 5.0 # How quickly velocity stops
|
||||
@export var gripping_angular_damping: float = 5.0 # How quickly spin stops
|
||||
@export var gripping_orient_speed: float = 2.0 # How quickly pawn rotates to face grip
|
||||
|
||||
# --- Climbing parameters ---
|
||||
@export var climb_speed: float = 2.0
|
||||
@export var grip_handover_distance: float = 1 # How close to next grip to initiate handover
|
||||
@export var climb_acceleration: float = 10.0 # How quickly pawn reaches climb_speed
|
||||
@export var climb_angle_threshold_deg: float = 120.0 # How wide the forward cone is
|
||||
@export var release_past_grip_threshold: float = 0.4 # How far past the grip origin before releasing
|
||||
var next_grip_target: GripArea3D = null # The grip we are trying to transition to
|
||||
|
||||
# --- Launch Parameters ---
|
||||
|
||||
# --- Seeking Climb State ---
|
||||
var _seeking_climb_input: Vector2 = Vector2.ZERO # The move_input held when seeking started
|
||||
|
||||
@export var launch_charge_rate: float = 20.0
|
||||
@export var max_launch_speed: float = 15.0
|
||||
var launch_direction: Vector3 = Vector3.ZERO
|
||||
var launch_charge: float = 0.0
|
||||
|
||||
# Enum for internal state
|
||||
enum MovementState {
|
||||
IDLE,
|
||||
REACHING,
|
||||
GRIPPING,
|
||||
CLIMBING,
|
||||
SEEKING_CLIMB,
|
||||
CHARGING_LAUNCH
|
||||
}
|
||||
var current_state: MovementState = MovementState.IDLE:
|
||||
set(new_state):
|
||||
if new_state == current_state: return
|
||||
_on_exit_state(current_state) # Call exit logic for old state
|
||||
current_state = new_state
|
||||
_on_enter_state(current_state) # Call enter logic for new state
|
||||
|
||||
func _ready():
|
||||
pawn = get_parent() as CharacterPawn3D
|
||||
if not pawn: printerr("ZeroGMovementComponent must be child of CharacterPawn3D")
|
||||
camera_pivot = pawn.get_node_or_null("CameraPivot")
|
||||
if not camera_pivot: printerr("ZeroGMovementComponent couldn't find CameraPivot")
|
||||
|
||||
# --- Standardized Movement API ---
|
||||
|
||||
## Called by Pawn when relevant state is active (e.g., GRABBING_GRIP, REACHING_MOVE)
|
||||
func process_movement(delta: float, move_input: Vector2, vertical_input: float, roll_input: float, reach_input: PlayerController3D.KeyInput, release_input: PlayerController3D.KeyInput):
|
||||
if not is_instance_valid(pawn): return
|
||||
|
||||
_update_state(
|
||||
delta,
|
||||
move_input,
|
||||
reach_input,
|
||||
release_input
|
||||
)
|
||||
|
||||
match current_state:
|
||||
MovementState.IDLE:
|
||||
_process_idle(delta, move_input, vertical_input, roll_input, release_input)
|
||||
MovementState.REACHING:
|
||||
_process_reaching(delta)
|
||||
MovementState.GRIPPING:
|
||||
_apply_grip_physics(delta, move_input, roll_input)
|
||||
MovementState.CLIMBING:
|
||||
_apply_climb_physics(delta, move_input)
|
||||
MovementState.SEEKING_CLIMB:
|
||||
_process_seeking_climb(delta, move_input)
|
||||
MovementState.CHARGING_LAUNCH:
|
||||
_handle_launch_charge(delta)
|
||||
|
||||
|
||||
## Called by Pawn for collision (optional, might not be needed if grabbing stops movement)
|
||||
func handle_collision(collision: KinematicCollision3D, collision_energy_loss: float):
|
||||
# Basic bounce if somehow colliding while using this controller
|
||||
var surface_normal = collision.get_normal()
|
||||
pawn.velocity = pawn.velocity.bounce(surface_normal)
|
||||
pawn.velocity *= (1.0 - collision_energy_loss * 0.5)
|
||||
|
||||
# === STATE MACHINE ===
|
||||
func _on_enter_state(state : MovementState):
|
||||
print("ZeroGMovementComponent activated for state: ", MovementState.keys()[state])
|
||||
if state == MovementState.GRIPPING:
|
||||
pawn.velocity = Vector3.ZERO
|
||||
pawn.angular_velocity = Vector3.ZERO
|
||||
# else: # e.g., REACHING_MOVE?
|
||||
# state = MovementState.IDLE # Or SEARCHING?
|
||||
|
||||
func _on_exit_state(state: MovementState):
|
||||
print("ZeroGMovementComponent deactivated for state: ", MovementState.keys()[state])
|
||||
pass
|
||||
|
||||
# Ensure grip is released if state changes unexpectedly
|
||||
#if state == MovementState.GRIPPING:
|
||||
#_release_current_grip()
|
||||
|
||||
func _update_state(
|
||||
_delta: float,
|
||||
move_input: Vector2,
|
||||
reach_input: PlayerController3D.KeyInput,
|
||||
release_input: PlayerController3D.KeyInput,
|
||||
):
|
||||
match current_state:
|
||||
MovementState.IDLE:
|
||||
# Already handled initiating reach in process_movement
|
||||
if reach_input.pressed or reach_input.held:
|
||||
current_state = MovementState.REACHING
|
||||
MovementState.REACHING:
|
||||
# TODO: If reach animation completes/hand near target -> GRIPPING
|
||||
# If interact released during reach -> CANCEL -> IDLE
|
||||
if _seeking_climb_input != Vector2.ZERO:
|
||||
# We are in a "seek-climb-reach" chain. Cancel if move input stops.
|
||||
if move_input == Vector2.ZERO:
|
||||
# This will transition state to IDLE
|
||||
_cancel_reach()
|
||||
elif not (reach_input.pressed or reach_input.held):
|
||||
# This was a normal reach initiated by click. Cancel if click is released.
|
||||
_cancel_reach()
|
||||
|
||||
MovementState.GRIPPING:
|
||||
# print("ZeroGMovementComponent: Gripping State Active")
|
||||
if release_input.pressed or release_input.held or not is_instance_valid(current_grip):
|
||||
_release_current_grip(move_input)
|
||||
return
|
||||
|
||||
# Check for launch charge *before* checking for climb, as it's a more specific action.
|
||||
if (reach_input.pressed or reach_input.held) and move_input != Vector2.ZERO:
|
||||
_start_charge(move_input)
|
||||
return
|
||||
elif move_input != Vector2.ZERO:
|
||||
_start_climb(move_input) # This is overshadowed by the above check.
|
||||
MovementState.CLIMBING:
|
||||
if reach_input.pressed or reach_input.held:
|
||||
_start_charge(move_input)
|
||||
return
|
||||
if release_input.pressed or release_input.held or not is_instance_valid(current_grip):
|
||||
_stop_climb(true) # Release grip and stop
|
||||
return
|
||||
if move_input == Vector2.ZERO: # Player stopped giving input
|
||||
_stop_climb(false) # Stop moving, return to GRIPPING
|
||||
return
|
||||
# Continue climbing logic (finding next grip) happens in _process_climbing
|
||||
MovementState.CHARGING_LAUNCH:
|
||||
if not (reach_input.pressed or reach_input.held):
|
||||
_execute_launch(move_input)
|
||||
elif move_input == Vector2.ZERO: # Cancel charge while holding interact
|
||||
current_state = MovementState.GRIPPING
|
||||
print("ZeroGMovementComponent: Cancelled Launch Charge")
|
||||
|
||||
|
||||
# === MOVEMENT PROCESSING ===
|
||||
func _process_idle(delta: float, move_input: Vector2, vertical_input: float, roll_input: float, release_input: PlayerController3D.KeyInput):
|
||||
# State is IDLE (free-floating).
|
||||
# Check for EVA suit usage.
|
||||
var has_movement_input = (move_input != Vector2.ZERO or vertical_input != 0.0 or roll_input != 0.0)
|
||||
if has_movement_input and is_instance_valid(pawn.eva_suit_component):
|
||||
# Use EVA suit
|
||||
pawn.eva_suit_component.apply_thrusters(pawn, delta, move_input, vertical_input, roll_input)
|
||||
|
||||
# Check for body orientation (if applicable)
|
||||
if release_input.held and is_instance_valid(pawn.eva_suit_component):
|
||||
pawn.eva_suit_component._orient_pawn(delta) # Use suit's orient
|
||||
|
||||
func _process_reaching(_delta: float):
|
||||
# TODO: Drive IK target towards current_grip.get_grip_transform().origin
|
||||
# TODO: Monitor distance / animation state
|
||||
# For now, we just instantly grip.
|
||||
if _seeking_climb_input != Vector2.ZERO:
|
||||
_attempt_grip(next_grip_target) # Complete the seek-reach
|
||||
else:
|
||||
_attempt_grip(_find_best_grip())
|
||||
|
||||
func _apply_grip_physics(delta: float, _move_input: Vector2, roll_input: float):
|
||||
if not is_instance_valid(pawn) or not is_instance_valid(current_grip):
|
||||
_release_current_grip(); return
|
||||
|
||||
# TODO: Later, replace step 2 and 3 with IK driving the hand bone to the target_transform.origin,
|
||||
# while the physics/orientation logic stops the main body's momentum.
|
||||
|
||||
# --- 1. Calculate Target Transform (Same as before) ---
|
||||
var grip_base_transform = current_grip.global_transform
|
||||
var target_direction = grip_base_transform.basis.z.normalized()
|
||||
var hold_distance = _get_hold_distance()
|
||||
var target_position = grip_base_transform.origin + target_direction * hold_distance
|
||||
|
||||
var target_basis = _choose_grip_orientation(grip_base_transform.basis)
|
||||
|
||||
# --- 2. Apply Linear Force (PD Controller) ---
|
||||
var error_pos = target_position - pawn.global_position
|
||||
# Simple P-controller for velocity (acts as a spring)
|
||||
var target_velocity_pos = error_pos * gripping_linear_damping # 'damping' here acts as Kp
|
||||
# Simple D-controller (damping)
|
||||
target_velocity_pos -= pawn.velocity * gripping_angular_damping # 'angular_damping' here acts as Kd
|
||||
# Apply force via acceleration
|
||||
pawn.velocity = pawn.velocity.lerp(target_velocity_pos, delta * 10.0) # Smoothly apply correction
|
||||
|
||||
# --- 3. Apply Angular Force (PD Controller) ---
|
||||
if not is_zero_approx(roll_input):
|
||||
# Manual Roll Input (applies torque)
|
||||
var roll_torque_global = pawn.global_transform.basis.z * (-roll_input) * gripping_orient_speed # Use global Z
|
||||
pawn.add_torque(roll_torque_global, delta)
|
||||
else:
|
||||
# Auto-Orient (PD Controller)
|
||||
_apply_orientation_torque(target_basis, delta)
|
||||
|
||||
func _apply_climb_physics(delta: float, move_input: Vector2):
|
||||
if not is_instance_valid(pawn) or not is_instance_valid(current_grip):
|
||||
_stop_climb(true); return
|
||||
|
||||
# 1. Calculate Climb Direction: For climbing we interpret W as up from the pawns perspective instead of forward
|
||||
var climb_direction = move_input.y * pawn.global_basis.y + move_input.x * pawn.global_basis.x
|
||||
climb_direction = climb_direction.normalized()
|
||||
|
||||
# 2. Find Next Grip
|
||||
next_grip_target = _find_best_grip(climb_direction, INF, climb_angle_threshold_deg)
|
||||
|
||||
# 3. Check for Handover: This should be more eager to mark a new grip as current than below check is to release when climbing past
|
||||
var performed_handover = _attempt_grip(next_grip_target)
|
||||
|
||||
# 4. Check for Release Past Grip (if no handover)
|
||||
if not performed_handover:
|
||||
var current_grip_pos = current_grip.global_position
|
||||
var vector_from_grip_to_pawn = pawn.global_position - current_grip_pos
|
||||
var distance_along_climb_dir = vector_from_grip_to_pawn.dot(climb_direction)
|
||||
if distance_along_climb_dir > release_past_grip_threshold: # Release threshold
|
||||
_release_current_grip(move_input)
|
||||
return # State changed to IDLE
|
||||
|
||||
# 5. Apply Movement Force
|
||||
var target_velocity = climb_direction * climb_speed
|
||||
pawn.velocity = pawn.velocity.lerp(target_velocity, delta * climb_acceleration)
|
||||
|
||||
# 6. Apply Angular Force (Auto-Orient to current grip)
|
||||
var grip_base_transform = current_grip.global_transform
|
||||
var target_basis = _choose_grip_orientation(grip_base_transform.basis)
|
||||
_apply_orientation_torque(target_basis, delta)
|
||||
|
||||
|
||||
func _process_seeking_climb(_delta: float, move_input: Vector2):
|
||||
# If the player's input has changed from what initiated the seek, cancel it.
|
||||
if not move_input.is_equal_approx(_seeking_climb_input):
|
||||
var target_grip = _find_best_grip()
|
||||
_seeking_climb_input = Vector2.ZERO # Reset for next time
|
||||
if _attempt_grip(target_grip):
|
||||
# Successfully found and grabbed a grip. The state is now GRIPPING.
|
||||
print("Seeking Climb ended, gripped new target.")
|
||||
else:
|
||||
current_state = MovementState.IDLE
|
||||
# No grip found. Transition to IDLE.
|
||||
print("Seeking Climb ended, no grip found. Reverting to IDLE.")
|
||||
|
||||
|
||||
# --- Grip Helpers
|
||||
|
||||
## The single, authoritative function for grabbing a grip.
|
||||
func _attempt_grip(target_grip: GripArea3D) -> bool:
|
||||
if not is_instance_valid(target_grip):
|
||||
return false
|
||||
|
||||
if target_grip.grab(pawn):
|
||||
# Successfully grabbed the new grip.
|
||||
var old_grip = current_grip
|
||||
if is_instance_valid(old_grip) and old_grip != target_grip:
|
||||
old_grip.release(pawn)
|
||||
|
||||
current_grip = target_grip
|
||||
next_grip_target = null
|
||||
_seeking_climb_input = Vector2.ZERO
|
||||
|
||||
# If we weren't already climbing, transition to GRIPPING state.
|
||||
if current_state != MovementState.CLIMBING:
|
||||
current_state = MovementState.GRIPPING
|
||||
|
||||
print("Successfully gripped: ", current_grip.get_parent().name)
|
||||
return true
|
||||
else:
|
||||
# Failed to grab the new grip.
|
||||
print("Failed to grip: ", target_grip.get_parent().name, " (likely occupied).")
|
||||
if current_state == MovementState.CLIMBING:
|
||||
_stop_climb(false) # Stop climbing, return to gripping previous one
|
||||
return false
|
||||
|
||||
# --- Grip Orientation Helper ---
|
||||
func _choose_grip_orientation(grip_basis: Basis) -> Basis:
|
||||
# 1. Define the two possible target orientations based on the grip.
|
||||
# Both will look away from the grip's surface (-Z).
|
||||
var look_at_dir = -grip_basis.z.normalized()
|
||||
var target_basis_up = Basis.looking_at(look_at_dir, grip_basis.y.normalized()).orthonormalized()
|
||||
var target_basis_down = Basis.looking_at(look_at_dir, -grip_basis.y.normalized()).orthonormalized()
|
||||
|
||||
# 2. Get the pawn's current orientation.
|
||||
var current_basis = pawn.global_basis
|
||||
|
||||
# 3. Compare which target orientation is "closer" to the current one.
|
||||
# We can do this by finding the angle of rotation needed to get from current to each target.
|
||||
# The quaternion dot product is related to the angle between orientations. A larger absolute dot product means a smaller angle.
|
||||
var dot_up = current_basis.get_rotation_quaternion().dot(target_basis_up.get_rotation_quaternion())
|
||||
var dot_down = current_basis.get_rotation_quaternion().dot(target_basis_down.get_rotation_quaternion())
|
||||
|
||||
# We choose the basis that results in a larger absolute dot product (smaller rotational distance).
|
||||
return target_basis_up if abs(dot_up) >= abs(dot_down) else target_basis_down
|
||||
|
||||
# --- Grip Selection Logic ---
|
||||
# Finds the best grip based on direction, distance, and angle constraints
|
||||
func _find_best_grip(direction := Vector3.ZERO, max_distance_sq := INF, angle_threshold_deg := 120.0) -> GripArea3D:
|
||||
var best_grip: GripArea3D = null
|
||||
var min_dist_sq = max_distance_sq # Start checking against max allowed distance
|
||||
|
||||
var use_direction_filter = direction != Vector3.ZERO
|
||||
var max_allowed_angle_rad = 0.0 # Initialize
|
||||
if use_direction_filter:
|
||||
# Calculate the maximum allowed angle deviation from the center direction
|
||||
max_allowed_angle_rad = deg_to_rad(angle_threshold_deg) / 2.0
|
||||
|
||||
# Iterate through all grips detected by the pawn
|
||||
for grip in nearby_grips:
|
||||
# Basic validity checks
|
||||
if not is_instance_valid(grip) or grip == current_grip or not grip.can_grab(pawn):
|
||||
continue
|
||||
|
||||
var grip_pos = grip.global_position
|
||||
# Use direction_to which automatically normalizes
|
||||
var dir_to_grip = pawn.global_position.direction_to(grip_pos)
|
||||
var dist_sq = pawn.global_position.distance_squared_to(grip_pos)
|
||||
|
||||
# Check distance first
|
||||
if dist_sq >= min_dist_sq: # Use >= because we update min_dist_sq later
|
||||
continue
|
||||
|
||||
# If using direction filter, check angle constraint
|
||||
if use_direction_filter:
|
||||
# Ensure the direction vector we compare against is normalized
|
||||
var normalized_direction = direction.normalized()
|
||||
# Calculate the dot product
|
||||
var dot = dir_to_grip.dot(normalized_direction)
|
||||
# Clamp dot product to handle potential floating-point errors outside [-1, 1]
|
||||
dot = clamp(dot, -1.0, 1.0)
|
||||
# Calculate the actual angle between the vectors in radians
|
||||
var angle_rad = acos(dot)
|
||||
|
||||
# Check if the calculated angle exceeds the maximum allowed deviation
|
||||
if angle_rad > max_allowed_angle_rad:
|
||||
# print("Grip ", grip.get_parent().name, " outside cone. Angle: ", rad_to_deg(angle_rad), " > ", rad_to_deg(max_allowed_angle_rad))
|
||||
continue # Skip this grip if it's outside the cone
|
||||
|
||||
# If it passes all filters and is closer than the previous best:
|
||||
min_dist_sq = dist_sq
|
||||
best_grip = grip
|
||||
|
||||
# if is_instance_valid(best_grip):
|
||||
# print("Best grip found: ", best_grip.get_parent().name, " at distance squared: ", min_dist_sq)
|
||||
|
||||
return best_grip
|
||||
|
||||
# --- Reaching Helpers ---
|
||||
func _get_hold_distance() -> float:
|
||||
# Use the pawn.grip_detector.position.length() method if you prefer that:
|
||||
if is_instance_valid(pawn) and is_instance_valid(pawn.grip_detector):
|
||||
return pawn.grip_detector.position.length()
|
||||
else:
|
||||
return 0.5 # Fallback distance if detector isn't set up right
|
||||
|
||||
func _release_current_grip(move_input: Vector2 = Vector2.ZERO):
|
||||
if is_instance_valid(current_grip):
|
||||
current_grip.release(pawn)
|
||||
current_grip = null
|
||||
|
||||
# If we were climbing and are still holding a climb input, start seeking.
|
||||
if move_input != Vector2.ZERO:
|
||||
current_state = MovementState.SEEKING_CLIMB
|
||||
_seeking_climb_input = move_input # Store the input that started the seek
|
||||
# print("ZeroGMovementComponent: Released grip, now SEEKING_CLIMB.")
|
||||
else:
|
||||
current_state = MovementState.IDLE
|
||||
# print("ZeroGMovementComponent: Released grip, now IDLE.")
|
||||
|
||||
|
||||
func _cancel_reach():
|
||||
# TODO: Logic to stop IK/animation if reach is cancelled mid-way
|
||||
_release_current_grip(Vector2.ZERO) # Ensure grip reference is cleared
|
||||
print("ZeroGMovementComponent: Reach cancelled.")
|
||||
|
||||
# --- Climbing Helpers ---
|
||||
func _start_climb(move_input: Vector2):
|
||||
if not is_instance_valid(current_grip): return
|
||||
current_state = MovementState.CLIMBING
|
||||
|
||||
# Calculate initial climb direction based on input relative to camera/grip
|
||||
var pawn_up = pawn.global_basis.y
|
||||
var pawn_right = pawn.global_basis.x
|
||||
|
||||
print("ZeroGMoveController: Started Climbing in direction: ", (pawn_up * move_input.y + pawn_right * move_input.x).normalized())
|
||||
|
||||
func _stop_climb(release_grip: bool):
|
||||
# print("ZeroGMoveController: Stopping Climb. Release Grip: ", release_grip)
|
||||
pawn.velocity = pawn.velocity.lerp(Vector3.ZERO, 0.5) # Apply some braking
|
||||
next_grip_target = null
|
||||
if release_grip:
|
||||
_release_current_grip() # Transitions to IDLE
|
||||
else:
|
||||
current_state = MovementState.GRIPPING # Go back to stationary gripping
|
||||
|
||||
func _apply_orientation_torque(target_basis: Basis, delta: float):
|
||||
var current_quat = pawn.global_transform.basis.get_rotation_quaternion()
|
||||
var target_quat = target_basis.get_rotation_quaternion()
|
||||
var error_quat = target_quat * current_quat.inverse()
|
||||
|
||||
# Ensure we take the shortest path for rotation. If W is negative, the
|
||||
# quaternion represents the "long way around". Negating it gives the same
|
||||
# orientation but via the shorter rotational path.
|
||||
if error_quat.w < 0: error_quat = -error_quat
|
||||
var error_angle = error_quat.get_angle()
|
||||
var error_axis = error_quat.get_axis()
|
||||
|
||||
var torque_proportional = error_axis.normalized() * error_angle * gripping_orient_speed
|
||||
var torque_derivative = -pawn.angular_velocity * gripping_angular_damping
|
||||
var total_torque_global = (torque_proportional + torque_derivative)
|
||||
pawn.add_torque(total_torque_global, delta)
|
||||
|
||||
# --- Launch helpers ---
|
||||
func _start_charge(move_input: Vector2):
|
||||
if not is_instance_valid(current_grip): return # Safety check
|
||||
current_state = MovementState.CHARGING_LAUNCH
|
||||
launch_charge = 0.0
|
||||
|
||||
# Calculate launch direction based on input and push-off normal
|
||||
# The direction is based on the pawn's current orientation, not the camera or grip.
|
||||
# This makes it feel like you're pushing off in a direction relative to your body.
|
||||
var pawn_up = pawn.global_basis.y
|
||||
var pawn_right = pawn.global_basis.x
|
||||
launch_direction = (pawn_up * move_input.y + pawn_right * move_input.x).normalized()
|
||||
|
||||
print("ZeroGMovementComponent: Charging Launch")
|
||||
|
||||
|
||||
func _handle_launch_charge(delta: float):
|
||||
launch_charge = min(launch_charge + launch_charge_rate * delta, max_launch_speed)
|
||||
pawn.velocity = Vector3.ZERO
|
||||
pawn.angular_velocity = Vector3.ZERO
|
||||
|
||||
func _execute_launch(move_input: Vector2):
|
||||
if not is_instance_valid(current_grip): return # Safety check
|
||||
pawn.velocity = launch_direction * launch_charge # Apply launch velocity to pawn
|
||||
launch_charge = 0.0
|
||||
_release_current_grip(move_input) # Release AFTER calculating direction
|
||||
|
||||
# Instead of going to IDLE, go to SEEKING_CLIMB to find the next grip.
|
||||
# The move_input that started the launch is what we'll use for the seek direction.
|
||||
# _seeking_climb_input = (pawn.global_basis.y.dot(launch_direction) * Vector2.UP) + (pawn.global_basis.x.dot(launch_direction) * Vector2.RIGHT)
|
||||
# current_state = MovementState.SEEKING_CLIMB
|
||||
print("ZeroGMovementComponent: Launched with speed ", pawn.velocity.length(), " and now SEEKING_CLIMB")
|
||||
|
||||
|
||||
# --- Signal Handlers ---
|
||||
func on_grip_area_entered(area: Area3D):
|
||||
if area is GripArea3D: # Check if the entered area is actually a GripArea3D node
|
||||
var grip = area as GripArea3D
|
||||
if not grip in nearby_grips:
|
||||
nearby_grips.append(grip)
|
||||
# print("Detected nearby grip: ", grip.get_parent().name if grip.get_parent() else "UNKNOWN") # Print parent name for context
|
||||
|
||||
func on_grip_area_exited(area: Area3D):
|
||||
if area is GripArea3D:
|
||||
var grip = area as GripArea3D
|
||||
if grip in nearby_grips:
|
||||
nearby_grips.erase(grip)
|
||||
# print("Grip out of range: ", grip.get_parent().name if grip.get_parent() else "UNKNOWN")
|
||||
1
scenes/tests/3d/zero_g_movement_component.gd.uid
Normal file
1
scenes/tests/3d/zero_g_movement_component.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://y3vo40i16ek3
|
||||
70
scenes/tests/tube_static_body.tscn
Normal file
70
scenes/tests/tube_static_body.tscn
Normal file
@ -0,0 +1,70 @@
|
||||
[gd_scene load_steps=11 format=3 uid="uid://dq0r0kk8wa414"]
|
||||
|
||||
[sub_resource type="TorusMesh" id="TorusMesh_nvgim"]
|
||||
flip_faces = true
|
||||
inner_radius = 21.0
|
||||
outer_radius = 24.0
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_5aph0"]
|
||||
points = PackedVector3Array(11.4275, 1.16637, 22.0937, 12.9507, -1.11875, 15.2381, 0, -1.11875, 20.5706, 13.7133, 1.92857, 15.2381, 15.2366, -1.88095, 19.0484, 0, 1.16637, 24.3792, 5.3343, -1.88095, 23.6176, 18.2832, 1.92857, 16.0014, 0, 1.92857, 21.3321, 18.2832, -1.88095, 15.2381, 5.3343, 1.92857, 23.6176, 0, -1.88095, 21.3321, 7.62017, -1.11875, 23.6176, 0, -1.11875, 24.3792, 15.2366, -1.11875, 19.8091, 0, 1.16637, 20.5706, 13.7133, -1.88095, 15.2381, 19.0476, -1.11875, 16.0014, 15.9992, 1.16637, 19.0484, 4.57173, 0.404538, 24.3792, 12.9507, 1.92857, 20.5706, 19.0476, 1.16637, 15.2381, 9.90417, -1.11875, 22.8561, 7.62017, 1.16637, 23.6176, 0, -1.88095, 23.6176, 0, 1.92857, 23.6176, 12.9507, 1.16637, 15.2381, 4.57173, -1.11875, 24.3792, 19.0476, 1.16637, 16.0014, 8.38274, -1.88095, 22.8561, 12.9507, -1.11875, 21.3321, 8.38274, 1.92857, 22.8561)
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_iayf5"]
|
||||
points = PackedVector3Array(13.7153, -0.356919, 15.2381, 24.3799, 0.404538, 4.57173, 24.3799, -1.11875, 4.57173, 20.5703, -1.11875, 0, 21.3324, 1.92857, 0, 19.0481, 1.92857, 15.2381, 19.0481, -1.88095, 15.2381, 15.2396, 1.92857, 13.7137, 23.6178, -1.88095, 0, 14.4775, -1.88095, 14.4759, 23.6178, 1.92857, 0, 22.8557, 1.16637, 9.90267, 13.7153, 0.404538, 14.4759, 21.3324, -1.11875, 12.9515, 20.5703, 1.16637, 0, 22.8557, -1.88095, 8.37827, 23.6178, 1.92857, 5.33393, 21.3324, -1.88095, 0, 24.3799, 1.16637, 0, 15.2396, 1.92857, 15.2381, 19.8082, 0.404538, 15.2381, 22.8557, -1.11875, 9.90267, 13.7153, -0.356919, 14.4759, 24.3799, -1.11875, 0, 14.4775, -1.88095, 15.2381, 22.8557, 1.92857, 8.37827, 23.6178, 1.16637, 7.61905, 22.0935, 1.16637, 11.4271, 23.6178, -1.88095, 5.33393, 24.3799, 1.16637, 3.80952, 19.8082, -1.11875, 15.2381, 23.6178, -1.11875, 7.61905)
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_ei2hu"]
|
||||
points = PackedVector3Array(-3.80952, 1.16637, 24.3799, -15.2381, -0.356919, 13.7153, -15.2381, 0.404538, 13.7153, 0, -1.11875, 20.5703, -12.9515, -1.88095, 20.5703, -15.2381, 1.92857, 19.0481, 0, 1.92857, 21.3324, 0, -1.88095, 23.6178, -15.2381, -1.88095, 14.4775, -13.7137, 1.92857, 15.2396, -9.90267, -1.11875, 22.8557, 0, 1.92857, 23.6178, -15.2381, -1.11875, 19.8082, -9.90267, 1.16637, 22.8557, 0, 1.16637, 20.5703, -4.57173, -1.11875, 24.3799, 0, -1.88095, 21.3324, -8.37827, 1.92857, 22.8557, -14.4759, -0.356919, 13.7153, 0, -1.11875, 24.3799, -5.33393, -1.88095, 23.6178, -14.4759, -1.88095, 14.4775, -14.4759, 0.404538, 13.7153, -15.2381, 1.92857, 15.2396, -12.9515, 0.404538, 21.3324, -15.2381, -1.88095, 19.0481, 0, 1.16637, 24.3799, -7.61905, 1.16637, 23.6178, -5.33393, 1.92857, 23.6178, -8.37827, -1.88095, 22.8557, -12.9515, -1.11875, 21.3324, -15.2381, 0.404538, 19.8082)
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_7k4ip"]
|
||||
points = PackedVector3Array(-24.3792, 0.404538, 4.57173, -15.2381, 1.16637, 19.0476, -15.2381, -1.11875, 19.0476, -20.5706, 1.16637, 0, -21.3321, -1.88095, 0, -20.5706, -1.88095, 12.9507, -15.2381, -1.88095, 13.7133, -20.5706, 1.92857, 12.9507, -15.2381, 1.92857, 13.7133, -23.6176, 1.92857, 0, -23.6176, -1.88095, 0, -22.8561, -1.11875, 9.90417, -19.8091, 0.404538, 15.2366, -22.8561, 1.16637, 9.90417, -16.0014, -1.88095, 18.2832, -23.6176, -1.88095, 5.3343, -16.0014, 1.92857, 18.2832, -21.3321, 1.92857, 0, -20.5706, -1.11875, 0, -15.2381, -1.11875, 12.9507, -23.6176, 1.92857, 5.3343, -24.3792, -1.11875, 0, -19.8091, -1.11875, 15.2366, -16.0014, 1.16637, 19.0476, -24.3792, 1.16637, 0, -24.3792, -1.11875, 4.57173, -15.2381, 1.16637, 12.9507, -16.0014, -1.11875, 19.0476, -19.0484, 1.16637, 15.9992, -21.3321, -1.11875, 12.9507, -23.6176, 1.16637, 7.62017, -22.8561, -1.88095, 8.38274)
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_ehkdd"]
|
||||
points = PackedVector3Array(18.2869, -1.88095, -15.9992, 24.3792, 1.16637, 0, 24.3792, -1.11875, 0, 15.2381, -1.11875, -12.9507, 16.0014, 1.92857, -18.2832, 20.5706, 1.16637, 0, 22.8561, 1.16637, -9.90417, 21.3321, -1.88095, 0, 22.8561, -1.88095, -8.38274, 15.2381, 1.92857, -13.7133, 23.6176, 1.92857, 0, 15.2381, -1.88095, -18.2832, 19.8091, -1.11875, -15.2366, 20.5706, 1.92857, -12.9507, 24.3792, -1.11875, -4.57173, 23.6176, 1.92857, -5.3343, 21.3321, 1.92857, 0, 20.5706, -1.11875, 0, 15.2381, -1.88095, -13.7133, 16.0014, -1.11875, -19.0476, 19.0484, 1.16637, -15.9992, 23.6176, -1.88095, 0, 15.2381, 1.16637, -19.0476, 22.8561, -1.11875, -9.90417, 24.3792, 1.16637, -3.81102, 15.2381, 1.16637, -12.9507, 16.0014, 1.16637, -19.0476, 21.3321, 0.404538, -12.9507, 23.6176, 1.16637, -7.62017, 20.5706, -1.88095, -12.9507, 22.8561, 1.92857, -8.38274, 23.6176, -1.88095, -5.3343)
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_lr4gi"]
|
||||
points = PackedVector3Array(15.2381, 0.404538, -19.8082, 0, -1.11875, -24.3799, 0, 1.16637, -24.3799, 14.4759, -0.356919, -13.7153, 15.2381, -1.88095, -19.0481, 0, -1.88095, -21.3324, 0, 1.92857, -21.3324, 15.2381, 1.92857, -15.2396, 8.37827, 1.92857, -22.8557, 8.37827, -1.88095, -22.8557, 15.2381, -1.88095, -14.4775, 0, 1.16637, -20.5703, 7.61905, -1.11875, -23.6178, 15.2381, 1.92857, -19.0481, 0, -1.11875, -20.5703, 13.7137, 1.92857, -15.2396, 4.57173, 0.404538, -24.3799, 9.90267, 1.16637, -22.8557, 12.9515, -1.11875, -21.3324, 14.4759, 0.404538, -13.7153, 0, 1.92857, -23.6178, 0, -1.88095, -23.6178, 14.4759, -1.88095, -14.4775, 4.57173, -1.11875, -24.3799, 5.33393, 1.92857, -23.6178, 15.2381, 0.404538, -13.7153, 7.61905, 1.16637, -23.6178, 11.4271, 1.16637, -22.0935, 9.90267, -1.11875, -22.8557, 5.33393, -1.88095, -23.6178, 3.80952, 1.16637, -24.3799, 15.2381, -1.11875, -19.8082)
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_4ekbl"]
|
||||
points = PackedVector3Array(-21.3324, -1.88095, -11.4271, -13.7153, 0.404538, -14.4759, -15.2396, 1.92857, -13.7137, -20.5703, -1.11875, 0, -23.6178, 1.92857, -5.33393, -21.3324, 1.92857, 0, -19.0481, 1.92857, -15.2381, -14.4775, -1.88095, -15.2381, -23.6178, -1.88095, 0, -23.6178, -1.11875, -7.61905, -19.8082, -1.11875, -15.2381, -24.3799, 1.16637, 0, -22.8557, 1.16637, -9.90267, -20.5703, 1.16637, 0, -21.3324, -1.88095, 0, -24.3799, -1.11875, -4.57173, -15.2396, 1.92857, -15.2381, -14.4775, -1.88095, -14.4759, -19.0481, -1.88095, -15.2381, -23.6178, -1.88095, -5.33393, -24.3799, 1.16637, -3.80952, -23.6178, 1.92857, 0, -21.3324, -1.11875, -12.9515, -13.7153, -0.356919, -14.4759, -24.3799, -1.11875, 0, -19.8082, 0.404538, -15.2381, -23.6178, 1.16637, -7.61905, -22.8557, 1.92857, -8.37827, -13.7153, 0.404538, -15.2381, -22.8557, -1.11875, -9.90267, -22.0935, 1.16637, -11.4271, -22.8557, -1.88095, -8.37827)
|
||||
|
||||
[sub_resource type="ConvexPolygonShape3D" id="ConvexPolygonShape3D_p1in1"]
|
||||
points = PackedVector3Array(-19.0476, 1.16637, -15.2381, -4.57173, -1.11875, -24.3792, -4.57173, 0.404538, -24.3792, 0, -1.11875, -20.5706, -18.2832, -1.88095, -15.2381, 0, 1.92857, -21.3321, -12.9507, 1.92857, -20.5706, -13.7133, 1.92857, -15.2381, -12.9507, -1.11875, -21.3321, 0, -1.88095, -23.6176, -13.7133, -1.88095, -15.2381, 0, 1.92857, -23.6176, -8.38274, -1.88095, -22.8561, -9.90417, 1.16637, -22.8561, -15.2366, 0.404538, -19.8091, -19.0476, -1.11875, -16.0014, -18.2832, 1.92857, -16.0014, 0, -1.88095, -21.3321, 0, 1.16637, -20.5706, -12.9507, -1.11875, -15.2381, -5.3343, 1.92857, -23.6176, 0, 1.16637, -24.3792, -15.2366, -1.88095, -19.0484, -9.90417, -1.11875, -22.8561, -12.9507, 1.16637, -15.2381, -19.0476, 1.16637, -16.0014, 0, -1.11875, -24.3792, -15.2366, -1.11875, -19.8091, -15.9992, 1.16637, -19.0484, -7.62017, 1.16637, -23.6176, -8.38274, 1.92857, -22.8561, -5.3343, -1.88095, -23.6176)
|
||||
|
||||
[sub_resource type="TorusMesh" id="TorusMesh_jlvj7"]
|
||||
inner_radius = 25.0
|
||||
outer_radius = 20.0
|
||||
|
||||
[node name="Node3D" type="Node3D"]
|
||||
|
||||
[node name="InnerTube" type="StaticBody3D" parent="."]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="InnerTube"]
|
||||
mesh = SubResource("TorusMesh_nvgim")
|
||||
|
||||
[node name="CollisionShape3D8" type="CollisionShape3D" parent="InnerTube"]
|
||||
shape = SubResource("ConvexPolygonShape3D_5aph0")
|
||||
|
||||
[node name="CollisionShape3D7" type="CollisionShape3D" parent="InnerTube"]
|
||||
shape = SubResource("ConvexPolygonShape3D_iayf5")
|
||||
|
||||
[node name="CollisionShape3D6" type="CollisionShape3D" parent="InnerTube"]
|
||||
shape = SubResource("ConvexPolygonShape3D_ei2hu")
|
||||
|
||||
[node name="CollisionShape3D5" type="CollisionShape3D" parent="InnerTube"]
|
||||
shape = SubResource("ConvexPolygonShape3D_7k4ip")
|
||||
|
||||
[node name="CollisionShape3D4" type="CollisionShape3D" parent="InnerTube"]
|
||||
shape = SubResource("ConvexPolygonShape3D_ehkdd")
|
||||
|
||||
[node name="CollisionShape3D3" type="CollisionShape3D" parent="InnerTube"]
|
||||
shape = SubResource("ConvexPolygonShape3D_lr4gi")
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="InnerTube"]
|
||||
shape = SubResource("ConvexPolygonShape3D_4ekbl")
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="InnerTube"]
|
||||
shape = SubResource("ConvexPolygonShape3D_p1in1")
|
||||
|
||||
[node name="OuterTube" type="StaticBody3D" parent="."]
|
||||
|
||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="OuterTube"]
|
||||
mesh = SubResource("TorusMesh_jlvj7")
|
||||
@ -1,67 +0,0 @@
|
||||
class_name CelestialBody
|
||||
extends RigidBody2D
|
||||
|
||||
# The celestial body that this body orbits.
|
||||
@export var primary: CelestialBody
|
||||
|
||||
# This is a placeholder for your pixel art texture.
|
||||
@export var texture: Texture2D
|
||||
|
||||
# The radius of the body, used for drawing and future collision detection.
|
||||
@export var radius: float = 10.0
|
||||
|
||||
# Default color based on body type for visualization.
|
||||
var body_color: Color = Color.ORANGE_RED
|
||||
|
||||
var orbit_radius_real : float = 0.0
|
||||
|
||||
var direction_to_primary : Vector2 = Vector2.ZERO
|
||||
|
||||
func get_class_name() -> String:
|
||||
return "CelestialBody"
|
||||
|
||||
func _ready() -> void:
|
||||
# We will handle gravity manually, so we set the built-in gravity scale to 0.
|
||||
gravity_scale = 0.0
|
||||
|
||||
# To make the simulation work without drag, we must set linear damping to 0.
|
||||
linear_damp = 0.0
|
||||
angular_damp = 0.0
|
||||
|
||||
can_sleep = false
|
||||
|
||||
# Set the color based on the class name for easy differentiation.
|
||||
match get_class_name():
|
||||
"Star":
|
||||
body_color = Color.GOLD
|
||||
"Planet":
|
||||
body_color = Color.BLUE
|
||||
"Moon":
|
||||
body_color = Color.PURPLE
|
||||
"Station":
|
||||
body_color = Color.WHITE
|
||||
"Asteroid":
|
||||
body_color = Color.BROWN
|
||||
_:
|
||||
body_color = Color.ORANGE_RED
|
||||
|
||||
# This callback is the correct place to apply custom forces to a RigidBody2D.
|
||||
func _integrate_forces(state: PhysicsDirectBodyState2D) -> void:
|
||||
if primary and is_instance_valid(primary):
|
||||
var force = OrbitalMechanics.simple_n_body_grav(self, primary)
|
||||
state.apply_central_force(force)
|
||||
|
||||
# We force a redraw here to update the body's visual representation.
|
||||
queue_redraw()
|
||||
|
||||
# Override the default drawing function to draw the body.
|
||||
# This is useful for debugging and visualization.
|
||||
func _draw() -> void:
|
||||
if texture:
|
||||
# If a texture is assigned, draw it.
|
||||
var size = Vector2(radius * 2, radius * 2)
|
||||
var offset = -size / 2.0
|
||||
draw_texture_rect(texture, Rect2(offset, size), false)
|
||||
else:
|
||||
# Otherwise, draw a simple placeholder circle.
|
||||
draw_circle(Vector2.ZERO, radius, body_color)
|
||||
@ -1 +0,0 @@
|
||||
uid://bn1u2xood3vs6
|
||||
50
scripts/network/network_handler.gd
Normal file
50
scripts/network/network_handler.gd
Normal file
@ -0,0 +1,50 @@
|
||||
extends Node
|
||||
|
||||
var port = 42069
|
||||
|
||||
func create_server() -> void:
|
||||
print(multiplayer.multiplayer_peer)
|
||||
var peer = ENetMultiplayerPeer.new()
|
||||
|
||||
setup_connections()
|
||||
|
||||
var error = peer.create_server(port)
|
||||
if error:
|
||||
push_error(error)
|
||||
return
|
||||
|
||||
multiplayer.multiplayer_peer = peer
|
||||
|
||||
print("Server Unique ID: ", multiplayer.get_unique_id())
|
||||
|
||||
func create_client() -> void:
|
||||
setup_connections()
|
||||
|
||||
var peer = ENetMultiplayerPeer.new()
|
||||
var error = peer.create_client("127.0.0.1", port)
|
||||
if error:
|
||||
push_error(error)
|
||||
return
|
||||
|
||||
multiplayer.multiplayer_peer = peer
|
||||
print("Client Unique ID: ", multiplayer.get_unique_id())
|
||||
|
||||
func setup_connections():
|
||||
multiplayer.peer_connected.connect(on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(on_peer_disconnected)
|
||||
multiplayer.connected_to_server.connect(on_connected_to_server)
|
||||
|
||||
func on_peer_connected(peer_id: int) -> void:
|
||||
print("Peer %s recieved connection: %s" % [multiplayer.get_unique_id(), peer_id])
|
||||
|
||||
# For each peer that connects, we put them in the queue to spawn
|
||||
if multiplayer.is_server():
|
||||
GameManager.queue_spawn_player(peer_id)
|
||||
|
||||
|
||||
func on_peer_disconnected(peer_id: int) -> void:
|
||||
print("Peer %s lost connection to: %s" % [multiplayer.get_unique_id(), peer_id])
|
||||
print(multiplayer.get_peers())
|
||||
|
||||
func on_connected_to_server() -> void:
|
||||
print("%s connected to server!" % multiplayer.get_unique_id())
|
||||
1
scripts/network/network_handler.gd.uid
Normal file
1
scripts/network/network_handler.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dw3hm45dog1sk
|
||||
@ -34,13 +34,13 @@ func _ready():
|
||||
|
||||
# --- PUBLIC FORCE APPLICATION METHODS ---
|
||||
# This method is called by a component (like Thruster) at its global position.
|
||||
func apply_force(force: Vector2, position: Vector2 = self.global_position):
|
||||
func apply_force(force: Vector2, pos: Vector2 = self.global_position):
|
||||
# This is the force routing logic.
|
||||
match physics_mode:
|
||||
PhysicsMode.INDEPENDENT:
|
||||
_add_forces(force, position)
|
||||
_add_forces(force, pos)
|
||||
PhysicsMode.COMPOSITE:
|
||||
_add_forces(force, position)
|
||||
_add_forces(force, pos)
|
||||
## If we are the root, accumulate the force and calculate torque on the total body.
|
||||
#accumulated_force += force
|
||||
#
|
||||
@ -56,7 +56,7 @@ func apply_force(force: Vector2, position: Vector2 = self.global_position):
|
||||
if p is OrbitalBody2D:
|
||||
# Recursively call the parent's apply_force method.
|
||||
# This sends the force (and its original global position) up the chain.
|
||||
p.apply_force(force, position)
|
||||
p.apply_force(force, pos)
|
||||
return # Stop at the first OrbitalBody2D parent
|
||||
p = p.get_parent()
|
||||
|
||||
@ -64,13 +64,13 @@ func apply_force(force: Vector2, position: Vector2 = self.global_position):
|
||||
physics_mode = PhysicsMode.COMPOSITE
|
||||
apply_force(force, position)
|
||||
|
||||
func _add_forces(force: Vector2, position: Vector2 = Vector2.ZERO):
|
||||
func _add_forces(force: Vector2, pos: Vector2 = Vector2.ZERO):
|
||||
# If we are the root, accumulate the force and calculate torque on the total body.
|
||||
accumulated_force += force
|
||||
|
||||
# Calculate torque (2D cross product: T = r x F = r.x * F.y - r.y * F.x)
|
||||
# 'r' is the vector from the center of mass (global_position) to the point of force application (position).
|
||||
var r = position - global_position
|
||||
var r = pos - global_position
|
||||
var torque = r.x * force.y - r.y * force.x
|
||||
accumulated_torque += torque
|
||||
|
||||
|
||||
3
scripts/singletons/constants.gd
Normal file
3
scripts/singletons/constants.gd
Normal file
@ -0,0 +1,3 @@
|
||||
extends Node
|
||||
|
||||
const UI_GRID_SIZE = 96
|
||||
1
scripts/singletons/constants.gd.uid
Normal file
1
scripts/singletons/constants.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cvqplp22t2op7
|
||||
@ -5,7 +5,7 @@ var config: GameConfig
|
||||
|
||||
# --- Dictionaries to track players and their objects ---
|
||||
var player_controllers: Dictionary = {} # Key: player_id, Value: PlayerController node
|
||||
var player_pawns: Dictionary = {} # Key: player_id, Value: PilotBall node
|
||||
var player_pawns: Dictionary = {} # Key: player_id, Value: CharacterPawn3D node
|
||||
|
||||
# This variable will hold the reference to the currently active star system.
|
||||
var current_star_system : StarSystem = null
|
||||
@ -13,95 +13,85 @@ var current_star_system : StarSystem = null
|
||||
var registered_spawners: Array[Spawner] = []
|
||||
var waiting_players: Array[int] = [] # A queue for players waiting to spawn
|
||||
|
||||
# --- Scene References for 3D ---
|
||||
var player_controller_3d_scene: PackedScene = preload("res://scenes/tests/3d/player_controller_3d.tscn")
|
||||
var character_pawn_3d_scene: PackedScene = preload("res://scenes/tests/3d/character_pawn_3d.tscn")
|
||||
|
||||
|
||||
func _ready():
|
||||
# Load the configuration resource from its fixed path.
|
||||
config = load("res://scripts/singletons/default_game_config.tres")
|
||||
if not config:
|
||||
push_error("GameManager could not load game_config.tres!")
|
||||
# This is now expected since we are using 3D scenes directly.
|
||||
print("GameManager: game_config.tres not found or used. Using direct 3D scene references.")
|
||||
return
|
||||
|
||||
# We need to initialize a network peer for the authority system to work,
|
||||
# even in a single-player game. This creates a "server" for just you.
|
||||
var peer = ENetMultiplayerPeer.new()
|
||||
peer.create_server(5999) # Port number can be anything for a local game
|
||||
multiplayer.multiplayer_peer = peer
|
||||
|
||||
# When the local server is created, it automatically calls on_player_connected(1).
|
||||
multiplayer.peer_connected.connect(on_player_connected)
|
||||
|
||||
start_game()
|
||||
# Check command-line arguments to determine if this instance should be a server or client.
|
||||
# Godot's "Run Multiple Instances" feature adds "--server" to the main instance.
|
||||
if "--server" in OS.get_cmdline_args():
|
||||
NetworkHandler.create_server()
|
||||
elif "--host" in OS.get_cmdline_args():
|
||||
NetworkHandler.create_server()
|
||||
# Host also acts as a player, so we need to handle its own connection.
|
||||
NetworkHandler.on_peer_connected.call_deferred(1)
|
||||
else:
|
||||
print("GameManager: Starting as CLIENT.")
|
||||
NetworkHandler.create_client()
|
||||
|
||||
func _process(_delta):
|
||||
if find_available_spawner():
|
||||
_try_spawn_waiting_player()
|
||||
|
||||
# Called when the game starts (e.g., from _ready() in StarSystemGenerator)
|
||||
func start_game():
|
||||
# For a single-player game, we simulate a player connecting with ID 1.
|
||||
on_player_connected(1)
|
||||
|
||||
# This would be connected to a network signal in a multiplayer game.
|
||||
func on_player_connected(player_id: int):
|
||||
print("GameManager: Player %d connected." % player_id)
|
||||
|
||||
# 1. Spawn a controller for the new player.
|
||||
var controller = config.player_controller_scene.instantiate()
|
||||
controller.name = "PlayerController_%d" % player_id
|
||||
add_child(controller)
|
||||
player_controllers[player_id] = controller
|
||||
|
||||
# 2. Attempt to spawn a pawn for them immediately.
|
||||
_attempt_to_spawn_player(player_id)
|
||||
|
||||
func _attempt_to_spawn_player(player_id: int):
|
||||
if registered_spawners.is_empty():
|
||||
# No spawners available, add the player to the waiting queue.
|
||||
if not player_id in waiting_players:
|
||||
waiting_players.append(player_id)
|
||||
print("GameManager: No spawners available. Player %d is now waiting." % player_id)
|
||||
# You could show a "Waiting for available spawner..." UI here.
|
||||
else:
|
||||
# Spawners are available, proceed with spawning.
|
||||
spawn_player_pawn(player_id)
|
||||
pass
|
||||
|
||||
|
||||
# NEW: A function to process the waiting queue.
|
||||
func queue_spawn_player(player_id: int):
|
||||
waiting_players.append(player_id)
|
||||
print("GameManager: Player %d queued for spawn." % player_id)
|
||||
|
||||
|
||||
# function to process the waiting queue.
|
||||
func _try_spawn_waiting_player():
|
||||
if not waiting_players.is_empty() and not registered_spawners.is_empty():
|
||||
var player_to_spawn = waiting_players.pop_front()
|
||||
print("GameManager: Spawner is now available. Spawning waiting player %d." % player_to_spawn)
|
||||
spawn_player_pawn(player_to_spawn)
|
||||
var player_id = waiting_players.pop_back()
|
||||
|
||||
print("GameManager: Spawner is now available. Spawning waiting player %d." % player_id)
|
||||
var spawn_point = find_available_spawner()
|
||||
|
||||
if spawn_point:
|
||||
_spawn_player_pawn(player_id)
|
||||
|
||||
var pawn = player_pawns[player_id]
|
||||
|
||||
pawn.set_multiplayer_authority(player_id)
|
||||
|
||||
spawn_point.add_child(pawn)
|
||||
|
||||
print("GameManager peer %s: Player %d spawned successfully." % [multiplayer.get_unique_id(), player_id])
|
||||
else:
|
||||
waiting_players.append(player_id)
|
||||
print("GameManager peer %s: Failed to spawn player %d." % [multiplayer.get_unique_id(), player_id])
|
||||
|
||||
func find_available_spawner() -> Spawner:
|
||||
var idx = registered_spawners.find_custom(func(spawner: Spawner) -> bool:
|
||||
return spawner.can_spawn()
|
||||
)
|
||||
return registered_spawners[idx] if idx != -1 else null
|
||||
|
||||
# @rpc("call_local")
|
||||
func _spawn_player_pawn(player_id: int):
|
||||
var pawn = character_pawn_3d_scene.instantiate()
|
||||
|
||||
pawn.name = str(player_id)
|
||||
|
||||
func spawn_player_pawn(player_id: int):
|
||||
if not player_controllers.has(player_id):
|
||||
push_error("Cannot spawn pawn for non-existent player %d" % player_id)
|
||||
return
|
||||
|
||||
# --- NEW SPAWNING LOGIC ---
|
||||
if registered_spawners.is_empty():
|
||||
push_error("GameManager: No spawners available to create pawn!")
|
||||
return
|
||||
|
||||
# For now, we'll just pick the first available spawner.
|
||||
# Later, you could present a UI for the player to choose.
|
||||
var spawn_point: Spawner = registered_spawners[0]
|
||||
if not is_instance_valid(spawn_point):
|
||||
push_error("GameManager: Spawn point not found!")
|
||||
return
|
||||
|
||||
var owning_module = spawn_point.get_root_module()
|
||||
if not is_instance_valid(owning_module):
|
||||
push_error("GameManager: Registered spawner has no owning module!")
|
||||
return
|
||||
|
||||
var pawn = config.default_pawn_scene.instantiate()
|
||||
owning_module.add_child(pawn)
|
||||
pawn.owner = owning_module
|
||||
|
||||
# 2. Set its position and initial velocity from the spawner.
|
||||
pawn.global_position = spawn_point.global_position
|
||||
|
||||
player_pawns[player_id] = pawn
|
||||
|
||||
# 3. Possess the pawn with the player's controller.
|
||||
player_controllers[player_id].possess(pawn)
|
||||
|
||||
pawn.set_multiplayer_authority(player_id)
|
||||
|
||||
print("GameManager: Spawned 3D Pawn for player %d" % player_id)
|
||||
|
||||
|
||||
# Any scene that generates a star system will call this function to register itself.
|
||||
func register_star_system(system_node):
|
||||
@ -121,7 +111,7 @@ func register_spawner(spawner_node: Spawner):
|
||||
print("GameManager: Spawner '%s' registered." % spawner_node.name)
|
||||
|
||||
# NEW: If a player is waiting, try to spawn them now.
|
||||
_try_spawn_waiting_player()
|
||||
# _try_spawn_waiting_player()
|
||||
|
||||
# A helper function for easily accessing the system's data.
|
||||
func get_system_data() -> SystemData:
|
||||
@ -139,3 +129,11 @@ func get_all_trackable_bodies() -> Array[OrbitalBody2D]:
|
||||
|
||||
# Next, add all registered ships to the list.
|
||||
return all_bodies
|
||||
|
||||
# --- CLIENT-SIDE RPC PROXY ---
|
||||
# A client-side script can call this function to send a request to the server.
|
||||
# Example: GameManager.request_server_action("some_action", [arg1, arg2])
|
||||
@rpc("call_remote")
|
||||
func request_server_action(action_name: String, args: Array = []):
|
||||
# This function's body only runs on the SERVER.
|
||||
print("Server received request: ", action_name, " with args: ", args)
|
||||
|
||||
@ -166,72 +166,51 @@ func _calculate_n_body_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVecto
|
||||
return path_points
|
||||
|
||||
# Calculates an array of points for the orbit RELATIVE to the primary body.
|
||||
func _calculate_relative_orbital_path(body_to_trace: CelestialBody) -> PackedVector2Array:
|
||||
if not is_instance_valid(body_to_trace) or not is_instance_valid(body_to_trace.primary):
|
||||
func _calculate_relative_orbital_path(body_to_trace: OrbitalBody2D) -> PackedVector2Array:
|
||||
if not is_instance_valid(body_to_trace) or not body_to_trace.has_method("get_primary") or not is_instance_valid(body_to_trace.get_primary()):
|
||||
return PackedVector2Array()
|
||||
|
||||
# --- Initial State ---
|
||||
var primary = body_to_trace.primary
|
||||
|
||||
var primary = body_to_trace.get_primary()
|
||||
var primary_mass = primary.mass
|
||||
var body_mass = body_to_trace.mass
|
||||
|
||||
# The position of the body relative to its parent.
|
||||
|
||||
var ghost_relative_pos = body_to_trace.global_position - primary.global_position
|
||||
# The velocity of the body relative to its parent's velocity.
|
||||
var ghost_relative_vel = body_to_trace.linear_velocity - primary.linear_velocity
|
||||
|
||||
# --- NEW: Dynamically Calculate Simulation Time from Orbital Period ---
|
||||
|
||||
var r_magnitude = ghost_relative_pos.length()
|
||||
if r_magnitude == 0:
|
||||
return PackedVector2Array()
|
||||
|
||||
var v_sq = ghost_relative_vel.length_squared()
|
||||
var mu = OrbitalMechanics.G * primary_mass # Standard Gravitational Parameter
|
||||
var mu = G * primary_mass
|
||||
|
||||
# 1. Calculate the specific orbital energy. Negative energy means it's a stable orbit.
|
||||
var specific_energy = v_sq / 2.0 - mu / r_magnitude
|
||||
|
||||
# --- Simulation Parameters ---
|
||||
var num_steps = 200 # The desired number of points for the orbit line's smoothness.
|
||||
var time_step: float # The duration of each step, which we will now calculate.
|
||||
|
||||
var num_steps = 200
|
||||
var time_step: float
|
||||
if specific_energy >= 0:
|
||||
# Escape trajectory (parabolic or hyperbolic). The period is infinite.
|
||||
# We'll just draw a segment of its path using a fixed time_step.
|
||||
time_step = 0.1
|
||||
else:
|
||||
# Stable elliptical orbit.
|
||||
# 2. Calculate the semi-major axis from the energy.
|
||||
var semi_major_axis = -mu / (2.0 * specific_energy)
|
||||
|
||||
# 3. Calculate the orbital period using Kepler's Third Law: T = 2π * sqrt(a³/μ)
|
||||
var orbital_period = 2.0 * PI * sqrt(pow(semi_major_axis, 3) / mu)
|
||||
|
||||
# 4. Calculate the time_step needed to complete one period in num_steps.
|
||||
time_step = orbital_period / float(num_steps)
|
||||
|
||||
var path_points = PackedVector2Array()
|
||||
|
||||
for i in range(num_steps):
|
||||
# --- Physics Calculation (Primary is now at the origin (0,0)) ---
|
||||
var distance_sq = ghost_relative_pos.length_squared()
|
||||
|
||||
if distance_sq < 1.0:
|
||||
break
|
||||
|
||||
# Direction is simply towards the origin.
|
||||
|
||||
var direction = -ghost_relative_pos.normalized()
|
||||
var force_magnitude = (G * primary_mass * body_mass) / distance_sq
|
||||
var force_vector = direction * force_magnitude
|
||||
|
||||
var acceleration = force_vector / body_mass
|
||||
|
||||
# --- Integration (Update ghost's relative state) ---
|
||||
|
||||
ghost_relative_vel += acceleration * time_step
|
||||
ghost_relative_pos += ghost_relative_vel * time_step
|
||||
|
||||
path_points.append(ghost_relative_pos)
|
||||
|
||||
|
||||
return path_points
|
||||
|
||||
# Calculates the Hill Sphere radius for a satellite.
|
||||
@ -260,70 +239,3 @@ func get_orbital_time_in_seconds(orbiter: OrbitalBody2D, primary: OrbitalBody2D)
|
||||
var mu = OrbitalMechanics.G * primary.mass
|
||||
var r = orbiter.global_position.distance_to(primary.global_position)
|
||||
return TAU * sqrt(pow(r, 3) / mu)
|
||||
|
||||
|
||||
## Projects the future paths of an array of bodies interacting with each other.
|
||||
## Returns a dictionary mapping each body to its calculated PackedVector2Array path.
|
||||
func project_n_body_paths(
|
||||
bodies_to_trace: Array[OrbitalBody2D],
|
||||
num_steps: int,
|
||||
time_step: float
|
||||
) -> Dictionary:
|
||||
|
||||
# --- Step 1: Create a "ghost state" for each body ---
|
||||
# A ghost state is just a simple dictionary holding the physics properties.
|
||||
var ghost_states = []
|
||||
for body in bodies_to_trace:
|
||||
ghost_states.append({
|
||||
"body_ref": body,
|
||||
"mass": body.mass,
|
||||
"position": body.global_position,
|
||||
"velocity": body.linear_velocity # Velocity is always in the same space
|
||||
})
|
||||
|
||||
# --- Step 2: Prepare the results dictionary ---
|
||||
var paths: Dictionary = {}
|
||||
for state in ghost_states:
|
||||
paths[state.body_ref] = PackedVector2Array()
|
||||
|
||||
# --- Step 3: Run the ghost simulation ---
|
||||
for i in range(num_steps):
|
||||
# Create a list to hold the forces for this time step
|
||||
var forces_for_step = {}
|
||||
for state in ghost_states:
|
||||
forces_for_step[state.body_ref] = Vector2.ZERO
|
||||
|
||||
# a) Calculate all gravitational forces between the ghosts
|
||||
for j in range(ghost_states.size()):
|
||||
var state_a = ghost_states[j]
|
||||
for k in range(j + 1, ghost_states.size()):
|
||||
var state_b = ghost_states[k]
|
||||
|
||||
# Calculate force between the two ghost states
|
||||
var force_vector = _calculate_force_between_ghosts(state_a, state_b)
|
||||
|
||||
# Store the forces to be applied
|
||||
forces_for_step[state_a.body_ref] += force_vector
|
||||
forces_for_step[state_b.body_ref] -= force_vector
|
||||
|
||||
# b) Integrate forces for each ghost to find its next position
|
||||
for state in ghost_states:
|
||||
if state.mass > 0:
|
||||
var acceleration = forces_for_step[state.body_ref] / state.mass
|
||||
state.velocity += acceleration * time_step
|
||||
state.position += state.velocity * time_step
|
||||
|
||||
# c) Record the new position in the path
|
||||
paths[state.body_ref].append(state.position)
|
||||
|
||||
return paths
|
||||
|
||||
# TODO this could be used in the above apply_n_body_forces method to instead calculate the force and store it for applying later
|
||||
# --- Private helper for the projection function ---
|
||||
func _calculate_force_between_ghosts(state_a: Dictionary, state_b: Dictionary) -> Vector2:
|
||||
var distance_sq = state_a.position.distance_squared_to(state_b.position)
|
||||
if distance_sq < 1.0: return Vector2.ZERO
|
||||
|
||||
var force_magnitude = (G * state_a.mass * state_b.mass) / distance_sq
|
||||
var direction = state_a.position.direction_to(state_b.position)
|
||||
return direction * force_magnitude
|
||||
|
||||
@ -1,42 +0,0 @@
|
||||
# scripts/singletons/popup_manager.gd
|
||||
extends Node
|
||||
|
||||
# A dictionary to track active UI panels, ensuring we only have one per object.
|
||||
# Key: context_object (e.g., the SystemStation), Value: the instantiated panel node
|
||||
var active_panels: Dictionary = {}
|
||||
|
||||
## The main entry point for the system.
|
||||
#func request_popup(panel_world: World2D, player: PilotBall):
|
||||
## Get the player's personal UI container (e.g., from PilotBall.gd)
|
||||
#var ui_container = player.get_ui_container()
|
||||
#if not ui_container: return
|
||||
#
|
||||
## Create a draggable Window for the UI
|
||||
#var window = Window.new()
|
||||
#window.title = panel_instance.name
|
||||
#window.size = panel_instance.size + Vector2(20, 40) # Add space for title bar
|
||||
#window.close_requested.connect(window.queue_free) # Close button works automatically
|
||||
#ui_container.add_child(window)
|
||||
#
|
||||
## Create the SubViewport setup
|
||||
#var vp_container = SubViewportContainer.new()
|
||||
#vp_container.stretch = true
|
||||
#vp_container.anchors_preset = Control.PRESET_FULL_RECT
|
||||
#window.add_child(vp_container)
|
||||
#
|
||||
#var sub_viewport = SubViewport.new()
|
||||
#sub_viewport.world_2d
|
||||
#sub_viewport.size = panel_instance.size
|
||||
#sub_viewport.transparent_bg = true
|
||||
#vp_container.add_child(sub_viewport)
|
||||
#
|
||||
## Create and configure the camera
|
||||
#var camera = Camera2D.new()
|
||||
#camera.set_cull_mask_value(1, false) # Don't see the default world
|
||||
#camera.set_cull_mask_value(2, true) # ONLY see the "UI_Panels" layer
|
||||
#camera.global_position = panel_instance.global_position
|
||||
## You can add zoom logic here if needed, similar to the station implementation
|
||||
#sub_viewport.add_child(camera)
|
||||
#
|
||||
## Forward input from the screen overlay to the world-space panel
|
||||
#vp_container.gui_input.connect(func(event): sub_viewport.push_input(event.duplicate()))
|
||||
@ -1 +0,0 @@
|
||||
uid://6s8bq0if4mev
|
||||
@ -1,11 +0,0 @@
|
||||
extends Node
|
||||
|
||||
# Signal emitted when the player requests to toggle the map view.
|
||||
signal map_mode_toggled
|
||||
|
||||
# Signal emitted from the map when a body is selected to be followed.
|
||||
# It passes the selected CelestialBody as an argument.
|
||||
signal follow_target_selected(body: CelestialBody)
|
||||
|
||||
# Emitted by the NavComputer to command a timed rotation.
|
||||
signal rotation_maneuver_planned(target_rotation_rad: float, time_window_seconds: float)
|
||||
@ -1 +0,0 @@
|
||||
uid://duu8vu31yyt7r
|
||||
@ -44,7 +44,7 @@ func generate(star_system: StarSystem) -> SystemData:
|
||||
planet.position = Vector2.ZERO
|
||||
planet_barycenter.recalculate_total_mass()
|
||||
# C. Create moons for this planet.
|
||||
_generate_moons(planet, planet_barycenter, star_system, system_data)
|
||||
_generate_moons(planet, planet_barycenter, system_data)
|
||||
|
||||
# D. Place the entire planetary system in a stable orbit.
|
||||
planet_barycenter.global_position = Vector2(current_orbit_radius, 0).rotated(randf_range(0, TAU))
|
||||
@ -63,7 +63,7 @@ func generate(star_system: StarSystem) -> SystemData:
|
||||
|
||||
return system_data
|
||||
|
||||
func _generate_moons(planet: OrbitalBody2D, planet_barycenter: Barycenter, star_system: StarSystem, system_data: SystemData):
|
||||
func _generate_moons(planet: OrbitalBody2D, planet_barycenter: Barycenter, system_data: SystemData):
|
||||
var num_moons = randi_range(0, int(planet.mass / MOON_MASS / 2.0)) # Heavier planets get more moons
|
||||
num_moons = min(num_moons, MAX_MOONS_PER_PLANET)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user