60 Commits

Author SHA1 Message Date
29851ea167 Move to physics based mouse input and try bugfix for basis reset 2025-11-06 16:15:53 +01:00
0cd9ebdd04 Remove low level network handler 2025-11-06 08:46:33 +01:00
4da8bcaec2 Fix camera jitter with disabled v-sync 2025-11-05 18:35:19 +01:00
71ad2f09ff Networked player pawns 2025-11-05 12:15:32 +01:00
2f5a88345f WIP Low level network handling 2025-11-03 17:27:36 +01:00
5e851049b5 WIP Networking 2025-11-02 20:37:46 +01:00
c4fd7f1330 WIP Server Client Logic 2025-11-01 18:30:38 +01:00
820da83397 Fix grab rotaiton issues 2025-11-01 18:30:17 +01:00
14b24beb23 Fix jetpack vectors, change seeking_climb behaviour 2025-11-01 14:09:26 +01:00
60f2ddb3d7 Functional launch mechanic in up and down vector 2025-10-31 16:44:07 +01:00
c50d0eae52 WIP Launch logic 2025-10-31 16:30:14 +01:00
d375e0d208 Remove bad AI inserted helper function 2025-10-31 15:03:59 +01:00
9b128a3540 Working release and grab logic 2025-10-31 14:52:16 +01:00
6df457a256 Functional release and coast for climb but no reach 2025-10-31 14:33:06 +01:00
bc443b884c Dev status update 2025-10-31 13:08:48 +01:00
8184ec06b4 Fix grip handover with new movement component 2025-10-31 11:50:03 +01:00
926a64c3dd WIP Refactoring to movement component hierarchy 2025-10-30 22:55:19 +01:00
29f9bccfd3 Move to physics based climbing 2025-10-30 17:29:09 +01:00
7d7580a123 Rotate on grip and climb past ladder 2025-10-29 21:18:16 +01:00
59d457e9ae Fix minor warnings 2025-10-29 19:43:57 +01:00
97ccb2a9ac Climb in more directions 2025-10-29 19:39:15 +01:00
1ab2c06336 Upgrade to 4.5 2025-10-29 19:39:01 +01:00
f51672c6a9 WIP Climbing movement 2025-10-28 16:25:32 +01:00
8e3f415cb4 Reverse rotation on grip area 2025-10-27 22:32:53 +01:00
cdcb4796f7 Grip rotation 2025-10-27 22:24:23 +01:00
24bc3afd2e Gripping offset 2025-10-26 13:17:44 +01:00
4f78de64ba Refactor into single state machine 2025-10-25 20:45:45 +02:00
fe050897dd Key Input struct 2025-10-25 19:58:16 +02:00
e075ff580d ZeroGMovementComponent WIP 2025-10-25 16:47:32 +02:00
90e756ad28 Initial registerering grip point 2025-10-25 15:42:31 +02:00
faf8e7c83a Fix roll 2025-10-25 10:54:22 +02:00
772f9c7df3 WIP Thruster pack refactor, non-working roll 2025-10-24 19:38:18 +02:00
e066bc4786 Free rotation for EVA pawn 2025-10-24 11:21:40 +02:00
cc681ae08a Bounce of materials in 3d 2025-10-23 23:04:40 +02:00
138e17503a Working camera controller 2025-10-23 10:17:55 +02:00
20a37dda17 Pitch and yaw camera and direct movement after look at 2025-10-22 16:58:06 +02:00
8645d2bdc4 Initial 3d controller and pawn 2025-10-22 14:40:19 +02:00
d8055752d5 Render of inputs to databanks 2025-10-21 17:26:31 +02:00
21bbbacbbe Hide front on flip 2025-10-21 12:15:12 +02:00
d18c87a051 WIP Wiring logic (now drawing wires) 2025-10-21 12:05:24 +02:00
a89154a1c2 WIP Wiring Mode 2025-10-21 08:28:11 +02:00
c662714997 WIP Wiring 2025-10-20 17:21:16 +02:00
6a4492ef37 Remove comment 2025-10-20 13:51:36 +02:00
85815d957b Cleanup 2025-10-20 10:49:14 +02:00
ec6ca92360 Autopilot readout connected 2025-10-20 09:45:18 +02:00
f25464df03 Working single player panel frame 2025-10-19 19:03:55 +02:00
2ceddb2bbf WIP Make panels function with autopilot 2025-10-17 17:19:07 +02:00
1228a79cae WIP Persistent panels 2025-10-17 14:27:11 +02:00
425e857ba9 WIP Helm and Panel Logic 2025-10-17 10:58:42 +02:00
588fa29484 Working maps of new simulation 2025-10-16 16:36:53 +02:00
c14b07d24f WIP Almost working reworked N-body simulation 2025-10-16 11:56:35 +02:00
c61fa2b917 WIP Big physics refactor 2025-10-14 20:44:58 +02:00
9dac569ad6 Merge branch 'feature/ship-systems' 2025-10-14 08:50:29 +02:00
63d6a67d97 Map in helm station 2025-10-14 08:46:02 +02:00
c79e016503 WIP readout 2025-10-13 16:47:04 +02:00
d03a00cbce Player Controller and start of networking 2025-10-13 13:12:56 +02:00
5f30ab6c2f Working calibration and thruster control 2025-10-13 08:45:43 +02:00
35d3110818 Working rcs calibration via helm station 2025-10-12 15:59:03 +02:00
3d85dda4db WIP System (non-func thrusters) 2025-10-12 14:52:56 +02:00
02908f114c WIP Character 2025-10-12 11:13:12 +02:00
158 changed files with 5185 additions and 1909 deletions

View File

@ -1,82 +1,246 @@
Project "Stardust Drifter" Development Status (Phase 0.2: Custom Physics & Ship Interior)
Overview
# Project "Millimeters of Aluminium" Development Log
## Overview
The project is currently focused on Phase 0.2, which involves migrating from Godot's built-in RigidBody2D physics to a custom, manually integrated OrbitalBody2D model. This is critical for supporting the multi-scale physics simulation (orbital vs. local) and preparing for networked multiplayer. The core physics model is now stable and functional.
I. Fully Implemented & Stable Systems
Custom Physics Core
The project is undergoing a major architectural refactor to move from a monolithic Spaceship class to a fully modular, component-based system. The foundation for this new architecture, centered around Module, Component, and Station classes, is now in place. The next steps involve migrating legacy systems into this new paradigm.
Physics Unification: All physically simulated objects (Spaceship, Module, Thruster, StructuralPiece) now inherit from the custom OrbitalBody2D class. This ensures a consistent hierarchy and simplified force routing.
## I. Fully Implemented & Stable Systems
Asymmetric Gravity Model (WIP Foundation): The OrbitalMechanics singleton is refactored to check if a body is a simulation root, applying forces manually (linear_velocity, angular_velocity). This is the foundation for implementing the SCALE_FACTOR for miniature ship gravity.
Custom Physics Core: All physical objects now inherit from a custom OrbitalBody2D class, which handles mass aggregation and force integration. The physics loop is correctly disabled in the editor to prevent errors.
Modular Force Integration: Thrusters are now OrbitalBody2D children that correctly apply force to themselves. The force is then routed up the hierarchy to the root ship node, calculating torque based on its application point. This solved the uncontrollable spinning bug.
Modular Ship Construction:
Modular Mass Aggregation (Top-Down): The OrbitalBody2D class recursively calculates the total mass of the ship by summing the base_mass of all its component children, even those nested behind non-physics containers.
Module as Root: The Module class now serves as the root for ship assemblies, managing its own list of structural pieces and components without needing container nodes.
Ship Interior & Crew Movement
Builder Plugin: The editor plugin is updated to work with this new architecture, allowing the placement of StructuralPiece nodes directly onto Module nodes.
Character Zero-G Movement: The PilotBall character now has complex, state-based movement logic to simulate zero-G physics inside the ship:
Character & Interaction Foundation:
No Control: Applies heavy drag if not overlapping a structural piece.
Zero-G Movement: The PilotBall character has a state machine for handling movement inside ship interiors, including sluggish zero-G floating and direct control on ladders.
Zero-G Interior: Allows sluggish control (simulating pushing off hullplates).
Generic Station Component: A StationComponent class has been implemented. It serves as a generic hardware terminal that characters can interact with.
Ladder Grip: Provides snappy, direct control (simulating climbing/gripping).
Data-Driven UI Architecture:
Ladder/Component Interaction: The character uses the interact input (Spacebar) to switch to the LADDER_GRIP state and uses the same input release to perform a velocity-based launch into the zero-G interior.
Databank Resource: A Databank Resource class has been created. It acts as "software," holding a reference to a UI scene that can be loaded by a station.
Ship Builder Foundation (Structural)
## II. Work-In-Progress (WIP) and Planned Systems
Component Base Class (Component.gd): Created to standardize component attachment, defining properties like grid_size and AttachmentType (Interior/Exterior).
This list details systems we have designed but are not yet fully implemented in the code.
Module Attachment Grid: The Module.gd script now exposes a get_attachment_points() method that calculates valid grid locations based on the placement and type of underlying structural pieces (Hullplate, Bulkhead).
System Migration to Databanks:
II. Work-In-Progress (WIP) and Planned Systems
Helm/Flight Controls: The logic from the old ThrusterController.gd needs to be moved into a HelmUI.tscn scene and driven by a HelmDatabank.
System
Navigation Computer: The UI has been moved, but the extensive planning and calculation logic from NavigationComputer.gd needs to be transferred to NavUI.gd.
Status
Fuel and Life Support: The FuelSystem and LifeSupport nodes are still part of the old Spaceship.tscn. They need to be fully redesigned as Component classes (e.g., FuelTank, AtmosphereProcessor).
Next Steps / Required Work
Component Wiring System:
Ship Builder Plugin (UI)
Signal/Socket Advertising: Components and Databanks need to be updated with get_input_sockets() and get_output_signals() functions.
WIP / Priority
Wiring Data Storage: The Module class needs a wiring_data array to store the connections made in the builder.
Integrate the new attachment logic (Module.get_attachment_points()) into the CustomGrid.gd to visualize placement points. Implement the logic for the Component Tab in the dock.
Builder UI: A visual wiring interface needs to be added to the module builder plugin.
Physics Cleanup (Celestial)
Orbital Stability Test:
WIP / Required
Ghost Simulator: A GhostSimulator class needs to be created to run predictive, in-memory physics calculations.
Unify the inheritance of all celestial bodies (Star, Planet, etc.) to inherit from OrbitalBody2D, removing reliance on deprecated RigidBody2D base nodes.
Test Runner: An orbital_stability_test.tscn scene and script are needed to orchestrate the test, compare live vs. ghost results, and generate a report.
Collision Handling
Full Spaceship Class Retirement: The final step will be to delete Spaceship.tscn and Spaceship.gd once all their logic and systems have been successfully migrated to the new modular architecture.
Planned
## Development Progress Report 14/10 - 25
### Overview
The project has successfully undergone a foundational architectural refactor. The legacy monolithic Spaceship class has been deprecated in favor of a fully modular, component-based architecture designed for multiplayer scalability. The core gameplay loop of a player spawning, possessing a character, using a station, and controlling a ship's systems via a diegetic UI is now functional. The project is now entering "Cycle 2" of development, focusing on unifying the physics system and migrating the remaining legacy gameplay logic into the new architecture.
Implement the _recalculate_collision_shape() function in Module.gd to merge the structural piece collision shapes into a single, cohesive shape for the root ship.
### ✅ Implemented Systems & Features
#### 1. Core Architecture
Modular Ships: Ships are now built around a root Module class which extends OrbitalBody2D. The Module dynamically understands its structure by finding its Component and StructuralPiece children, removing the need for rigid container nodes.
Hull Sealing Logic
Custom Physics Body: The OrbitalBody2D class serves as the base for all physical objects in the simulation, including ship parts and modules. It correctly handles force routing from child components to the root physics body and includes a robust, deferred check to warn if a child class forgets to call super() in its _ready function.
Designed
Dynamic Inertia Calculation: The ship's moment of inertia is now calculated realistically based on the mass and distribution of all its component parts in local space, leading to more authentic rotational physics.
Implement the pressure and hull sealing logic using the HullVolume nodes.
#### 2. Player Control & Multiplayer Foundation
PlayerController/Pawn Architecture: A multiplayer-ready control scheme has been implemented.
Scaled Gravity Implementation
The PlayerController class is responsible for capturing raw input and sending it to the server via RPCs.
Designed
The PilotBall (the "Pawn") is now a "dumb" character that only acts on commands received from its controller, with all direct calls to the Input singleton successfully removed.
Implement the SCALE_FACTOR and ScaledOrbitalBody2D inheritance to test the multi-scale simulation (low-speed local movement vs. high-speed orbital movement).
Dynamic Spawning & Possession: The GameManager now manages the game's startup sequence. It dynamically spawns a PlayerController and a default ship (Tube.tscn), and then correctly "possesses" the PilotBall within the ship with its corresponding controller.
Local Server Initialization: The GameManager correctly initializes a local ENet server, which enables the multiplayer authority system (is_multiplayer_authority()) to function correctly in a single-player testing environment.
#### 3. Station & UI Systems
Modular UI Framework: The "databank" system has been fully implemented and separated into three distinct resource types:
ControlPanel: A resource representing a physical UI element (e.g., a screen, a lever).
Databank: A resource representing a "datashard" which contains pure logic in a script.
SystemStation: The physical station component that acts as a "chassis," hosting panels and databanks.
Persistent Station Logic: The SystemStation has been refactored to instantiate datashard logic (_ready) once for its entire lifetime, allowing for background processing. UI Panels are created ephemerally only when a player occupies the station.
Functional Helm: The helm is partially migrated.
A HelmLogicShard contains the attitude-hold (PD controller) and calibration logic.
ControlPanels for a throttle lever, buttons, and a status readout are functional.
The station correctly wires the panels to the helm shard at runtime, allowing the player to control the ship's main engine and RCS thrusters.
Functional Sensor Display: The sensor/map system is partially migrated.
A SensorSystemShard is responsible for gathering all trackable bodies in the system.
A refactored SensorPanel acts as a "dumb" display that visualizes the "sensor feed" signal it receives.
The ShipStatusShard correctly displays the ship's velocity and rotational data on the ReadoutScreen and also displays the currently selected target from the map.
### ❌ Not Yet Implemented / Pending Tasks
#### 1. Physics & Simulation (Cycle 2 Priority)
CelestialBody Refactor: All celestial bodies (Planets, Moons, etc.) still inherit from Godot's RigidBody2D and use the _integrate_forces callback. They must be refactored to extend our custom OrbitalBody2D to create a single, stable physics simulation.
Astronomical vs. Ship Scale: A system for scaling forces needs to be designed and implemented to allow massive planets and lightweight ships to coexist and interact realistically within the same simulation.
Simulation Stability Test: The proposed GhostSimulator and test runner for verifying long-term orbital stability has not yet been created.
Interior Physics: The simulation does not yet account for how the ship's acceleration affects characters or loose objects inside it.
#### 2. System & Feature Migration
Full Helm/Nav Migration: The complex maneuver planning logic (e.g., Hohmann transfers) still resides in the legacy navigation_computer.gd script and needs to be migrated into one or more Databank shards.
Thruster Refactor: The Thruster component still uses a simple turn_on()/turn_off() model. It needs to be refactored to accept a variable set_throttle(value) command for more precise control.
Retirement of Legacy Scenes/Scripts: The old Spaceship.tscn, spaceship.gd, thruster_controller.gd, and navigation_computer.gd files are still in the project and need to be fully removed once their logic is migrated.
#### 3. Planned Features (Future Cycles)
Wiring System: The foundational classes exist, but the in-game system for players to visually wire panels to databanks, and the editor tools to support this, have not been started.
Character & Movement: The current PilotBall is a placeholder. The planned humanoid character, grapple points, and EVA gameplay are not yet implemented.
Core Gameplay Systems: The foundational systems for Electricity, Life Support (pressurization), Fuel, Character Damage, and Inventory/Pickupable Objects have not yet been created.
Multiplayer Connectivity: While the architecture supports it, the UI and logic for multiple players to connect to a server (e.g., a main menu with a "Join Game" option) do not yet exist.
## Project Development Status Update 16/10 - 25
### Overview
The project has successfully completed a major architectural refactor, establishing a stable and scalable foundation for the simulation. The core physics model has been unified under a custom OrbitalBody2D class and a hierarchical Barycenter system, which has resolved previous orbital instabilities. The ship's control systems are being migrated to a flexible, data-driven "databank" architecture, and the UI is now managed by a robust grid-based layout system. The focus can now shift to migrating the remaining legacy systems and building out core gameplay features on this new foundation.
### ✅ Implemented Systems & Features
#### 1. Hierarchical Physics Simulation (Barycenter Architecture)
Global & Local Grids: The simulation is now anchored by a StarSystem root node, which defines the global grid. Procedurally generated planetary systems are encapsulated within Barycenter nodes, which act as moving "local grids" for their contents. This has stabilized the orbits of moons and planets.
Physics Roles: A clear distinction has been made between physics actors and passive bodies.
Barycenter nodes are the primary physics objects in the global simulation, inheriting from OrbitalBody2D and responding to gravitational forces.
Celestial bodies (Star, Planet, Moon) are now simple Node2Ds that provide mass data to their parent Barycenter but do not run their own physics integration, solving the "triple velocity" bug.
Centralized Physics Loop: All gravity calculations are now managed by the OrbitalMechanics singleton in a multi-stage _physics_process loop, which handles global (Barycenter-to-Star) and local (Moon-to-Planet) interactions separately.
#### 2. Procedural Generation & Player Spawn
Generator as a Tool: The StarSystemGenerator has been refactored into a RefCounted class that acts as a factory, cleanly separating the generation process from the final StarSystem product.
Stable Orbit Placement: The generator now uses astrophysical concepts like the Roche Limit and Hill Sphere (abstracted into helper functions in OrbitalMechanics) to procedurally place planets and moons in stable, non-overlapping orbits.
Lagrange Point Spawning: The player ship is now correctly spawned at the L4 or L5 Lagrange point of the outermost planet, with the proper initial velocity to maintain a stable position.
#### 3. Data-Driven Ship Systems (Databanks)
Autopilot Migration: The core logic for planning and executing maneuvers has been successfully migrated from the legacy ThrusterController into a series of decoupled databank shards:
NavSelectionDatabank: Stores the current navigation target.
ManeuverPlannerDatabank: Calculates maneuver burn plans (e.g., Hohmann transfers).
AutopilotDatabank: Executes the steps of a received plan.
Modular UI Layout: The SystemStation now functions as a layout manager, instancing and positioning UI panels based on grid data defined in ControlPanel resources. This has removed hardcoded positions and allows for flexible, data-driven UI configurations.
#### 4. Orbit Projection & Debugging
Unified Projection Function: The OrbitalMechanics library now contains a single, generalized project_n_body_paths function. This function can run a "ghost simulation" on any arbitrary set of bodies in either local or global space to generate predictive orbital paths for the map panel.
Orrery View: A dedicated debugging tool, the OrreryView scene, has been created to provide a clean, interactive chart for inspecting procedurally generated star systems without the interference of game UI or camera logic.
### ⏳ Planned & Discussed Future Implementations
#### 1. Advanced Physics Optimization
Centralized N-Body Calculation: The plan is to have the OrbitalMechanics singleton manage all gravity calculations in a single, authoritative loop each frame. This will enable advanced optimizations and debugging, such as a "force queue" to prevent calculation errors.
Sphere of Influence (SOI) Model: For dynamic objects like the player's ship, we will implement an SOI system. The ship will calculate its gravity against the full system hierarchy when in "deep space" but will switch to calculating against only the local bodies (e.g., a planet and its moons) when it enters a Barycenter's sphere of influence.
Performance Culling & Caching: For performance-intensive scenarios like asteroid belts, we've discussed implementing timers to cache and reuse negligible force calculations over several frames, only recalculating when necessary.
#### 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.
## 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.

View File

@ -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
View 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 were 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)`)

View File

@ -327,8 +327,8 @@ func _place_component_from_preview():
undo_redo.create_action("Place Component")
undo_redo.add_do_method(target_module, "attach_component", component_to_place, closest_point.position, closest_point.piece)
undo_redo.add_undo_method(target_module, "remove_child", component_to_place)
undo_redo.add_do_method(target_module, "_update_mass_and_inertia")
undo_redo.add_undo_method(target_module, "_update_mass_and_inertia")
undo_redo.add_do_method(target_module, "recalculate_physical_properties")
undo_redo.add_undo_method(target_module, "recalculate_physical_properties")
undo_redo.commit_action()
preview_node.global_position = closest_point.position

6
eva_suit_controller.tscn Normal file
View 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")

View File

@ -1,23 +1,6 @@
[gd_scene load_steps=8 format=3 uid="uid://dogqi2c58qdc0"]
[gd_scene load_steps=2 format=3 uid="uid://dogqi2c58qdc0"]
[ext_resource type="Script" uid="uid://j3j483itissq" path="res://scripts/star_system_generator.gd" id="1_h2yge"]
[ext_resource type="PackedScene" uid="uid://5uqp4amjj7ww" path="res://scenes/celestial_bodies/star.tscn" id="2_7mycd"]
[ext_resource type="PackedScene" uid="uid://clt4qlsjcfgln" path="res://scenes/celestial_bodies/planet.tscn" id="3_272bh"]
[ext_resource type="PackedScene" uid="uid://74ppvxcw8an4" path="res://scenes/celestial_bodies/moon.tscn" id="4_5vw27"]
[ext_resource type="PackedScene" uid="uid://dm3s33o4xhqfv" path="res://scenes/celestial_bodies/station.tscn" id="5_kek77"]
[ext_resource type="PackedScene" uid="uid://bawsujtlpmh5r" path="res://scenes/celestial_bodies/asteroid.tscn" id="6_4c57u"]
[ext_resource type="PackedScene" uid="uid://dlck1lyrn1xvp" path="res://scenes/ship/spaceship.tscn" id="7_5vw27"]
[ext_resource type="Script" uid="uid://bkcouefvi7iup" path="res://scripts/star_system.gd" id="1_ig7tw"]
[node name="StarSystem" type="Node2D"]
script = ExtResource("1_h2yge")
min_planets = 1
max_planets = 4
max_moons = 10
max_asteroid_belts = 2
max_star_stations = 0
star_scene = ExtResource("2_7mycd")
planet_scene = ExtResource("3_272bh")
moon_scene = ExtResource("4_5vw27")
station_scene = ExtResource("5_kek77")
asteroid_scene = ExtResource("6_4c57u")
spaceship_scene = ExtResource("7_5vw27")
script = ExtResource("1_ig7tw")

View File

@ -1,49 +1,145 @@
[gd_scene load_steps=4 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://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="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="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="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
script = ExtResource("1_nqe0s")
physics_mode = 1
mass = 1.0
inertia = 0.0
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="StructuralContainer" type="Node2D" parent="."]
[node name="HullVolumeContainer" type="Node2D" parent="."]
[node name="AtmosphereVisualizer" type="Node2D" parent="."]
[node name="Hullplate" parent="." instance=ExtResource("2_foqop")]
physics_interpolation_mode = 2
is_pressurized = false
base_mass = 0.0
[node name="@StaticBody2D@30634" parent="." instance=ExtResource("2_foqop")]
physics_interpolation_mode = 2
position = Vector2(0, 100)
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="@StaticBody2D@30635" parent="." instance=ExtResource("2_foqop")]
physics_interpolation_mode = 2
position = Vector2(0, -100)
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="Bulkhead" parent="." instance=ExtResource("4_dmrms")]
physics_interpolation_mode = 2
position = Vector2(-50, 100)
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="@StaticBody2D@30636" parent="." instance=ExtResource("4_dmrms")]
physics_interpolation_mode = 2
position = Vector2(-50, 0)
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="@StaticBody2D@30637" parent="." instance=ExtResource("4_dmrms")]
physics_interpolation_mode = 2
position = Vector2(-50, -100)
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="@StaticBody2D@30638" parent="." instance=ExtResource("4_dmrms")]
physics_interpolation_mode = 2
position = Vector2(50, -100)
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="@StaticBody2D@30639" parent="." instance=ExtResource("4_dmrms")]
physics_interpolation_mode = 2
position = Vector2(0, -150)
rotation = 1.5708
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="@StaticBody2D@30640" parent="." instance=ExtResource("4_dmrms")]
physics_interpolation_mode = 2
position = Vector2(0, 150)
rotation = 4.71239
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="@StaticBody2D@30641" parent="." instance=ExtResource("4_dmrms")]
physics_interpolation_mode = 2
position = Vector2(50, 100)
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="@StaticBody2D@30642" parent="." instance=ExtResource("4_dmrms")]
physics_interpolation_mode = 2
position = Vector2(50, 0)
is_pressurized = false
health = 0.0
base_mass = 0.0
[node name="Station" parent="." instance=ExtResource("5_nqe0s")]
position = Vector2(0, -10)
panel_scenes = Array[PackedScene]([ExtResource("11_erhv3"), ExtResource("11_erhv3"), ExtResource("12_q1rtr"), ExtResource("10_px2ne"), ExtResource("13_rsa1x")])
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
[node name="Thruster" parent="." instance=ExtResource("12_vmx8o")]
position = Vector2(-95, -130)
rotation = 1.5708
main_thruster = false
physics_mode = 2
[node name="Thruster2" parent="." instance=ExtResource("12_vmx8o")]
position = Vector2(-95, 130)
rotation = 1.5708
main_thruster = false
physics_mode = 2
[node name="Thruster3" parent="." instance=ExtResource("12_vmx8o")]
position = Vector2(95, 130)
rotation = -1.5708
main_thruster = false
physics_mode = 2
[node name="Thruster4" parent="." instance=ExtResource("12_vmx8o")]
position = Vector2(95, -130)
rotation = -1.5708
main_thruster = false
physics_mode = 2
[node name="MainEngine" parent="." instance=ExtResource("12_vmx8o")]
position = Vector2(0, 195)
max_thrust = 10.0
physics_mode = 2
[node name="Spawner" parent="." instance=ExtResource("13_83bu1")]
position = Vector2(0, 27)
physics_mode = 2

View File

@ -12,14 +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"
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]
@ -57,22 +66,118 @@ time_reset={
"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":82,"key_label":0,"unicode":114,"location":0,"echo":false,"script":null)
]
}
move_up={
"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_down={
"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)
]
}
move_left={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":65,"key_label":0,"unicode":97,"location":0,"echo":false,"script":null)
]
}
move_right={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":68,"key_label":0,"unicode":100,"location":0,"echo":false,"script":null)
]
}
interact={
"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)
]
}
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]
2d_physics/layer_1="ship_hull"
2d_render/layer_2="UI_Panels"
2d_physics/layer_1="hullplates"
2d_physics/layer_2="ship_components"
2d_physics/layer_3="celestial_bodies"
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]

View File

@ -7,7 +7,7 @@ signal follow_requested(body: Node2D)
@onready var name_label: Label = $NameLabel
var body_reference: Node2D
var body_reference: OrbitalBody2D
var dot_color: Color = Color.WHITE
var hover_tween: Tween
@ -27,17 +27,18 @@ func _ready() -> void:
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
func initialize(body: Node2D):
func initialize(body: OrbitalBody2D):
body_reference = body
name_label.text = body.name
if body is OrbitalBody2D:
if body is Star:
dot_color = Color.GOLD
elif body is Planet:
dot_color = Color.DODGER_BLUE
elif body is Moon:
dot_color = Color.PURPLE
else:
dot_color = Color.CYAN
elif body is CelestialBody:
match body.get_class_name():
"Star": dot_color = Color.GOLD
"Planet": dot_color = Color.DODGER_BLUE
"Moon": dot_color = Color.PURPLE
self.tooltip_text = _generate_tooltip_text()
@ -79,7 +80,8 @@ func _gui_input(event: InputEvent) -> void:
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
emit_signal("selected", body_reference)
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
emit_signal("follow_requested", body_reference)
print(body_reference)
follow_requested.emit(body_reference)
# No changes are needed here; the tweens will automatically use the new setter function.
@ -96,29 +98,34 @@ func _on_mouse_exited():
func _generate_tooltip_text() -> String:
var info = [body_reference.name]
if body_reference is CelestialBody:
var celestial = body_reference as CelestialBody
if is_instance_valid(celestial.primary):
var mu = OrbitalMechanics.G * celestial.primary.mass
var r = celestial.global_position.distance_to(celestial.primary.global_position)
var period_seconds = TAU * sqrt(pow(r, 3) / mu)
info.append("Orbital Period: %s" % _format_seconds_to_mmss(period_seconds))
if body_reference is Planet:
var planet_system = body_reference.get_parent() as Barycenter
var period_seconds = OrbitalMechanics.get_orbital_time_in_seconds(planet_system, GameManager.get_system_data().star)
info.append("Orbital Period: %s" % _format_seconds_to_mmss(period_seconds))
var moon_count = 0
for child in celestial.get_children():
if child is CelestialBody and child.get_class_name() == "Moon":
for child in planet_system.get_internal_attractors():
if child is Moon:
moon_count += 1
if moon_count > 0:
info.append("Moons: %d" % moon_count)
if body_reference is Spaceship:
if body_reference is Moon:
var planet_system = body_reference.get_parent() as Barycenter
var period_seconds = OrbitalMechanics.get_orbital_time_in_seconds(body_reference as Moon, planet_system)
info.append("Orbital Period: %s" % _format_seconds_to_mmss(period_seconds))
if body_reference is Module:
info.append("Class: Player Vessel")
info.append("Mass: %.0f kg" % body_reference.mass)
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
View 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

View File

@ -0,0 +1 @@
uid://d3g84xgbh8nlp

44
scenes/UI/ui_window.tscn Normal file
View 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

View File

@ -1,5 +1,5 @@
class_name Asteroid
extends CelestialBody
extends OrbitalBody2D
# The orbital radius for this asteroid.
var orbital_radius: float
@ -11,7 +11,7 @@ func get_class_name() -> String:
func _ready() -> void:
# An Asteroid has negligible mass for physics calculations.
#mass = 0.001
radius = 5.0
#radius = 5.0
# You can set a default texture here.
# texture = preload("res://assets/asteroid_texture.png")

View File

@ -1,7 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://bawsujtlpmh5r"]
[ext_resource type="Script" uid="uid://c816xae77cbmq" path="res://scenes/celestial_bodies/asteroid.gd" id="1_akfqu"]
[ext_resource type="Script" uid="uid://0isnsk356que" path="res://scripts/orbital_body_2d.gd" id="1_4q05e"]
[node name="Asteroid" type="RigidBody2D"]
script = ExtResource("1_akfqu")
metadata/_custom_type_script = "uid://c816xae77cbmq"
[node name="Asteroid" type="Node2D"]
script = ExtResource("1_4q05e")
base_mass = 50000.0
metadata/_custom_type_script = "uid://0isnsk356que"

View File

@ -1,5 +1,5 @@
class_name Moon
extends CelestialBody
extends OrbitalBody2D
# The orbital radius for this moon.
var orbital_radius: float
@ -10,8 +10,6 @@ func get_class_name() -> String:
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# A Moon has a smaller mass than a planet.
#mass = 100.0
radius = 5.0
# You can set a default texture here.
# texture = preload("res://assets/moon_texture.png")

View File

@ -2,6 +2,7 @@
[ext_resource type="Script" uid="uid://b1xsx7er22nxn" path="res://scenes/celestial_bodies/moon.gd" id="1_530pw"]
[node name="Moon" type="RigidBody2D"]
[node name="Moon" type="Node2D"]
script = ExtResource("1_530pw")
metadata/_custom_type_script = "uid://bn1u2xood3vs6"
base_mass = 1e+06
metadata/_custom_type_script = "uid://0isnsk356que"

View File

@ -1,5 +1,5 @@
class_name Planet
extends CelestialBody
extends OrbitalBody2D
# The orbital radius for this planet.
var orbital_radius: float
@ -9,10 +9,6 @@ func get_class_name() -> String:
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# A Planet has a smaller mass than a star.
#mass = 1000.0
radius = 10.0
# You can set a default texture here.
# texture = preload("res://assets/planet_texture.png")

View File

@ -2,6 +2,9 @@
[ext_resource type="Script" uid="uid://5f6ipgu65urb" path="res://scenes/celestial_bodies/planet.gd" id="1_cktii"]
[node name="Planet" type="RigidBody2D"]
[node name="Planet" type="Node2D"]
script = ExtResource("1_cktii")
metadata/_custom_type_script = "uid://bn1u2xood3vs6"
base_mass = 2.5e+07
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]

View File

@ -1,5 +1,5 @@
class_name Star
extends CelestialBody
extends OrbitalBody2D
func get_class_name() -> String:
return "Star"
@ -7,8 +7,6 @@ func get_class_name() -> String:
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# A Star has no primary and a very large mass.
primary = null
radius = 100.0
# You can set a default texture here, or assign it in the Inspector.
# texture = preload("res://assets/star_texture.png")

View File

@ -1,7 +1,15 @@
[gd_scene load_steps=2 format=3 uid="uid://5uqp4amjj7ww"]
[gd_scene load_steps=3 format=3 uid="uid://5uqp4amjj7ww"]
[ext_resource type="Script" uid="uid://um2sfghmii42" path="res://scripts/star.gd" id="1_mcqwg"]
[ext_resource type="Script" uid="uid://um2sfghmii42" path="res://scenes/celestial_bodies/star.gd" id="1_mcqwg"]
[node name="Star" type="RigidBody2D"]
[sub_resource type="CircleShape2D" id="CircleShape2D_508pf"]
radius = 200.0
[node name="Star" type="Node2D"]
script = ExtResource("1_mcqwg")
metadata/_custom_type_script = "uid://bn1u2xood3vs6"
base_mass = 5e+08
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_508pf")
debug_color = Color(0.863865, 0.471779, 0.162305, 1)

View File

@ -1,5 +1,5 @@
class_name Station
extends CelestialBody
extends OrbitalBody2D
# The orbital radius for this station.
var orbital_radius: float
@ -10,8 +10,6 @@ func get_class_name() -> String:
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# A Station has negligible mass for physics calculations.
#mass = 0.001
radius = 1.0
# You can set a default texture here.
# texture = preload("res://assets/station_texture.png")

View File

@ -1,7 +1,8 @@
[gd_scene load_steps=2 format=3 uid="uid://dm3s33o4xhqfv"]
[ext_resource type="Script" uid="uid://ulw61oxppwdu" path="res://scripts/station.gd" id="1_rod8h"]
[ext_resource type="Script" uid="uid://ulw61oxppwdu" path="res://scenes/celestial_bodies/station.gd" id="1_rod8h"]
[node name="Station" type="RigidBody2D"]
[node name="Station" type="Node2D"]
script = ExtResource("1_rod8h")
metadata/_custom_type_script = "uid://bn1u2xood3vs6"
base_mass = 5000.0
metadata/_custom_type_script = "uid://0isnsk356que"

View File

@ -3,25 +3,33 @@ 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
const INTERIOR_SLUGGISH_ACCEL: float = 0.15 # Low acceleration, simulating mass and small push
const INTERIOR_SLUGGISH_ACCEL: float = 5 # Low acceleration, simulating mass and small push
# When gripping a ladder (high friction, direct control)
const LADDER_SPEED: float = 350.0
const LADDER_ACCEL: float = 0.9 # High acceleration, simulating direct grip
const LADDER_SPEED: float = 100.0
const LADDER_ACCEL: float = 20 # High acceleration, simulating direct grip
@onready var camera: Camera2D = $Camera2D
@onready var overlap_area: Area2D = $OverlapDetector
@onready var ui_container: Control = $CanvasLayer/UIContainer
var nearby_station: SystemStation = null
var current_station: SystemStation = null
# --- State Variables ---
enum MovementState {
NO_CONTROL,
ZERO_G_INTERIOR,
LADDER_GRIP
LADDER_GRIP,
IN_STATION
}
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) ---
@ -30,30 +38,37 @@ var overlapping_modules: int = 0
# --- Ladder Constants ---
const LAUNCH_VELOCITY: float = 300.0
var _movement_input: Vector2 = Vector2.ZERO
var _interact_just_pressed: bool = false
var _interact_held: bool = false
# --- PUBLIC INPUT METHODS (Called by the PlayerController) ---
func set_movement_input(input_dir: Vector2):
_movement_input = input_dir
func set_interaction_input(just_pressed: bool, is_held: bool):
_interact_just_pressed = just_pressed
_interact_held = is_held
# --- New: Physics Initialization (Assuming CharacterBody2D is parented to the scene root or Ship) ---
# NOTE: CharacterBody2D cannot inherit OrbitalBody2D, so we manage its velocity manually.
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.
# Placeholder: Assuming the PilotBall has an Area2D named 'OverlapChecker'
var overlap_checker = find_child("OverlapChecker")
if overlap_checker:
overlap_checker.body_entered.connect(on_body_entered)
overlap_checker.body_exited.connect(on_body_exited)
# Ensure this action is set in project settings: "interact" mapped to Space.
if !InputMap.has_action("interact"):
push_error("Missing 'interact' input action for ladder logic.")
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)
overlap_area.area_exited.connect(_on_station_area_exited)
camera.make_current()
func on_body_entered(body: Node2D):
# Detect Modules (which all inherit OrbitalBody2D via StructuralPiece)
if body is StructuralPiece:
overlapping_modules += 1
# Detect Ladders
# Detect Ladders
if body is Ladder:
ladder_area = body.find_child("ClimbArea") # Assuming the Ladder has a specific Area2D for climbing
@ -66,37 +81,82 @@ func on_body_exited(body: Node2D):
ladder_area = null
is_grabbing_ladder = false # Force detach if the ladder moves away
# --- NEW: Functions to be called by the Station ---
func enter_station_state():
current_state = MovementState.IN_STATION
velocity = Vector2.ZERO # FIX: Stop all movement when entering a station
func _physics_process(delta):
# 1. Update State based on environment
_update_movement_state()
func exit_station_state():
# When leaving, transition to a sensible default state.
current_state = MovementState.ZERO_G_INTERIOR
var input_dir = Input.get_vector("move_left", "move_right", "move_up", "move_down")
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
# Reset input flags for the next frame
_interact_just_pressed = false
_interact_held = false
# The 'input_dir' now comes from our variable, not the Input singleton.
var input_dir = _movement_input
match current_state:
MovementState.NO_CONTROL:
# Apply heavy drag to simulate floating in space without external push
_apply_drag(EXTERIOR_DRAG_FACTOR)
MovementState.ZERO_G_INTERIOR:
# Sluggish movement: player is pushing off nearby walls/hullplates
_sluggish_movement(input_dir, delta)
MovementState.LADDER_GRIP:
# Snappy movement: direct control and high acceleration
_ladder_movement(input_dir, delta)
# 2. Handle Ladder Grab/Launch Input
_handle_ladder_input(input_dir)
# Reset input for the next frame
_movement_input = Vector2.ZERO
move_and_slide()
# 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
# Priority 1: Disengage from a station if we are in one.
if current_station:
current_station.disengage(self)
current_station = null
return
# Priority 2: Occupy a nearby station if we are not in one.
elif is_instance_valid(nearby_station):
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()
if launch_direction == Vector2.ZERO:
# Default launch: use the character's forward direction
launch_direction = Vector2.UP.rotated(rotation)
velocity = launch_direction * LAUNCH_VELOCITY
# Immediately switch to zero-G interior state
is_grabbing_ladder = false
current_state = MovementState.ZERO_G_INTERIOR
# --- State Machine Update ---
func _update_movement_state():
# Priority 1: Ladder Grip
if ladder_area and Input.is_action_pressed("interact"):
# This now checks the variable instead of the Input singleton.
if ladder_area and _interact_held:
is_grabbing_ladder = true
current_state = MovementState.LADDER_GRIP
return
@ -105,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
@ -119,38 +179,27 @@ func _update_movement_state():
# --- Movement Implementations ---
func _apply_drag(factor: float):
# Gently slow down the velocity (simulating environmental drag)
velocity = velocity.lerp(Vector2.ZERO, factor)
func _sluggish_movement(input_dir: Vector2, delta: float):
# Simulates pushing off the wall: slow acceleration, but minimal drag
var target_velocity = input_dir * INTERIOR_SLUGGISH_SPEED
velocity = velocity.lerp(target_velocity, INTERIOR_SLUGGISH_ACCEL)
var target_velocity = input_dir * INTERIOR_SLUGGISH_ACCEL
velocity = velocity + target_velocity * delta
#velocity.lerp(velocity + interi, INTERIOR_SLUGGISH_ACCEL)
func _ladder_movement(input_dir: Vector2, delta: float):
# Simulates direct grip: fast acceleration, perfect control
var target_velocity = input_dir * LADDER_SPEED
velocity = velocity.lerp(target_velocity, LADDER_ACCEL)
velocity = velocity.lerp(target_velocity, LADDER_ACCEL * delta)
# --- New Functions for Station Interaction ---
func _on_station_area_entered(area: Area2D):
if area.get_parent() is SystemStation:
nearby_station = area.get_parent()
print("Near station: ", nearby_station.name)
# --- Ladder Input and Launch Logic ---
func _on_station_area_exited(area: Area2D):
if area.get_parent() == nearby_station:
nearby_station = null
func _handle_ladder_input(input_dir: Vector2):
# If currently grabbing, SPACE press is handled in _update_movement_state
if current_state == MovementState.LADDER_GRIP:
if Input.is_action_just_released("interact"):
# Launch the player away from the ladder
# Determine launch direction: opposite of input, or default forward
var launch_direction = -input_dir.normalized()
if launch_direction == Vector2.ZERO:
# Default launch: use the character's forward direction (e.g., rotation 0)
launch_direction = Vector2.UP.rotated(rotation)
velocity = launch_direction * LAUNCH_VELOCITY
# Immediately switch to zero-G interior state
is_grabbing_ladder = false
current_state = MovementState.ZERO_G_INTERIOR
# Stations will call this to get the node where they should place their UIs.
func get_ui_container() -> Control:
return ui_container

View File

@ -1,10 +1,14 @@
[gd_scene load_steps=3 format=3 uid="uid://chgycmkkaf7jv"]
[gd_scene load_steps=4 format=3 uid="uid://chgycmkkaf7jv"]
[ext_resource type="Script" uid="uid://dxngvoommn5f1" path="res://scenes/characters/pilot_ball.gd" id="1_rhbna"]
[sub_resource type="CircleShape2D" id="CircleShape2D_6jclb"]
[sub_resource type="CircleShape2D" id="CircleShape2D_rhbna"]
[node name="PilotBall" type="CharacterBody2D"]
collision_layer = 32
collision_mask = 16
script = ExtResource("1_rhbna")
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
@ -12,3 +16,21 @@ shape = SubResource("CircleShape2D_6jclb")
debug_color = Color(0.61528, 0.358023, 1, 1)
[node name="Sprite2D" type="Sprite2D" parent="."]
[node name="Camera2D" type="Camera2D" parent="."]
zoom = Vector2(4, 4)
[node name="OverlapDetector" type="Area2D" parent="."]
[node name="CollisionShape2D" type="CollisionShape2D" parent="OverlapDetector"]
shape = SubResource("CircleShape2D_rhbna")
[node name="CanvasLayer" type="CanvasLayer" parent="."]
[node name="UIContainer" type="Control" parent="CanvasLayer"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2

View File

@ -0,0 +1,37 @@
extends Node
class_name PlayerController
# TODO: Change this to custom pawn type
var possessed_pawn: Node # The character this controller is currently driving
func _ready():
# --- FIX: Manually enable input processing for this node ---
set_process_input(true)
func _physics_process (delta):
if not is_multiplayer_authority():
return
# 1. Gather all input states for this frame.
var input_dir = Input.get_vector("move_left", "move_right", "move_up", "move_down")
var is_interact_just_pressed = Input.is_action_just_pressed("interact")
var is_interact_held = Input.is_action_pressed("interact")
#print(is_interact_just_pressed)
#print(input_dir)
# 2. Send the collected input state to the server via RPC.
server_process_input.rpc_id(1, input_dir, is_interact_just_pressed, is_interact_held)
@rpc("any_peer", "call_local")
func server_process_input(input_dir: Vector2, is_interact_just_pressed: bool, is_interact_held: bool):
if is_instance_valid(possessed_pawn):
possessed_pawn.set_movement_input(input_dir)
# Pass both interact states to the pawn
possessed_pawn.set_interaction_input(is_interact_just_pressed, is_interact_held)
func possess(pawn_to_control: Node):
possessed_pawn = pawn_to_control
reparent(pawn_to_control, false)
self.owner = pawn_to_control
print("PlayerController possessed: ", possessed_pawn.name)

View File

@ -0,0 +1 @@
uid://dmhwqmbwk0t8k

View File

@ -0,0 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://dnre6svquwdtb"]
[ext_resource type="Script" uid="uid://dmhwqmbwk0t8k" path="res://scenes/characters/player_controller.gd" id="1_b8jga"]
[node name="PlayerController" type="Node"]
script = ExtResource("1_b8jga")

View File

@ -0,0 +1,26 @@
# res://scenes/orrey_view/debug_marker.gd
class_name DebugMarker
extends Control
signal marker_selected(body: Node2D)
@onready var icon: ColorRect = $Icon
@onready var label: Label = $Label
var body_reference: Node2D
func initialize(body: Node2D):
body_reference = body
label.text = body.name
# Color-code the marker for easy identification
if body is Barycenter:
icon.color = Color.YELLOW
elif body.get_parent() is Barycenter and body.get_parent().name == "StarBarycenter":
icon.color = Color.ORANGE
else:
icon.color = Color.CYAN
func _gui_input(event: InputEvent):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
emit_signal("marker_selected", body_reference)

View File

@ -0,0 +1 @@
uid://b5tcvtjh2050f

View File

@ -0,0 +1,18 @@
[gd_scene load_steps=2 format=3 uid="uid://b8fmmp4axba1j"]
[ext_resource type="Script" uid="uid://b5tcvtjh2050f" path="res://scenes/orrey_view/debug_marker.gd" id="1_2uxs6"]
[node name="DebugMarker" type="Control"]
layout_mode = 3
anchors_preset = 0
script = ExtResource("1_2uxs6")
[node name="Icon" type="ColorRect" parent="."]
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
[node name="Label" type="Label" parent="."]
layout_mode = 0
offset_right = 40.0
offset_bottom = 23.0

View File

@ -0,0 +1,74 @@
# res://scenes/debug/orrery_view.gd
extends Node2D
@export_group("Generation Assets")
@export var star_scene: PackedScene
@export var planet_scene: PackedScene
@export var moon_scene: PackedScene
@export var barycenter_scene: PackedScene
@export var spaceship_scene: PackedScene # Add this even if unused for now
@export_group("Orrery Settings")
@export var debug_marker_scene: PackedScene # Link your new DebugMarker.tscn here
@onready var camera: Camera2D = $Camera2D
@onready var system_root: Node2D = $GeneratedSystem
@onready var info_label: RichTextLabel = $UI/InfoPanel/MarginContainer/InfoLabel
var markers: Dictionary = {} # Maps a body to its marker
func _ready():
# 1. Create and configure the generator tool.
var generator = StarSystemGenerator.new()
# Create a temporary StarSystem-like object to pass to the generator
var temp_system_node = Node2D.new()
temp_system_node.star_scene = star_scene
temp_system_node.planet_scene = planet_scene
temp_system_node.moon_scene = moon_scene
temp_system_node.barycenter_scene = barycenter_scene
temp_system_node.spaceship_scene = spaceship_scene
# 2. Generate the system under our dedicated root node.
var generation_result = generator.generate(temp_system_node)
system_root.add_child(temp_system_node) # Keep the generated hierarchy
# 3. Create a debug marker for every generated body.
var all_bodies = generation_result.all_bodies + generation_result.all_barycenters
for body in all_bodies:
if is_instance_valid(body):
var marker = debug_marker_scene.instantiate()
add_child(marker) # Add markers to the OrreryView, not the generated system
marker.initialize(body)
marker.marker_selected.connect(_on_marker_selected)
markers[body] = marker
func _process(_delta):
# Keep the markers synced with their body's global position.
for body in markers:
var marker = markers[body]
marker.global_position = body.global_position
func _unhandled_input(event: InputEvent):
# Simple camera controls
if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_MIDDLE:
camera.offset -= event.relative / camera.zoom.x
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_WHEEL_UP:
camera.zoom *= 1.2
elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
camera.zoom /= 1.2
func _on_marker_selected(body: Node2D):
# Update the info panel with the selected body's data.
var text = "[b]%s[/b]\n" % body.name
if body is OrbitalBody2D:
text += "Mass: %.2f\n" % body.mass
text += "Velocity: (%.2f, %.2f)\n" % [body.linear_velocity.x, body.linear_velocity.y]
text += "Position: (%.0f, %.0f)\n" % [body.global_position.x, body.global_position.y]
if body is Barycenter and is_instance_valid(body.get_parent()):
text += "Parent: %s" % body.get_parent().name
info_label.text = text

View File

@ -0,0 +1 @@
uid://cwurbqorbwtil

View File

@ -0,0 +1,22 @@
[gd_scene load_steps=2 format=3 uid="uid://bysrpulqqlic3"]
[ext_resource type="Script" uid="uid://cwurbqorbwtil" path="res://scenes/orrey_view/orrey_view.gd" id="1_4smux"]
[node name="OrreyView" type="Node2D"]
script = ExtResource("1_4smux")
[node name="GeneratedSystem" type="Node2D" parent="."]
[node name="Camera2D" type="Camera2D" parent="."]
[node name="UI" type="CanvasLayer" parent="."]
[node name="InfoPanel" type="PanelContainer" parent="UI"]
offset_right = 40.0
offset_bottom = 40.0
[node name="MarginContainer" type="MarginContainer" parent="UI/InfoPanel"]
layout_mode = 2
[node name="InfoLabel" type="RichTextLabel" parent="UI/InfoPanel/MarginContainer"]
layout_mode = 2

View File

@ -2,7 +2,8 @@
class_name Module
extends OrbitalBody2D
# REMOVED: @onready vars for containers are no longer needed.
@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
const COMPONENT_GRID_SIZE = 64.0
@ -63,13 +64,14 @@ func attach_component(component: Component, global_pos: Vector2, parent_piece: S
component.position = global_pos - global_position
component.attached_piece = parent_piece
add_child(component)
component.owner = self
_update_mass_and_inertia()
component.owner = self
component.physics_mode = PhysicsMode.ANCHORED
recalculate_physical_properties()
# --- 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
@ -92,3 +94,21 @@ func clear_module():
component.queue_free()
_recalculate_collision_shape()
# 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:
# Find the LifeSupport component and check for a breach
for child in get_children():
if child is LifeSupport: # Assuming LifeSupport becomes a Component class
child.check_for_breach(damage_position, self)
func destroy_ship():
print("%s has been destroyed!" % ship_name)
# Add explosion/destruction effects here
queue_free()

View File

@ -6,7 +6,8 @@
size = Vector2(10, 100)
[node name="Bulkhead" type="StaticBody2D"]
collision_layer = 5
collision_layer = 16
collision_mask = 60
script = ExtResource("1_1wp2n")
metadata/_custom_type_script = "uid://b7f8x2qimvn37"

View File

@ -14,6 +14,7 @@ enum AttachmentType { INTERIOR_WALL, EXTERIOR_HULL, FLOOR_OR_CEILING }
var attached_piece: StructuralPiece = null
func _ready():
super()
# OrbitalBody2D will handle mass initialization and physics setup.
pass
@ -23,3 +24,12 @@ func activate():
func deactivate():
pass
# Helper to find the main ship/module this component belongs to
func get_root_module() -> Module:
var current_node = self
while is_instance_valid(current_node):
if current_node is Module and current_node.physics_mode == PhysicsMode.COMPOSITE:
return current_node
current_node = current_node.get_parent()
return null

View File

@ -1,6 +1,6 @@
# Accelerometer.gd
class_name Accelerometer
extends ShipComponent
extends Component
# --- Tunable Sensor Properties ---
@export var spring_stiffness: float = 2000.0
@ -11,6 +11,7 @@ extends ShipComponent
@onready var rear_spring: DampedSpringJoint2D = $RearSpring
func _ready() -> void:
super()
# Configure the springs based on the exported variables
front_spring.stiffness = spring_stiffness
rear_spring.stiffness = spring_stiffness

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=4 format=3 uid="uid://cdctp617gk35b"]
[ext_resource type="Script" uid="uid://dd32rqdya446r" path="res://scenes/ship/components/accelerometer.gd" id="1_8lsml"]
[ext_resource type="Script" uid="uid://dd32rqdya446r" path="res://scenes/ship/components/hardware/accelerometer.gd" id="1_8lsml"]
[sub_resource type="RectangleShape2D" id="RectangleShape2D_8lsml"]
size = Vector2(20, 100)

View File

@ -9,6 +9,7 @@ class_name Ladder
# --- Inherited OrbitalBody2D & Component Setup ---
func _ready():
super()
# Set the base mass based on its material/size
base_mass = float(ladder_grid_height) * 25.0 # Example: 25kg per grid unit height

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=2 format=3 uid="uid://dxtxb2p7lpt51"]
[ext_resource type="Script" uid="uid://bh1t0cqdjm5ye" path="res://scenes/ship/components/ladder.gd" id="1_ygkvf"]
[ext_resource type="Script" uid="uid://bh1t0cqdjm5ye" path="res://scenes/ship/components/hardware/ladder.gd" id="1_ygkvf"]
[node name="Ladder" type="Node2D"]
script = ExtResource("1_ygkvf")

View File

@ -0,0 +1,16 @@
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()
# 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.

View File

@ -0,0 +1 @@
uid://db1u2qqihhnq4

View File

@ -0,0 +1,16 @@
[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"]
[sub_resource type="SphereShape3D" id="SphereShape3D_lldyu"]
radius = 1.0
[node name="Spawner" type="Area3D"]
script = ExtResource("1_lldyu")
[node name="CollisionShape3D" type="CollisionShape3D" parent="."]
shape = SubResource("SphereShape3D_lldyu")
[node name="MultiplayerSpawner" type="MultiplayerSpawner" parent="."]
_spawnable_scenes = PackedStringArray("uid://7yc6a07xoccy")
spawn_path = NodePath("..")

View File

@ -0,0 +1,238 @@
class_name SystemStation
extends Component
signal occupancy_changed(is_occupied: bool)
# --- Configuration ---
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[Databank] = []
var persistent_panel_instances: Array[BasePanel] = []
var occupant_panel_map: Dictionary = {}
# --- LIFECYCLE ---
func _ready():
super()
# 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
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)
# if shard_instance.has_method("initialize"):
# shard_instance.initialize(root_module)
_connect_internals([], active_shard_instances)
# Future: Connections wbetween shards and other hardware would be made here.
func _process(_delta):
# Check for disengagement for every occupant
for character in occupants:
# Note: This simple check won't work for multiple local players.
# A real implementation would need to check input for a specific player ID.
if is_instance_valid(character) and Input.is_action_just_pressed("interact"):
disengage(character)
return # Avoid modifying the array while iterating
# --- INTERACTION ---
func is_occupied() -> bool:
return not occupants.is_empty()
func occupy(character: PilotBall):
if character in occupants: return
occupants.append(character)
character.enter_station_state()
character.global_position = global_position # Move character to the station
# --- Launch UI for THIS character only ---
launch_interfaces_for_occupant(character)
occupancy_changed.emit(true)
func disengage(character: PilotBall):
if not character in occupants: return
# --- FIX: Close UI for THIS character only ---
close_interfaces_for_occupant(character)
character.exit_station_state()
occupants.erase(character)
if not is_occupied():
occupancy_changed.emit(false)
# --- UI MANAGEMENT ---
func close_interfaces_for_occupant(character: PilotBall):
if occupant_panel_map.has(character):
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: 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
var hohman_planner_shard: HohmanPlannerShard
var autopilot_shard: AutopilotShard
var nav_selection_shard: NavSelectionShard
for panel in panel_instances:
if panel is ThrottleLeverPanel:
lever_panel = panel
if panel is ButtonPanel:
button_panel = panel
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
for shard in shard_instances:
if shard is HelmLogicShard:
helm_shard = shard
if shard is ShipStatusShard:
status_shard = shard
if shard is SensorSystemShard: # Look for the new sensor shard class
sensor_shard = shard
if shard is HohmanPlannerShard:
hohman_planner_shard = shard
if shard is AutopilotShard:
autopilot_shard = shard
if shard is NavSelectionShard:
nav_selection_shard = shard
if lever_panel and helm_shard:
lever_panel.lever_value_changed.connect(helm_shard.set_throttle_input)
print("Wired: Lever -> Helm Shard (Throttle)")
# You would do the same for the button panel, connecting its signals
# to a set_rotation_input or similar function on the helm_shard.
if button_panel and helm_shard:
button_panel.connect("button_1_pressed", func(): helm_shard.set_rotation_input(1.0))
button_panel.connect("button_2_pressed", func(): helm_shard.set_rotation_input(-1.0))
button_panel.connect("button_3_pressed", helm_shard.shutdown_rcs)
button_panel.connect("button_4_pressed", helm_shard.calibrate_rcs_performance)
button_panel.connect("button_5_pressed", hohman_planner_shard.calculate_hohmann_transfer)
button_panel.connect("button_6_pressed", autopilot_shard.execute_plan)
if readout_screen and status_shard:
print("Wired: Status Shard -> Readout Screen")
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)
# to the map's "update_sensor_feed" socket.
sensor_shard.connect("sensor_feed_updated", map_panel.update_sensor_feed)
print("Wired: Sensor Shard -> Map Panel (Sensor Feed)")
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.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.target_updated)
print("Wired: Nav Selection -> Maneuver Planner")
if hohman_planner_shard and autopilot_shard:
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")

View File

@ -0,0 +1 @@
uid://2reyxkr78ra0

View File

@ -0,0 +1,24 @@
[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://cadvugf4oqgvk" path="res://scenes/ship/computer/panels/frame/panel_frame.gd" id="3_p17qi"]
[sub_resource type="CircleShape2D" id="CircleShape2D_8usqu"]
[node name="Station" type="Node2D"]
physics_interpolation_mode = 2
script = ExtResource("1_8usqu")
mass = 1.0
[node name="InteractionArea" type="Area2D" parent="."]
[node name="CollisionShape2D" type="CollisionShape2D" parent="InteractionArea"]
shape = SubResource("CircleShape2D_8usqu")
debug_color = Color(0, 0.551549, 0.918484, 0.42)
[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"

View File

@ -5,9 +5,6 @@ extends Component
@onready var pin_joint_a: PinJoint2D = $PinJointA
@onready var pin_joint_b: PinJoint2D = $PinJointB
# Get a reference to the parent ship.
@onready var ship: Spaceship = GameManager._find_parent_ship(self)
# Max force the thruster can produce (in scaled Newtons).
@export var max_thrust: float = 0.1
@ -26,23 +23,22 @@ var is_firing: bool = false
func _ready() -> void:
super()
# TODO: Figure out where this should go if anywhere
# --- Self-connecting logic ---
if ship and ship.get_path():
var ship_path = ship.get_path()
var self_path = get_path()
# --- Configure Pin Joint A ---
pin_joint_a.node_b = ship_path
# --- Configure Pin Joint B ---
pin_joint_b.node_b = ship_path
else:
print("Thruster Warning: 'Attach To Node' path is not set for ", self.name)
#if ship and ship.get_path():
#var ship_path = ship.get_path()
#var self_path = get_path()
#
## --- Configure Pin Joint A ---
#pin_joint_a.node_b = ship_path
#
## --- Configure Pin Joint B ---
#pin_joint_b.node_b = ship_path
#else:
#print("Thruster Warning: 'Attach To Node' path is not set for ", self.name)
# This thruster announces its existence to the whole scene tree.
add_to_group("ship_thrusters")
#self.body_entered.connect(_on_body_entered)
# This function calculates how much fuel is needed for a given thrust level and duration.
func calculate_fuel_consumption(thrust_force: float, delta_time: float) -> float:
@ -52,32 +48,33 @@ 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")
if enabled:
is_firing = true
await get_tree().physics_frame
#print(" - firing: %s" % is_firing)
# The controller calls this ONCE to deactivate the thruster.
func turn_off():
#print("THRUSTER: Recieved Turn Off Signal")
is_firing = false
await get_tree().physics_frame
#print(" - firing: %s" % is_firing)
# --- Godot Physics Callback ---
func _physics_process(delta: float):
super(delta)
if not enabled:
is_firing = false
# If the thruster is active, apply a constant central force in its local "up" direction.
if is_firing:
apply_thrust_force()
#apply_central_force(Vector2.UP * -max_thrust)
# Also, ensure the visual effect is running
queue_redraw()

View File

@ -1,6 +1,6 @@
[gd_scene load_steps=3 format=3 uid="uid://c0bb77rmyatr0"]
[ext_resource type="Script" uid="uid://cpc34i7o1puq2" path="res://scenes/ship/components/thruster.gd" id="1_fnb47"]
[ext_resource type="Script" uid="uid://cpc34i7o1puq2" path="res://scenes/ship/components/hardware/thruster.gd" id="1_fnb47"]
[sub_resource type="CapsuleShape2D" id="CapsuleShape2D_tb2hn"]
@ -21,3 +21,16 @@ node_a = NodePath("..")
visible = false
position = Vector2(-20, 0)
node_a = NodePath("..")
[node name="ColorRect" type="ColorRect" parent="."]
anchors_preset = 8
anchor_left = 0.5
anchor_top = 0.5
anchor_right = 0.5
anchor_bottom = 0.5
offset_left = -20.0
offset_top = -40.0
offset_right = 20.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.376471, 0.376471, 0.376471, 1)

View File

@ -0,0 +1,100 @@
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
## Returns the SystemStation component that this panel is installed in.
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)

View File

@ -0,0 +1 @@
uid://bvk2kd1dphe7g

View File

@ -0,0 +1,36 @@
extends BasePanel
## This UI provides three output signals, one for each button.
class_name ButtonPanel
signal button_1_pressed
signal button_2_pressed
signal button_3_pressed
signal button_4_pressed
signal button_5_pressed
signal button_6_pressed
@onready var RCSPosBtn: Button = $RCSPos
@onready var RCSNegBtn: Button = $RCSNeg
@onready var ShutdownBtn: Button = $RCSShutdown
@onready var CalibrateRcsBtn: Button = $CalibrateRCS
@onready var CalculationBtn: Button = $CalculateHohman
@onready var AutopilotButton: Button = $EngageAutopilot
func _ready():
RCSPosBtn.pressed.connect(button_1_pressed.emit)
RCSNegBtn.pressed.connect(button_2_pressed.emit)
ShutdownBtn.pressed.connect(button_3_pressed.emit)
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",
]

View File

@ -0,0 +1 @@
uid://ojt1koy5h64m

View File

@ -0,0 +1,36 @@
[gd_scene load_steps=2 format=3 uid="uid://dt1t2n7dewucw"]
[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 = 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
text = "RCS Pos"
[node name="RCSNeg" type="Button" parent="."]
layout_mode = 2
text = "RCS Neg"
[node name="RCSShutdown" type="Button" parent="."]
layout_mode = 2
text = "RCS Shutdown"
[node name="CalibrateRCS" type="Button" parent="."]
layout_mode = 2
text = "Calibrate RCS"
[node name="CalculateHohman" type="Button" parent="."]
layout_mode = 2
text = "Calculate Hohman
"
[node name="EngageAutopilot" type="Button" parent="."]
layout_mode = 2
toggle_mode = true
text = "Engage Autopilot"

View File

@ -0,0 +1,15 @@
extends BasePanel
## This UI provides one "socket": a function to update its text.
class_name ReadoutScreenPanel
@onready var display = $RichTextLabel
# This is the "input socket" for this panel.
# A datashard can call this function to show information to the player.
func update_display(text: String):
if display:
display.text = text
func get_input_sockets():
return ["update_display"]

View File

@ -0,0 +1 @@
uid://laeom8fvlfkf

View File

@ -0,0 +1,13 @@
[gd_scene load_steps=2 format=3 uid="uid://cdbqjkgsj02or"]
[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 = 1.0
script = ExtResource("1_w2pab")
grid_width = 2
[node name="RichTextLabel" type="RichTextLabel" parent="."]
layout_mode = 2
bbcode_enabled = true
fit_content = true

View File

@ -1,22 +1,23 @@
# space_simulation/scripts/map_controller.gd
class_name SensorPanel
extends BasePanel
class_name MapController
extends Control
signal body_selected_for_planning(body: RigidBody2D)
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
var map_scale: float = 0.001
var map_offset: Vector2 = Vector2.ZERO
var focal_body: RigidBody2D
var focal_body: OrbitalBody2D
var icon_map: Dictionary = {}
var followed_body: RigidBody2D = null
var followed_body: OrbitalBody2D = null
var map_tween: Tween
# The starting point for our lerp animation.
@ -28,67 +29,92 @@ var follow_progress: float = 0.0:
# 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 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.
# We'll check which bodies are new and which have been removed.
var bodies_in_feed = all_bodies.duplicate()
focal_body = bodies_in_feed[0] if bodies_in_feed.size() else get_owning_station().get_root_module()
# Remove icons for bodies that are no longer in the feed
for body in icon_map.keys():
if not body in bodies_in_feed:
if is_instance_valid(icon_map[body]):
icon_map[body].queue_free()
icon_map.erase(body)
# Add icons for new bodies
for body in bodies_in_feed:
if not body in icon_map:
var icon = map_icon_scene.instantiate() as MapIcon
map_canvas.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 _process(_delta: float) -> void:
_update_icon_positions()
func _draw() -> void:
var map_center = get_rect().size / 2.0
# TODO: Turn this into drawing of trajectories for bodies that are selected
# TODO: The calculation of the projections should be moved into a databank
# as this panel should only ever display control nodes and possibly projection paths that are fed to it
var star_system = GameManager.current_star_system
var star_orbiters: Array[OrbitalBody2D] = []
star_orbiters.append(star_system.get_star())
star_orbiters.append_array(star_system.get_planetary_systems())
star_orbiters.append_array(star_system.get_orbital_bodies())
draw_projected_orbits(star_orbiters)
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)
if body is Planet:
var planet_system = body.get_parent() as Barycenter
draw_projected_orbits(planet_system.get_internal_attractors())
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 draw_projected_orbits(bodies_to_project: Array[OrbitalBody2D]):
var map_center = get_rect().size / 2.0
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)
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
@ -147,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
@ -166,10 +192,10 @@ func _gui_input(event: InputEvent) -> void:
map_offset += event.relative
func _on_map_icon_selected(body: RigidBody2D):
emit_signal("body_selected_for_planning", body)
func _on_map_icon_selected(body: OrbitalBody2D):
body_selected_for_planning.emit(body)
func _on_follow_requested(body: RigidBody2D):
func _on_follow_requested(body: OrbitalBody2D):
print("Map view locking on to: ", body.name)
follow_progress = 0.0
followed_body = body

View File

@ -0,0 +1 @@
uid://bat1cxo15fnl7

View File

@ -0,0 +1,20 @@
[gd_scene load_steps=3 format=3 uid="uid://rd1c22nsru8y"]
[ext_resource type="Script" uid="uid://bat1cxo15fnl7" path="res://scenes/ship/computer/UI/sensor_panel.gd" id="1_5yxry"]
[ext_resource type="PackedScene" uid="uid://c2imrmgjthfdm" path="res://scenes/UI/MapIcon.tscn" id="2_kvnmq"]
[node name="SensorPanel" type="Control"]
clip_contents = true
layout_mode = 3
anchors_preset = 0
mouse_filter = 1
script = ExtResource("1_5yxry")
map_icon_scene = ExtResource("2_kvnmq")
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

View File

@ -0,0 +1,23 @@
extends BasePanel
## This UI provides one output signal: the current value of the lever.
class_name ThrottleLeverPanel
signal lever_value_changed(value: float)
@onready var lever_slider: VSlider = $LeverSlider
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")
#if not is_equal_approx(input_value, lever_slider.value):
#lever_slider.value = input_value

View File

@ -0,0 +1 @@
uid://dho2ww3hmmsra

View File

@ -0,0 +1,21 @@
[gd_scene load_steps=2 format=3 uid="uid://pq55j75t3fda"]
[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 = 63.0
offset_bottom = 35.0
script = ExtResource("1_q6svd")
grid_height = 4
[node name="Label" type="Label" parent="."]
layout_mode = 2
text = "Throttle"
horizontal_alignment = 1
[node name="LeverSlider" type="VSlider" parent="."]
layout_mode = 2
size_flags_horizontal = 1
size_flags_vertical = 3
max_value = 1.0
step = 0.01

View File

@ -0,0 +1,14 @@
@tool
class_name ControlPanel
extends Resource
## The UI scene for this panel (e.g., a lever, a screen).
@export var ui_scene: PackedScene
## Describes the signals this panel emits (e.g., "lever_pulled").
func get_output_signals() -> Dictionary:
return {}
## Describes the functions this panel has to display data (e.g., "update_text").
func get_input_sockets() -> Dictionary:
return {}

View File

@ -0,0 +1 @@
uid://cskf26i7vnxug

View 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

View File

@ -0,0 +1 @@
uid://jew2ur3plyy6

View File

@ -0,0 +1,16 @@
class_name Databank
extends Node
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() -> Array[String]:
return []
## Describes the signals this shard can output.
func get_output_sockets() -> Array[String]:
return []

View File

@ -0,0 +1 @@
uid://osk1l75vlikn

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="StationPanel" load_steps=3 format=3 uid="uid://c4wyouanvf86c"]
[ext_resource type="Script" uid="uid://cskf26i7vnxug" path="res://scenes/ship/computer/control_panel.gd" id="1_ts7kh"]
[ext_resource type="PackedScene" uid="uid://dt1t2n7dewucw" path="res://scenes/ship/computer/UI/button_panel.tscn" id="2_72ur5"]
[resource]
script = ExtResource("1_ts7kh")
ui_scene = ExtResource("2_72ur5")
metadata/_custom_type_script = "uid://cskf26i7vnxug"

View 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

View File

@ -0,0 +1 @@
uid://cadvugf4oqgvk

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ControlPanel" load_steps=3 format=3 uid="uid://57y6igb07e10"]
[ext_resource type="Script" uid="uid://cskf26i7vnxug" path="res://scenes/ship/computer/control_panel.gd" id="1_540xq"]
[ext_resource type="PackedScene" uid="uid://cdbqjkgsj02or" path="res://scenes/ship/computer/UI/readout_screen_panel.tscn" id="2_iy0t0"]
[resource]
script = ExtResource("1_540xq")
ui_scene = ExtResource("2_iy0t0")
metadata/_custom_type_script = "uid://cskf26i7vnxug"

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="ControlPanel" load_steps=3 format=3 uid="uid://dl7g67mtqkfx2"]
[ext_resource type="Script" uid="uid://cskf26i7vnxug" path="res://scenes/ship/computer/control_panel.gd" id="1_f0h3m"]
[ext_resource type="PackedScene" uid="uid://rd1c22nsru8y" path="res://scenes/ship/computer/UI/sensor_panel.tscn" id="2_kyhrs"]
[resource]
script = ExtResource("1_f0h3m")
ui_scene = ExtResource("2_kyhrs")
metadata/_custom_type_script = "uid://cskf26i7vnxug"

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="StationPanel" load_steps=3 format=3 uid="uid://dcyr6utrk376h"]
[ext_resource type="Script" uid="uid://cskf26i7vnxug" path="res://scenes/ship/computer/control_panel.gd" id="1_8h7ox"]
[ext_resource type="PackedScene" uid="uid://pq55j75t3fda" path="res://scenes/ship/computer/UI/throttle_lever_panel.tscn" id="2_8h7ox"]
[resource]
script = ExtResource("1_8h7ox")
ui_scene = ExtResource("2_8h7ox")
metadata/_custom_type_script = "uid://cskf26i7vnxug"

View File

@ -0,0 +1,170 @@
# scenes/ship/computer/shards/autopilot_databank.gd
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)
## 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[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):
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]
fmt_out.emit(fmt)
# INPUT SOCKET: Connected to the ManeuverPlanner's "maneuver_calculated" signal.
func maneuver_received(plan: Array[DataTypes.ImpulsiveBurnPlan]):
current_plan = plan
print("AUTOPILOT: Maneuver plan received.")
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.
func execute_plan():
if not current_plan.is_empty():
current_state = State.WAITING_FOR_WINDOW
print("AUTOPILOT: Executing plan. Waiting for first burn window.")
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
status = "Executing Main Engine Burn: %s"
print("AUTOPILOT: Commanding main engine burn for %.2f seconds." % step.burn_duration)
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
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

View File

@ -0,0 +1 @@
uid://ceqdi6jobefnc

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://dwhpjwuobcqdu"]
[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]
script = ExtResource("1_tpm1x")
logic_script = ExtResource("1_0abvf")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View File

@ -0,0 +1,252 @@
extends Databank
class_name HelmLogicShard
# --- References ---
@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
@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
if not is_instance_valid(root_module):
push_error("Helm Shard initialized without a valid ship root!")
return
thrusters = _find_all_thrusters(root_module)
# You can add logic here to listen for parts being added/removed to re-scan.
# Default to holding the initial attitude.
target_rotation_rad = root_module.rotation
func _physics_process(_delta):
if not is_instance_valid(root_module): return
# If attitude hold is on, run the PD controller.
if attitude_hold_enabled:
_perform_manual_hold()
# --- INPUT SOCKETS (Called by Panels or other Shards) ---
## This is an "input socket" for rotational control.
## It takes a value from -1.0 to 1.0.
func set_rotation_input(value: float):
if abs(value) > 0.1:
# Manual input overrides attitude hold.
attitude_hold_enabled = false
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.
if not attitude_hold_enabled:
attitude_hold_enabled = true
target_rotation_rad = root_module.rotation
## This is an "input socket" for translational control (main thrusters).
## 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.
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)
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):
if not is_instance_valid(root_module):
return
# Iterate through all available RCS thrusters that have been calibrated
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.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:
if not thruster.main_thruster:
thruster.turn_off()
func _find_all_thrusters(node: Node) -> Array[Thruster]:
var thrusters: Array[Thruster] = []
for child in node.get_children():
if child is Thruster:
thrusters.append(child)
if child.get_child_count() > 0:
thrusters.append_array(_find_all_thrusters(child))
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:
return difference - TAU
else:
return difference
signal thruster_calibrated(data: DataTypes.ThrusterCalibration)
var calibration_data: DataTypes.ThrusterCalibration
# --- CALIBRATION LOGIC (Migrated from ThrusterController.gd) ---
## Manages the calibration sequence for all non-main thrusters.
func calibrate_rcs_performance():
print("Helm Shard: Beginning RCS calibration protocol...")
if not is_instance_valid(root_module): return
# --- 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
await get_tree().physics_frame
print("Helm Shard: Attitude hold protocol: %s" % ("enabled" if attitude_hold_enabled else "disabled"))
calibration_data = DataTypes.ThrusterCalibration.new()
for thruster in thrusters:
var data: DataTypes.ThrusterData = await _calibrate_single_thruster(thruster)
calibration_data.thruster_data[thruster] = data
print(calibration_data)
# Now that we have the data, calculate the ship's max torque 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:
calibration_data.max_pos_torque += data.measured_torque
else:
calibration_data.max_neg_torque += abs(data.measured_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 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])
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) -> 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
# --- 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 to ensure the velocity update is complete
await get_tree().physics_frame
# --- 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)
if root_module.inertia > 0:
data.measured_torque = root_module.inertia * (delta_angular_velocity / test_burn_duration)
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
var counter_burn_duration = (root_module.inertia * root_module.angular_velocity) / counter_torque
# Find a thruster that can apply the counter-torque
for other_thruster in thrusters:
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
other_thruster.turn_off()
break # Use the first one we find
await get_tree().physics_frame
return data

View File

@ -0,0 +1 @@
uid://cfbyqvnvf3hna

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://dghg3pbws42yu"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_kih5s"]
[ext_resource type="Script" uid="uid://cfbyqvnvf3hna" path="res://scenes/ship/computer/shards/helm_logic_databank.gd" id="1_vvsub"]
[resource]
script = ExtResource("1_kih5s")
logic_script = ExtResource("1_vvsub")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View File

@ -0,0 +1,37 @@
extends Databank
class_name ShipStatusShard
## This shard emits a signal with the formatted ship status text.
signal status_updated(text: String)
# 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
# 1. Gather all the data from the root module.
var rotation_deg = rad_to_deg(root_module.rotation)
var angular_vel_dps = rad_to_deg(root_module.angular_velocity)
var linear_vel_mps = root_module.linear_velocity.length()
# 2. Build the string that will be displayed.
var status_text = """
[font_size=24]Ship Status[/font_size]
[font_size=18]Rotation: %.1f deg[/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]
# 3. Emit the signal with the formatted text.
status_updated.emit(status_text)

View File

@ -0,0 +1 @@
uid://ctgl5kxyagw0f

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://bx7wgunvy5hfa"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_2fbxe"]
[ext_resource type="Script" uid="uid://ctgl5kxyagw0f" path="res://scenes/ship/computer/shards/helm_ship_status.gd" id="1_880kd"]
[resource]
script = ExtResource("1_2fbxe")
logic_script = ExtResource("1_880kd")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View 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)

View File

@ -0,0 +1 @@
uid://ghluwjd5c5ul

View File

@ -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"

View File

@ -0,0 +1,150 @@
# scenes/ship/computer/shards/maneuver_planner_databank.gd
extends Databank
class_name HohmanPlannerShard
## Emitted when a maneuver plan has been successfully calculated.
signal maneuver_calculated(plan: Array[DataTypes.ImpulsiveBurnPlan])
# --- References ---
var selection_shard: NavSelectionShard
var target_body: OrbitalBody2D = null
# --- Configurations ---
var boost_factor: float = 1.0
## 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("MANEUVER PLANNER: Target recieved %s." % new_target)
target_body = new_target
# 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):
print("MANEUVER PLANNER: Cannot calculate without ship and target.")
return
var star = GameManager.current_star_system.get_star()
if not is_instance_valid(star): return
var mu = OrbitalMechanics.G * star.mass
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 * boost_factor
var v_source_orbit = sqrt(mu / r1)
var v_target_orbit = sqrt(mu / r2)
var v_transfer_periapsis = sqrt(mu * ((2.0 / r1) - (1.0 / a_transfer)))
var v_transfer_apoapsis = sqrt(mu * ((2.0 / r2) - (1.0 / a_transfer)))
var delta_v1 = v_transfer_periapsis - v_source_orbit
var delta_v2 = v_target_orbit - v_transfer_apoapsis
var time_of_flight = PI * sqrt(pow(a_transfer, 3) / mu)
var ang_vel_target = sqrt(mu / pow(r2, 3))
var travel_angle = ang_vel_target * time_of_flight
var required_phase_angle = PI - travel_angle
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)
var ang_vel_ship = sqrt(mu / pow(r1, 3))
var relative_ang_vel = ang_vel_ship - ang_vel_target
var angle_to_wait = current_phase_angle - required_phase_angle
if relative_ang_vel == 0: return # Avoid division by zero
var wait_time = abs(angle_to_wait / relative_ang_vel)
# 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 / root_module.mass
# --- 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
# --- 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 = 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.")
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
}

View File

@ -0,0 +1 @@
uid://bghu5lhcbcfmh

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://6jj1jd14cdlt"]
[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]
script = ExtResource("1_nleqa")
logic_script = ExtResource("1_attn3")
metadata/_custom_type_script = "uid://osk1l75vlikn"

View 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

View File

@ -0,0 +1 @@
uid://dsbn7ushwqrko

View 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)

View File

@ -0,0 +1 @@
uid://0f6v6iu3o5qo

View 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"

View File

@ -0,0 +1,23 @@
# scenes/ship/computer/shards/nav_selection_databank.gd
extends Databank
class_name NavSelectionShard
## Emitted whenever a new navigation target is selected from the map.
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 body_selected(body: OrbitalBody2D):
if is_instance_valid(body) and body != selected_body:
print("NAV SELECTION: New target acquired - ", body.name)
selected_body = body
emit_signal("target_selected", body)

View File

@ -0,0 +1 @@
uid://t12etsdx2h38

View File

@ -0,0 +1,9 @@
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://g4ho63f30vjm"]
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_d0eru"]
[ext_resource type="Script" uid="uid://t12etsdx2h38" path="res://scenes/ship/computer/shards/nav_selection_databank.gd" id="1_mt7ap"]
[resource]
script = ExtResource("1_d0eru")
logic_script = ExtResource("1_mt7ap")
metadata/_custom_type_script = "uid://osk1l75vlikn"

Some files were not shown because too many files have changed in this diff Show More