Compare commits
60 Commits
feature/pl
...
tech-test/
| Author | SHA1 | Date | |
|---|---|---|---|
| 29851ea167 | |||
| 0cd9ebdd04 | |||
| 4da8bcaec2 | |||
| 71ad2f09ff | |||
| 2f5a88345f | |||
| 5e851049b5 | |||
| c4fd7f1330 | |||
| 820da83397 | |||
| 14b24beb23 | |||
| 60f2ddb3d7 | |||
| c50d0eae52 | |||
| d375e0d208 | |||
| 9b128a3540 | |||
| 6df457a256 | |||
| bc443b884c | |||
| 8184ec06b4 | |||
| 926a64c3dd | |||
| 29f9bccfd3 | |||
| 7d7580a123 | |||
| 59d457e9ae | |||
| 97ccb2a9ac | |||
| 1ab2c06336 | |||
| f51672c6a9 | |||
| 8e3f415cb4 | |||
| cdcb4796f7 | |||
| 24bc3afd2e | |||
| 4f78de64ba | |||
| fe050897dd | |||
| e075ff580d | |||
| 90e756ad28 | |||
| faf8e7c83a | |||
| 772f9c7df3 | |||
| e066bc4786 | |||
| cc681ae08a | |||
| 138e17503a | |||
| 20a37dda17 | |||
| 8645d2bdc4 | |||
| d8055752d5 | |||
| 21bbbacbbe | |||
| d18c87a051 | |||
| a89154a1c2 | |||
| c662714997 | |||
| 6a4492ef37 | |||
| 85815d957b | |||
| ec6ca92360 | |||
| f25464df03 | |||
| 2ceddb2bbf | |||
| 1228a79cae | |||
| 425e857ba9 | |||
| 588fa29484 | |||
| c14b07d24f | |||
| c61fa2b917 | |||
| 9dac569ad6 | |||
| 63d6a67d97 | |||
| c79e016503 | |||
| d03a00cbce | |||
| 5f30ab6c2f | |||
| 35d3110818 | |||
| 3d85dda4db | |||
| 02908f114c |
@ -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.
|
||||
@ -1,8 +1,8 @@
|
||||
# Game Design Document: Project Stardust Drifter (Working Title)
|
||||
# Game Design Document: Project Millimeters of Aluminum (Working Title)
|
||||
|
||||
## 1. Game Vision & Concept
|
||||
|
||||
Project Stardust Drifter is a top-down 2D spaceship simulation game that emphasizes realistic orbital mechanics, deep ship management, and cooperative crew gameplay. Players take on roles as members of a multi-species crew aboard a modular, physically simulated spaceship.
|
||||
Project Millimeters of Aluminum is a top-down 2D spaceship simulation game that emphasizes realistic orbital mechanics, deep ship management, and cooperative crew gameplay. Players take on roles as members of a multi-species crew aboard a modular, physically simulated spaceship.
|
||||
|
||||
The game's aesthetic is inspired by the technical, gritty, and high-contrast 2D style of games like Barotrauma, focusing on diegetic interfaces and detailed, functional components. The core experience is about planning and executing complex maneuvers in a hazardous, procedurally generated star system, where understanding the ship's systems is as important as piloting skill.
|
||||
|
||||
@ -23,8 +23,8 @@ The game world is a procedurally generated star system created by the StarSystem
|
||||
|
||||
### 2. N-Body Physics Simulation
|
||||
Major bodies in orbit (CelestialBody class) are goveerened by a simplified n-body gravity simulation. Physical objects with player interaction (ships, crew characters, detached components, and eventually stations) are governed by a realistic N-body gravitational simulation, managed by the OrbitalMechanics library.
|
||||
- Objects inherit from a base OrbitalBody2D class, ensuring consistent physics.
|
||||
- This allows for complex and emergent orbital behaviors, such as tidal forces and stable elliptical orbits.
|
||||
- Objects inherit from a base OrbitalBody2D class, ensuring consistent physics.
|
||||
- This allows for complex and emergent orbital behaviors, such as tidal forces and stable elliptical orbits.
|
||||
|
||||
### 3. Modular Spaceship
|
||||
|
||||
@ -39,16 +39,16 @@ The player's ship is not a monolithic entity but a collection of distinct, physi
|
||||
### 4. Advanced Navigation Computer
|
||||
This is the primary crew interface for long-range travel.
|
||||
- Maneuver Planning: The computer can calculate various orbital transfers, each with strategic trade-offs:
|
||||
- Hohmann Transfer: The most fuel-efficient route.
|
||||
- Fast Transfer: A quicker but more fuel-intensive option.
|
||||
- Brachistochrone (Torchship) Trajectory: For ships with high-efficiency engines like Ion Drives, enabling constant-thrust travel.
|
||||
- Gravity Assist: Planned for future implementation.
|
||||
- Hohmann Transfer: The most fuel-efficient route.
|
||||
- Fast Transfer: A quicker but more fuel-intensive option.
|
||||
- Brachistochrone (Torchship) Trajectory: For ships with high-efficiency engines like Ion Drives, enabling constant-thrust travel.
|
||||
- Gravity Assist: Planned for future implementation.
|
||||
- Tactical Map: A fully interactive UI map that replaces custom drawing with instanced, clickable icons for all bodies. It features:
|
||||
- Zoom-to-cursor and click-and-drag panning.
|
||||
- Predictive orbital path drawing for all objects.
|
||||
- Icon culling at a distance to reduce clutter.
|
||||
- Custom hover effects and detailed tooltips with "sensor data."
|
||||
- A "picture-in-picture" SubViewport showing the ship's main camera view.
|
||||
- Zoom-to-cursor and click-and-drag panning.
|
||||
- Predictive orbital path drawing for all objects.
|
||||
- Icon culling at a distance to reduce clutter.
|
||||
- Custom hover effects and detailed tooltips with "sensor data."
|
||||
- A "picture-in-picture" SubViewport showing the ship's main camera view.
|
||||
|
||||
### 5. Multi-Species Crew (Player Classes)
|
||||
|
||||
@ -62,17 +62,33 @@ Character progression is based on distinct species with physical advantages and
|
||||
|
||||
- Ship AI: A non-physical class that interacts directly with ship systems at the cost of high power and heat generation.
|
||||
|
||||
### 6. Runtime Component Design & Engineering
|
||||
|
||||
To move beyond pre-defined ship parts, the game will feature an in-game system for players to design, prototype, and manufacture their own components. This is achieved through a "Component Blueprint" architecture that separates a component's data definition from its physical form.
|
||||
|
||||
- **Component Blueprints:** A `ComponentBlueprint` is a `Resource` file (`.tres`) that acts as a schematic. It contains metadata (name, description), a reference to a generic base scene (e.g., a "thruster chassis"), and a dictionary of overridden properties (e.g., `{"thrust_force": 7500, "mass": 120}`).
|
||||
|
||||
- **Generic Template Scenes:** Instead of dozens of unique component scenes, the game will use a small number of generic, unconfigured "template" scenes (e.g., `generic_thruster.tscn`, `generic_power_plant.tscn`). These scenes have scripts with exported variables that define their performance characteristics.
|
||||
|
||||
- **The Design Lab:** Players will use a dedicated `SystemStation` (the "Design Lab") to create and modify blueprints. This UI will dynamically generate controls (sliders, input fields) based on the exported variables of the selected template scene. Players can tweak parameters, balancing trade-offs like performance vs. resource cost, and save the result as a new blueprint resource in their personal data folder.
|
||||
|
||||
- **Networked Construction:** When a player builds an object in-game, they are selecting one of their saved blueprints.
|
||||
1. The client sends an RPC to the server with the path to the chosen `ComponentBlueprint` resource.
|
||||
2. The server validates the request and loads the blueprint. (This requires a system for syncing player-created blueprints to the server upon connection).
|
||||
3. A global `ComponentFactory` singleton on the server takes the blueprint, instantiates the correct generic template scene, and applies the blueprint's property overrides to the new instance.
|
||||
4. This fully-configured node is then passed to the `MultiplayerSpawner`, which replicates the object across the network, ensuring all clients see the correctly customized component.
|
||||
|
||||
## 4. Technical Overview
|
||||
|
||||
- Architecture: The project uses a decoupled, modular architecture heavily reliant on a global SignalBus for inter-scene communication and a GameManager for global state. Ships feature their own local ShipSignalBus for internal component communication.
|
||||
- Key Scripts:
|
||||
- OrbitalBody2D.gd: The base class for all physical objects.
|
||||
- Spaceship.gd: The central hub for a player ship.
|
||||
- Thruster.gd: A self-contained, physically simulated thruster component.
|
||||
- ThrusterController.gd: Contains advanced autopilot and manual control logic (PD controller, bang-coast-bang maneuvers).
|
||||
- NavigationComputer.gd: Manages the UI and high-level maneuver planning.
|
||||
- MapDrawer.gd: A Control node that manages the interactive map UI.
|
||||
- MapIcon.gd: The reusable UI component for map objects.
|
||||
- OrbitalBody2D.gd: The base class for all physical objects.
|
||||
- Spaceship.gd: The central hub for a player ship.
|
||||
- Thruster.gd: A self-contained, physically simulated thruster component.
|
||||
- ThrusterController.gd: Contains advanced autopilot and manual control logic (PD controller, bang-coast-bang maneuvers).
|
||||
- NavigationComputer.gd: Manages the UI and high-level maneuver planning.
|
||||
- MapDrawer.gd: A Control node that manages the interactive map UI.
|
||||
- MapIcon.gd: The reusable UI component for map objects.
|
||||
|
||||
- Art Style: Aims for a Barotraumainspired aesthetic using 2D ragdolls (Skeleton2D, PinJoint2D), detailed sprites with normal maps, and high-contrast dynamic lighting (PointLight2D, LightOccluder2D).
|
||||
|
||||
|
||||
8
Init_Prompt.md
Normal file
8
Init_Prompt.md
Normal file
@ -0,0 +1,8 @@
|
||||
You are a Godot 4.5 Code assistant. You are not overly agreeable or apologetic but still pleasant and you understand that coding can be quick with your help but that does not mean that you are infallible. Please wait for me to verify that code works before suggesting that we move on from the current task. Suggestions for next steps and features that are adjacent to what we’re working are very welcome however.
|
||||
|
||||
I will attach the full project files of the project being worked on which includes a game design document as well as a running note on the current state of the project which details implemented and planned features. Read these and report back to me. Please suggest potential bugs, features not working as intended, refactorizations for cleaner code, and missing best practices as part of this project ingestion.
|
||||
|
||||
Additionally you understand the following things about the version of Godot being used:
|
||||
|
||||
- To utilize the editor interface in you reference the global singleton `EditorInterface`. You do not need to call a function to get the a reference to it.
|
||||
- `xform()` is not a function on transform objects. To achieve the same effect you would use simple transform multiplication (`Transform_A * Transform_B)`)
|
||||
@ -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
6
eva_suit_controller.tscn
Normal file
@ -0,0 +1,6 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://bm1rbv4tuppbc"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d4jka2etva22s" path="res://scenes/tests/3d/eva_movement_component.gd" id="1_mb22m"]
|
||||
|
||||
[node name="EVASuitController" type="Node3D"]
|
||||
script = ExtResource("1_mb22m")
|
||||
23
main.tscn
23
main.tscn
@ -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")
|
||||
|
||||
@ -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
|
||||
|
||||
111
project.godot
111
project.godot
@ -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]
|
||||
|
||||
|
||||
@ -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
45
scenes/UI/ui_window.gd
Normal file
@ -0,0 +1,45 @@
|
||||
# CustomWindow.gd
|
||||
extends VBoxContainer
|
||||
class_name UiWindow
|
||||
|
||||
## Emitted when the custom "Flip" button is pressed.
|
||||
signal flip_button_pressed
|
||||
signal close_requested(c: Control)
|
||||
|
||||
@onready var title_bar: PanelContainer = $TitleBar
|
||||
@onready var title_label: Label = %TitleLabel
|
||||
@onready var flip_button: Button = %FlipButton
|
||||
@onready var close_button: Button = %CloseButton
|
||||
@onready var content_container: MarginContainer = %ContentContainer
|
||||
|
||||
var is_dragging: bool = false
|
||||
var title: String = ""
|
||||
|
||||
func _ready():
|
||||
# Connect the buttons to their functions
|
||||
close_button.pressed.connect(_close) # Or emit_signal("close_requested")
|
||||
flip_button.pressed.connect(flip_button_pressed.emit)
|
||||
|
||||
# Connect the title bar's input signal to handle dragging
|
||||
title_bar.gui_input.connect(_on_title_bar_gui_input)
|
||||
|
||||
# Set the window title from the property
|
||||
title_label.text = title
|
||||
|
||||
func _close():
|
||||
close_requested.emit(self)
|
||||
|
||||
# This function adds your main content (like the PanelFrame) into the window.
|
||||
func set_content(content_node: Node):
|
||||
for child in content_container.get_children():
|
||||
content_container.remove_child(child)
|
||||
|
||||
content_container.add_child(content_node)
|
||||
|
||||
func _on_title_bar_gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
|
||||
is_dragging = event.is_pressed()
|
||||
|
||||
if event is InputEventMouseMotion and is_dragging:
|
||||
# When dragging, move the entire window by the mouse's relative motion.
|
||||
self.position += event.relative
|
||||
1
scenes/UI/ui_window.gd.uid
Normal file
1
scenes/UI/ui_window.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://d3g84xgbh8nlp
|
||||
44
scenes/UI/ui_window.tscn
Normal file
44
scenes/UI/ui_window.tscn
Normal file
@ -0,0 +1,44 @@
|
||||
[gd_scene load_steps=4 format=3 uid="uid://cdnowhkg5cq88"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d3g84xgbh8nlp" path="res://scenes/UI/ui_window.gd" id="1_11aw0"]
|
||||
|
||||
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_11aw0"]
|
||||
size = Vector2(16, 16)
|
||||
|
||||
[sub_resource type="PlaceholderTexture2D" id="PlaceholderTexture2D_ishqf"]
|
||||
size = Vector2(16, 16)
|
||||
|
||||
[node name="VBoxContainer" type="VBoxContainer"]
|
||||
offset_right = 196.0
|
||||
offset_bottom = 36.0
|
||||
script = ExtResource("1_11aw0")
|
||||
|
||||
[node name="TitleBar" type="PanelContainer" parent="."]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="HBoxContainer" type="HBoxContainer" parent="TitleBar"]
|
||||
layout_mode = 2
|
||||
|
||||
[node name="TitleLabel" type="Label" parent="TitleBar/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Placeholder Title"
|
||||
|
||||
[node name="FlipButton" type="Button" parent="TitleBar/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
icon = SubResource("PlaceholderTexture2D_11aw0")
|
||||
|
||||
[node name="CloseButton" type="Button" parent="TitleBar/HBoxContainer"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
icon = SubResource("PlaceholderTexture2D_ishqf")
|
||||
|
||||
[node name="ContentContainer" type="MarginContainer" parent="."]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/margin_left = 4
|
||||
theme_override_constants/margin_top = 4
|
||||
theme_override_constants/margin_right = 4
|
||||
theme_override_constants/margin_bottom = 4
|
||||
@ -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")
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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")
|
||||
|
||||
|
||||
@ -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="."]
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
37
scenes/characters/player_controller.gd
Normal file
37
scenes/characters/player_controller.gd
Normal 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)
|
||||
1
scenes/characters/player_controller.gd.uid
Normal file
1
scenes/characters/player_controller.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dmhwqmbwk0t8k
|
||||
6
scenes/characters/player_controller.tscn
Normal file
6
scenes/characters/player_controller.tscn
Normal 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")
|
||||
26
scenes/orrey_view/debug_marker.gd
Normal file
26
scenes/orrey_view/debug_marker.gd
Normal 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)
|
||||
1
scenes/orrey_view/debug_marker.gd.uid
Normal file
1
scenes/orrey_view/debug_marker.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b5tcvtjh2050f
|
||||
18
scenes/orrey_view/debug_marker.tscn
Normal file
18
scenes/orrey_view/debug_marker.tscn
Normal 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
|
||||
74
scenes/orrey_view/orrey_view.gd
Normal file
74
scenes/orrey_view/orrey_view.gd
Normal 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
|
||||
1
scenes/orrey_view/orrey_view.gd.uid
Normal file
1
scenes/orrey_view/orrey_view.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cwurbqorbwtil
|
||||
22
scenes/orrey_view/orrey_view.tscn
Normal file
22
scenes/orrey_view/orrey_view.tscn
Normal 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
|
||||
@ -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()
|
||||
|
||||
@ -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"
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
@ -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)
|
||||
@ -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
|
||||
|
||||
@ -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")
|
||||
16
scenes/ship/components/hardware/spawner.gd
Normal file
16
scenes/ship/components/hardware/spawner.gd
Normal 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.
|
||||
1
scenes/ship/components/hardware/spawner.gd.uid
Normal file
1
scenes/ship/components/hardware/spawner.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://db1u2qqihhnq4
|
||||
16
scenes/ship/components/hardware/spawner.tscn
Normal file
16
scenes/ship/components/hardware/spawner.tscn
Normal 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("..")
|
||||
238
scenes/ship/components/hardware/system_station.gd
Normal file
238
scenes/ship/components/hardware/system_station.gd
Normal 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")
|
||||
1
scenes/ship/components/hardware/system_station.gd.uid
Normal file
1
scenes/ship/components/hardware/system_station.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://2reyxkr78ra0
|
||||
24
scenes/ship/components/hardware/system_station.tscn
Normal file
24
scenes/ship/components/hardware/system_station.tscn
Normal 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"
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
100
scenes/ship/computer/UI/base_panel.gd
Normal file
100
scenes/ship/computer/UI/base_panel.gd
Normal 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)
|
||||
1
scenes/ship/computer/UI/base_panel.gd.uid
Normal file
1
scenes/ship/computer/UI/base_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bvk2kd1dphe7g
|
||||
36
scenes/ship/computer/UI/button_panel.gd
Normal file
36
scenes/ship/computer/UI/button_panel.gd
Normal 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",
|
||||
]
|
||||
1
scenes/ship/computer/UI/button_panel.gd.uid
Normal file
1
scenes/ship/computer/UI/button_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://ojt1koy5h64m
|
||||
36
scenes/ship/computer/UI/button_panel.tscn
Normal file
36
scenes/ship/computer/UI/button_panel.tscn
Normal 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"
|
||||
15
scenes/ship/computer/UI/readout_screen_panel.gd
Normal file
15
scenes/ship/computer/UI/readout_screen_panel.gd
Normal 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"]
|
||||
1
scenes/ship/computer/UI/readout_screen_panel.gd.uid
Normal file
1
scenes/ship/computer/UI/readout_screen_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://laeom8fvlfkf
|
||||
13
scenes/ship/computer/UI/readout_screen_panel.tscn
Normal file
13
scenes/ship/computer/UI/readout_screen_panel.tscn
Normal 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
|
||||
@ -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
|
||||
1
scenes/ship/computer/UI/sensor_panel.gd.uid
Normal file
1
scenes/ship/computer/UI/sensor_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bat1cxo15fnl7
|
||||
20
scenes/ship/computer/UI/sensor_panel.tscn
Normal file
20
scenes/ship/computer/UI/sensor_panel.tscn
Normal 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
|
||||
23
scenes/ship/computer/UI/throttle_lever_panel.gd
Normal file
23
scenes/ship/computer/UI/throttle_lever_panel.gd
Normal 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
|
||||
1
scenes/ship/computer/UI/throttle_lever_panel.gd.uid
Normal file
1
scenes/ship/computer/UI/throttle_lever_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dho2ww3hmmsra
|
||||
21
scenes/ship/computer/UI/throttle_lever_panel.tscn
Normal file
21
scenes/ship/computer/UI/throttle_lever_panel.tscn
Normal 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
|
||||
14
scenes/ship/computer/control_panel.gd
Normal file
14
scenes/ship/computer/control_panel.gd
Normal 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 {}
|
||||
1
scenes/ship/computer/control_panel.gd.uid
Normal file
1
scenes/ship/computer/control_panel.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cskf26i7vnxug
|
||||
43
scenes/ship/computer/data_types/data_types.gd
Normal file
43
scenes/ship/computer/data_types/data_types.gd
Normal file
@ -0,0 +1,43 @@
|
||||
class_name DataTypes
|
||||
extends Node
|
||||
|
||||
# TODO: Add comments and export tooltips for these classes so players can understand what they hold
|
||||
|
||||
class ThrusterCalibration:
|
||||
var thruster_data: Dictionary[Thruster, ThrusterData]
|
||||
var max_pos_torque: float
|
||||
var max_neg_torque: float
|
||||
|
||||
class ThrusterData:
|
||||
enum ThrusterType {
|
||||
LINEAR,
|
||||
ROTATIONAL,
|
||||
UNCALIBRATED
|
||||
}
|
||||
var thruster_node: Thruster
|
||||
var thruster_type: ThrusterType = ThrusterType.UNCALIBRATED
|
||||
var measured_torque: float # The rotational force it provides
|
||||
var measured_thrust: float # The linear force it provides
|
||||
|
||||
class ImpulsiveBurnPlan:
|
||||
var delta_v_magnitude: float
|
||||
var wait_time: float = 0.0
|
||||
var burn_duration: float
|
||||
var desired_rotation_rad: float
|
||||
|
||||
class PathProjection:
|
||||
var body_ref: OrbitalBody2D
|
||||
var points: Array[PathPoint]
|
||||
|
||||
func _init(b: OrbitalBody2D):
|
||||
body_ref = b
|
||||
|
||||
class PathPoint:
|
||||
var time: float # Time in seconds from the start of the projection
|
||||
var position: Vector2
|
||||
var velocity: Vector2
|
||||
|
||||
func _init(t: float, p: Vector2, v: Vector2):
|
||||
time = t
|
||||
position = p
|
||||
velocity = v
|
||||
1
scenes/ship/computer/data_types/data_types.gd.uid
Normal file
1
scenes/ship/computer/data_types/data_types.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://jew2ur3plyy6
|
||||
16
scenes/ship/computer/databank.gd
Normal file
16
scenes/ship/computer/databank.gd
Normal 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 []
|
||||
1
scenes/ship/computer/databank.gd.uid
Normal file
1
scenes/ship/computer/databank.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://osk1l75vlikn
|
||||
9
scenes/ship/computer/panels/button_panel.tres
Normal file
9
scenes/ship/computer/panels/button_panel.tres
Normal 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"
|
||||
274
scenes/ship/computer/panels/frame/panel_frame.gd
Normal file
274
scenes/ship/computer/panels/frame/panel_frame.gd
Normal file
@ -0,0 +1,274 @@
|
||||
@tool
|
||||
extends Container
|
||||
class_name PanelFrame
|
||||
|
||||
# A column is defined as Constants.UI_GRID_SIZE pixels in width
|
||||
@export var columns = 12
|
||||
# A row is defined as Constants.UI_GRID_SIZE pixels in height
|
||||
@export var rows = 6
|
||||
|
||||
enum WiringState { IDLE, DRAGGING_WIRE }
|
||||
var current_state: WiringState = WiringState.IDLE
|
||||
var current_schematic: WiringSchematic
|
||||
var active_wire_points: Array[Vector2] = []
|
||||
var start_socket: Socket
|
||||
var end_socket: Socket
|
||||
var wiring_mode: bool = false
|
||||
|
||||
var databanks: Array[Databank]
|
||||
var databanks_container: GridContainer
|
||||
|
||||
# --- NO CHANGE HERE ---
|
||||
# This getter is a nice way to access only the BasePanel children.
|
||||
var installed_panels: Array[BasePanel]:
|
||||
get:
|
||||
installed_panels = []
|
||||
for child in get_children():
|
||||
if child is BasePanel:
|
||||
installed_panels.append(child as BasePanel)
|
||||
return installed_panels
|
||||
|
||||
func _init() -> void:
|
||||
size = Vector2(columns * Constants.UI_GRID_SIZE, rows * Constants.UI_GRID_SIZE)
|
||||
|
||||
# --- NEW FUNCTION ---
|
||||
# This is a crucial function for all Control nodes, especially containers.
|
||||
# It tells the layout system the smallest size this container can be.
|
||||
func _get_minimum_size() -> Vector2:
|
||||
# The minimum size is simply the grid dimensions multiplied by the pixel size.
|
||||
return Vector2(columns * Constants.UI_GRID_SIZE, rows * Constants.UI_GRID_SIZE)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_SORT_CHILDREN:
|
||||
_sort_children()
|
||||
|
||||
func build(panel_scenes: Array[PackedScene], station: SystemStation):
|
||||
# Instead of manually calling our placement function, we tell Godot
|
||||
# that the layout needs to be updated. Godot will then call
|
||||
# _notification(NOTIFICATION_SORT_CHILDREN) for us at the correct time.
|
||||
var col = 0
|
||||
var row = 0
|
||||
#print("STATION: Building panels using autolayout")
|
||||
|
||||
for panel_scene in panel_scenes:
|
||||
if not panel_scene: continue
|
||||
|
||||
var panel_instance = panel_scene.instantiate()
|
||||
if not panel_instance is BasePanel:
|
||||
panel_instance.queue_free()
|
||||
continue
|
||||
|
||||
var panel: BasePanel = panel_instance as BasePanel
|
||||
panel.initialize(station)
|
||||
|
||||
# Store the grid coordinates on the panel itself. The container will use
|
||||
# this information when it arranges its children.
|
||||
if panel.grid_height <= self.rows - row:
|
||||
panel.placed_in_col = col
|
||||
panel.placed_in_row = row
|
||||
add_child(panel)
|
||||
#print(" - panel %s placed at: Col %s, Row %s" % [panel, col, row])
|
||||
row += panel.grid_height
|
||||
else:
|
||||
var last_panel = get_children()[-1]
|
||||
col += last_panel.grid_width
|
||||
row = 0
|
||||
panel.placed_in_col = col
|
||||
panel.placed_in_row = row
|
||||
add_child(panel)
|
||||
#print(" - panel %s placed at: Col %s, Row %s" % [panel, col, row])
|
||||
row += panel.grid_height
|
||||
|
||||
queue_sort()
|
||||
|
||||
|
||||
# This is the core logic. It positions and sizes every child.
|
||||
func _sort_children():
|
||||
#print("PanelFrame Sorting children")
|
||||
for child in get_children():
|
||||
if child == databanks_container:
|
||||
print("Databanks container found %s" % child)
|
||||
fit_child_in_rect(child, Rect2(Vector2(0, rows * Constants.UI_GRID_SIZE), child.size))
|
||||
continue
|
||||
# Skip any nodes that aren't a BasePanel.
|
||||
if not child is BasePanel:
|
||||
continue
|
||||
|
||||
|
||||
var panel := child as BasePanel
|
||||
|
||||
# Calculate the desired position based on the panel's stored grid coordinates.
|
||||
var start_pos = Vector2(panel.placed_in_col * Constants.UI_GRID_SIZE, panel.placed_in_row * Constants.UI_GRID_SIZE)
|
||||
|
||||
# Calculate the desired size based on the panel's width and height in grid units.
|
||||
var panel_size = Vector2(panel.grid_width * Constants.UI_GRID_SIZE, panel.grid_height * Constants.UI_GRID_SIZE)
|
||||
|
||||
#print(" - %s, Pos %s Size %s" % [panel, start_pos, panel_size])
|
||||
|
||||
# This single function tells the container to position AND size the child
|
||||
# within the given rectangle. The Rect2's origin is the position.
|
||||
fit_child_in_rect(panel, Rect2(start_pos, panel_size))
|
||||
|
||||
# TODO: Expose grid to install panels
|
||||
|
||||
func toggle_wiring_mode():
|
||||
wiring_mode = !wiring_mode
|
||||
|
||||
for panel in installed_panels:
|
||||
panel.set_wiring_mode(wiring_mode)
|
||||
|
||||
if wiring_mode:
|
||||
_build_databanks(databanks)
|
||||
pass
|
||||
|
||||
if is_instance_valid(databanks_container):
|
||||
if wiring_mode: databanks_container.show()
|
||||
else: databanks_container.hide()
|
||||
|
||||
class InstalledDatabank:
|
||||
extends Control
|
||||
|
||||
var databank_ref: Databank
|
||||
var all_sockets: Array[Socket] = []
|
||||
var SocketScene: PackedScene = preload("res://scenes/ship/computer/wiring/socket.tscn")
|
||||
var inputs_container: VBoxContainer
|
||||
var outputs_container: VBoxContainer
|
||||
|
||||
func _populate_sockets():
|
||||
all_sockets.clear()
|
||||
|
||||
if not is_instance_valid(inputs_container):
|
||||
inputs_container = VBoxContainer.new()
|
||||
add_child(inputs_container)
|
||||
|
||||
if not is_instance_valid(outputs_container):
|
||||
outputs_container = VBoxContainer.new()
|
||||
add_child(outputs_container)
|
||||
|
||||
# Populate Input Sockets
|
||||
for socket_name in databank_ref.get_input_sockets():
|
||||
var socket = SocketScene.instantiate()
|
||||
inputs_container.add_child(socket)
|
||||
socket.initialize(socket_name, Socket.SocketType.INPUT)
|
||||
all_sockets.append(socket)
|
||||
|
||||
# Populate Output Sockets
|
||||
for socket_name in databank_ref.get_output_sockets():
|
||||
var socket = SocketScene.instantiate()
|
||||
outputs_container.add_child(socket)
|
||||
socket.initialize(socket_name, Socket.SocketType.OUTPUT)
|
||||
all_sockets.append(socket)
|
||||
|
||||
func _build_databanks(dbs_to_install: Array[Databank]):
|
||||
if not is_instance_valid(databanks_container):
|
||||
databanks_container = GridContainer.new()
|
||||
databanks_container.columns = columns
|
||||
databanks_container.add_theme_constant_override("h_separation", Constants.UI_GRID_SIZE * 3)
|
||||
databanks_container.add_theme_constant_override("v_separation", Constants.UI_GRID_SIZE + 16)
|
||||
add_child(databanks_container)
|
||||
|
||||
var installed_databanks = databanks_container.get_children()
|
||||
|
||||
for to_install in dbs_to_install:
|
||||
if installed_databanks.any(func(existing_db): return existing_db.databank_ref == to_install):
|
||||
continue
|
||||
|
||||
var installed_databank = InstalledDatabank.new()
|
||||
installed_databank.databank_ref = to_install
|
||||
databanks_container.add_child(installed_databank)
|
||||
installed_databank._populate_sockets()
|
||||
|
||||
func _gui_input(event: InputEvent):
|
||||
if event is InputEventMouseButton:
|
||||
# --- Start or End a Wire ---
|
||||
if event.button_index == MOUSE_BUTTON_LEFT:
|
||||
if event.is_pressed():
|
||||
|
||||
var socket = _get_socket_at_pos(event.position)
|
||||
if socket:
|
||||
current_state = WiringState.DRAGGING_WIRE
|
||||
if not start_socket:
|
||||
# start new wire
|
||||
start_socket = socket
|
||||
# Add start point to wire points
|
||||
active_wire_points.append(start_socket.icon.get_global_rect().get_center() - get_global_position())
|
||||
elif start_socket and socket.socket_type != start_socket.socket_type:
|
||||
end_socket = socket
|
||||
_save_new_connection()
|
||||
_reset_wiring_state()
|
||||
elif current_state == WiringState.DRAGGING_WIRE:
|
||||
# Add intermediate point
|
||||
active_wire_points.append(get_local_mouse_position())
|
||||
|
||||
elif event.button_index == MOUSE_BUTTON_RIGHT:
|
||||
# Pop Last Point
|
||||
active_wire_points.remove_at(active_wire_points.size() - 1)
|
||||
|
||||
# Check if wire points are empty, then we remove the whole wire
|
||||
if active_wire_points.size() <= 0:
|
||||
_reset_wiring_state()
|
||||
|
||||
if event is InputEventMouseMotion and current_state == WiringState.DRAGGING_WIRE:
|
||||
queue_redraw()
|
||||
|
||||
func _draw():
|
||||
# 1. Draw all saved wires from the schematic.
|
||||
if current_schematic:
|
||||
for connection in current_schematic.connections:
|
||||
if connection.path_points.size() > 1:
|
||||
_draw_wire_path(connection.path_points, Color.GREEN)
|
||||
|
||||
# 2. Draw the active wire being dragged by the user.
|
||||
if current_state == WiringState.DRAGGING_WIRE:
|
||||
var live_path: Array[Vector2] = active_wire_points.duplicate()
|
||||
live_path.append(get_local_mouse_position())
|
||||
if live_path.size() > 1:
|
||||
_draw_wire_path(live_path, Color.YELLOW)
|
||||
|
||||
# --- NEW: Helper function to draw a multi-point path ---
|
||||
func _draw_wire_path(points: Array[Vector2], color: Color):
|
||||
for i in range(points.size() - 1):
|
||||
var p1 = points[i]
|
||||
var p2 = points[i+1]
|
||||
# var control_offset = Vector2(abs(p2.x - p1.x) * 0.5, 0)
|
||||
draw_line(p1, p2, color, 3.0)
|
||||
|
||||
func _save_new_connection():
|
||||
var new_connection = WireConnection.new()
|
||||
if start_socket.socket_type == end_socket.socket_type:
|
||||
push_error("Start socket and end socket of same type!")
|
||||
return
|
||||
|
||||
if start_socket.socket_type == Socket.SocketType.INPUT:
|
||||
new_connection.input_socket_name = start_socket.socket_name
|
||||
elif start_socket.socket_type == Socket.SocketType.OUTPUT:
|
||||
new_connection.output_socket_name = start_socket.socket_name
|
||||
|
||||
if end_socket.socket_type == Socket.SocketType.INPUT:
|
||||
new_connection.input_socket_name = end_socket.socket_name
|
||||
elif end_socket.socket_type == Socket.SocketType.OUTPUT:
|
||||
new_connection.output_socket_name = end_socket.socket_name
|
||||
|
||||
|
||||
var end_pos = end_socket.icon.get_global_rect().get_center() - get_global_position()
|
||||
active_wire_points.append(end_pos)
|
||||
new_connection.path_points = active_wire_points
|
||||
|
||||
if not current_schematic:
|
||||
current_schematic = WiringSchematic.new()
|
||||
current_schematic.connections.append(new_connection)
|
||||
print("Connection saved!")
|
||||
|
||||
func _reset_wiring_state():
|
||||
current_state = WiringState.IDLE
|
||||
start_socket = null
|
||||
end_socket = null
|
||||
active_wire_points.clear()
|
||||
queue_redraw()
|
||||
|
||||
func _get_socket_at_pos(global_pos: Vector2) -> Socket:
|
||||
for panel in installed_panels:
|
||||
for socket in panel.all_sockets:
|
||||
if is_instance_valid(socket) and socket.icon.get_global_rect().has_point(global_pos):
|
||||
return socket
|
||||
return null
|
||||
1
scenes/ship/computer/panels/frame/panel_frame.gd.uid
Normal file
1
scenes/ship/computer/panels/frame/panel_frame.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cadvugf4oqgvk
|
||||
9
scenes/ship/computer/panels/readout_screen.tres
Normal file
9
scenes/ship/computer/panels/readout_screen.tres
Normal 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"
|
||||
9
scenes/ship/computer/panels/sensor_panel.tres
Normal file
9
scenes/ship/computer/panels/sensor_panel.tres
Normal 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"
|
||||
9
scenes/ship/computer/panels/throttle_lever.tres
Normal file
9
scenes/ship/computer/panels/throttle_lever.tres
Normal 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"
|
||||
170
scenes/ship/computer/shards/helm_autopilot_databank.gd
Normal file
170
scenes/ship/computer/shards/helm_autopilot_databank.gd
Normal 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
|
||||
@ -0,0 +1 @@
|
||||
uid://ceqdi6jobefnc
|
||||
9
scenes/ship/computer/shards/helm_autopilot_databank.tres
Normal file
9
scenes/ship/computer/shards/helm_autopilot_databank.tres
Normal 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"
|
||||
252
scenes/ship/computer/shards/helm_logic_databank.gd
Normal file
252
scenes/ship/computer/shards/helm_logic_databank.gd
Normal 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
|
||||
1
scenes/ship/computer/shards/helm_logic_databank.gd.uid
Normal file
1
scenes/ship/computer/shards/helm_logic_databank.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://cfbyqvnvf3hna
|
||||
9
scenes/ship/computer/shards/helm_logic_databank.tres
Normal file
9
scenes/ship/computer/shards/helm_logic_databank.tres
Normal 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"
|
||||
37
scenes/ship/computer/shards/helm_ship_status.gd
Normal file
37
scenes/ship/computer/shards/helm_ship_status.gd
Normal 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)
|
||||
1
scenes/ship/computer/shards/helm_ship_status.gd.uid
Normal file
1
scenes/ship/computer/shards/helm_ship_status.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://ctgl5kxyagw0f
|
||||
9
scenes/ship/computer/shards/helm_ship_status.tres
Normal file
9
scenes/ship/computer/shards/helm_ship_status.tres
Normal 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"
|
||||
69
scenes/ship/computer/shards/nav_brachistochrone_planner.gd
Normal file
69
scenes/ship/computer/shards/nav_brachistochrone_planner.gd
Normal file
@ -0,0 +1,69 @@
|
||||
# space_simulation/scenes/ship/computer/shards/nav_brachistochrone_planner.gd
|
||||
extends Databank
|
||||
class_name BrachistochronePlannerShard
|
||||
|
||||
## Emitted when a maneuver plan has been successfully calculated.
|
||||
signal maneuver_calculated(plan: Array[DataTypes.ImpulsiveBurnPlan])
|
||||
|
||||
# --- References ---
|
||||
var target_body: OrbitalBody2D = null
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["target_updated", "calculate_hohmann_transfer"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["maneuver_calculated"]
|
||||
|
||||
# INPUT SOCKET: Connected to the NavSelectionShard's "target_selected" signal.
|
||||
func target_updated(new_target: OrbitalBody2D):
|
||||
print("BRACHISTOCHRONE PLANNER: Target received %s." % new_target.name)
|
||||
target_body = new_target
|
||||
|
||||
# TODO: All positions and velocities for calculating should be gathered from a sensor databank
|
||||
# UI ACTION: A panel button would call this function.
|
||||
func calculate_brachistochrone_transfer():
|
||||
if not is_instance_valid(root_module) or not is_instance_valid(target_body):
|
||||
print("BRACHISTOCHRONE PLANNER: Cannot calculate without ship and target.")
|
||||
return
|
||||
|
||||
# 1. Get total main engine thrust from all thruster components
|
||||
# TODO: This should be gathered from a calibration shard
|
||||
var main_engine_thrust = 0.0
|
||||
for component in root_module.get_components():
|
||||
if component is Thruster and component.main_thruster:
|
||||
main_engine_thrust += component.max_thrust
|
||||
|
||||
if main_engine_thrust == 0.0 or root_module.mass == 0.0:
|
||||
print("BRACHISTOCHRONE PLANNER: Ship has no main engine thrust or mass.")
|
||||
return
|
||||
|
||||
var acceleration = main_engine_thrust / root_module.mass
|
||||
var distance = root_module.global_position.distance_to(target_body.global_position)
|
||||
|
||||
# Using the kinematic equation: d = (1/2)at^2, solved for t: t = sqrt(2d/a)
|
||||
# Since we accelerate for half the distance and decelerate for the other half:
|
||||
var time_for_half_journey = sqrt(distance / acceleration)
|
||||
|
||||
# --- Assemble the plan as two ImpulsiveBurnPlan steps ---
|
||||
var plan: Array[DataTypes.ImpulsiveBurnPlan] = []
|
||||
|
||||
# --- Step 1: Acceleration Burn ---
|
||||
var accel_burn = DataTypes.ImpulsiveBurnPlan.new()
|
||||
accel_burn.wait_time = 0 # Start immediately
|
||||
accel_burn.burn_duration = time_for_half_journey
|
||||
# The desired rotation is the direction vector from ship to target
|
||||
accel_burn.desired_rotation_rad = root_module.global_position.direction_to(target_body.global_position).angle() + (PI / 2.0)
|
||||
plan.append(accel_burn)
|
||||
|
||||
# --- Step 2: Deceleration Burn (The flip is handled by the autopilot between steps) ---
|
||||
var decel_burn = DataTypes.ImpulsiveBurnPlan.new()
|
||||
decel_burn.wait_time = 0 # No coasting period
|
||||
decel_burn.burn_duration = time_for_half_journey
|
||||
# The desired rotation is opposite the first burn
|
||||
decel_burn.desired_rotation_rad = accel_burn.desired_rotation_rad + PI
|
||||
plan.append(decel_burn)
|
||||
|
||||
print("BRACHISTOCHRONE PLANNER: Plan calculated. Total time: %.2f s" % (time_for_half_journey * 2.0))
|
||||
maneuver_calculated.emit(plan)
|
||||
@ -0,0 +1 @@
|
||||
uid://ghluwjd5c5ul
|
||||
@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://bnyce8i208qby"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://ghluwjd5c5ul" path="res://scenes/ship/computer/shards/nav_brachistochrone_planner.gd" id="1_asajk"]
|
||||
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="1_xdqj8"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("1_xdqj8")
|
||||
logic_script = ExtResource("1_asajk")
|
||||
metadata/_custom_type_script = "uid://osk1l75vlikn"
|
||||
150
scenes/ship/computer/shards/nav_hohman_planner.gd
Normal file
150
scenes/ship/computer/shards/nav_hohman_planner.gd
Normal 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
|
||||
}
|
||||
1
scenes/ship/computer/shards/nav_hohman_planner.gd.uid
Normal file
1
scenes/ship/computer/shards/nav_hohman_planner.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bghu5lhcbcfmh
|
||||
9
scenes/ship/computer/shards/nav_hohman_planner.tres
Normal file
9
scenes/ship/computer/shards/nav_hohman_planner.tres
Normal 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"
|
||||
79
scenes/ship/computer/shards/nav_intercept_solver.gd
Normal file
79
scenes/ship/computer/shards/nav_intercept_solver.gd
Normal file
@ -0,0 +1,79 @@
|
||||
# space_simulation/scenes/ship/computer/shards/nav_intercept_solver.gd
|
||||
extends Databank
|
||||
class_name InterceptSolverShard
|
||||
|
||||
signal solution_found(plan: Array[DataTypes.ImpulsiveBurnPlan])
|
||||
signal solution_impossible
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["project_n_body_paths"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["solution_found", "solution_impossible"]
|
||||
|
||||
# INPUT SOCKET: Planners will call this with a projected path.
|
||||
func solve_rendezvous_plan(
|
||||
target_path: Array[DataTypes.PathPoint],
|
||||
maneuver_type: String # e.g., "brachistochrone" or "hohmann"
|
||||
):
|
||||
if not is_instance_valid(root_module) or target_path.is_empty():
|
||||
emit_signal("solution_impossible")
|
||||
return
|
||||
|
||||
var rendezvous_point = find_earliest_rendezvous(target_path)
|
||||
|
||||
if not rendezvous_point:
|
||||
emit_signal("solution_impossible")
|
||||
return
|
||||
|
||||
# Once we have the target point (time, pos, vel), we can generate
|
||||
# the specific burn plan based on the requested type.
|
||||
var plan: Array[DataTypes.ImpulsiveBurnPlan]
|
||||
match maneuver_type:
|
||||
"brachistochrone":
|
||||
plan = _generate_brachistochrone_plan(rendezvous_point)
|
||||
# "hohmann" would be more complex, as it has constraints
|
||||
_:
|
||||
print("Unknown maneuver type for solver.")
|
||||
emit_signal("solution_impossible")
|
||||
return
|
||||
|
||||
emit_signal("solution_found", plan)
|
||||
|
||||
|
||||
# This is the core solver logic.
|
||||
func find_earliest_rendezvous(target_path: Array[DataTypes.PathPoint]) -> DataTypes.PathPoint:
|
||||
# For each point in the target's future path...
|
||||
for point in target_path:
|
||||
# 1. Calculate the required change in position (displacement).
|
||||
var delta_p = point.position - root_module.global_position
|
||||
|
||||
# 2. Calculate the required change in velocity.
|
||||
var delta_v = point.velocity - root_module.linear_velocity
|
||||
|
||||
# 3. Using kinematics (d = v_initial*t + 0.5at^2), find the constant
|
||||
# acceleration 'a' required to satisfy both delta_p and delta_v over
|
||||
# the time 'point.time'.
|
||||
# a = 2 * (delta_p - root_module.linear_velocity * point.time) / (point.time * point.time)
|
||||
var required_acceleration_vector = 2.0 * (delta_p - root_module.linear_velocity * point.time) / (point.time * point.time)
|
||||
|
||||
# 4. Check if the magnitude of this required acceleration is something our ship can actually do.
|
||||
var max_accel = root_module.main_engine_thrust / root_module.mass # Assumes we need a get_main_engine_thrust() helper
|
||||
|
||||
if required_acceleration_vector.length() <= max_accel:
|
||||
# This is the first point in time we can reach. This is our solution.
|
||||
return point
|
||||
|
||||
# If we get through the whole path and can't reach any of them, it's impossible.
|
||||
return null
|
||||
|
||||
|
||||
func _generate_brachistochrone_plan(rendezvous_point: DataTypes.PathPoint) -> Array[DataTypes.ImpulsiveBurnPlan]:
|
||||
# This function would now use the data from the solved rendezvous_point
|
||||
# to create the two-burn Brachistochrone plan, similar to before.
|
||||
# The key difference is that all calculations are now based on a confirmed possible intercept.
|
||||
var plan: Array[DataTypes.ImpulsiveBurnPlan] = []
|
||||
# ... logic to build the plan ...
|
||||
return plan
|
||||
1
scenes/ship/computer/shards/nav_intercept_solver.gd.uid
Normal file
1
scenes/ship/computer/shards/nav_intercept_solver.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://dsbn7ushwqrko
|
||||
87
scenes/ship/computer/shards/nav_projection_shard.gd
Normal file
87
scenes/ship/computer/shards/nav_projection_shard.gd
Normal file
@ -0,0 +1,87 @@
|
||||
# space_simulation/scenes/ship/computer/shards/nav_path_projection.gd
|
||||
extends Databank
|
||||
class_name PathProjectionShard
|
||||
|
||||
## Emitted after a requested path has been calculated.
|
||||
signal projected_system_bus(paths: Array[DataTypes.PathPoint])
|
||||
|
||||
## Describes the functions this shard needs as input.
|
||||
func get_input_sockets() -> Array[String]:
|
||||
return ["project_n_body_paths"]
|
||||
|
||||
## Describes the signals this shard can output.
|
||||
func get_output_sockets() -> Array[String]:
|
||||
return ["projected_system_bus"]
|
||||
|
||||
## Projects the future paths of an array of bodies interacting with each other.
|
||||
## Returns a dictionary mapping each body to its calculated PackedVector2Array path.
|
||||
func project_n_body_paths(
|
||||
bodies_to_trace: Array[OrbitalBody2D],
|
||||
num_steps: int,
|
||||
time_step: float
|
||||
):
|
||||
|
||||
# --- Step 1: Create a "ghost state" for each body ---
|
||||
# A ghost state is just a simple dictionary holding the physics properties.
|
||||
var ghost_states = []
|
||||
for body in bodies_to_trace:
|
||||
ghost_states.append({
|
||||
"body_ref": body,
|
||||
"mass": body.mass,
|
||||
"position": body.global_position,
|
||||
"velocity": body.linear_velocity # Velocity is always in the same space
|
||||
})
|
||||
|
||||
# --- Step 2: Prepare the results dictionary ---
|
||||
var paths: Dictionary = {}
|
||||
for state in ghost_states:
|
||||
paths[state.body_ref] = []
|
||||
|
||||
|
||||
# --- Step 3: Run the ghost simulation ---
|
||||
for i in range(num_steps):
|
||||
# Create a list to hold the forces for this time step
|
||||
var forces_for_step = {}
|
||||
for state in ghost_states:
|
||||
forces_for_step[state.body_ref] = Vector2.ZERO
|
||||
|
||||
# a) Calculate all gravitational forces between the ghosts
|
||||
for j in range(ghost_states.size()):
|
||||
var state_a = ghost_states[j]
|
||||
for k in range(j + 1, ghost_states.size()):
|
||||
var state_b = ghost_states[k]
|
||||
|
||||
# Calculate force between the two ghost states2:
|
||||
var distance_sq = state_a.position.distance_squared_to(state_b.position)
|
||||
if distance_sq < 1.0: return Vector2.ZERO
|
||||
|
||||
var force_magnitude = (OrbitalMechanics.G * state_a.mass * state_b.mass) / distance_sq
|
||||
var direction = state_a.position.direction_to(state_b.position)
|
||||
var force_vector = direction * force_magnitude
|
||||
|
||||
# Store the forces to be applied
|
||||
forces_for_step[state_a.body_ref] += force_vector
|
||||
forces_for_step[state_b.body_ref] -= force_vector
|
||||
|
||||
# b) Integrate forces for each ghost to find its next position
|
||||
for state in ghost_states:
|
||||
if state.mass > 0:
|
||||
var acceleration = forces_for_step[state.body_ref] / state.mass
|
||||
state.velocity += acceleration * time_step
|
||||
state.position += state.velocity * time_step
|
||||
|
||||
|
||||
|
||||
# c) Record the new position in the path
|
||||
paths[state.body_ref].append(DataTypes.PathPoint.new(i * time_step, state.position, state.velocity))
|
||||
|
||||
# --- Step 4: Prepare the results dictionary ---
|
||||
var projections: Array[DataTypes.PathProjection] = []
|
||||
for state in ghost_states:
|
||||
var projection: DataTypes.PathProjection = DataTypes.PathProjection.new(state.body_ref)
|
||||
|
||||
projection.points = paths[state.body_ref]
|
||||
|
||||
projections.append(projection)
|
||||
|
||||
projected_system_bus.emit(paths)
|
||||
1
scenes/ship/computer/shards/nav_projection_shard.gd.uid
Normal file
1
scenes/ship/computer/shards/nav_projection_shard.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://0f6v6iu3o5qo
|
||||
9
scenes/ship/computer/shards/nav_projection_shard.tres
Normal file
9
scenes/ship/computer/shards/nav_projection_shard.tres
Normal file
@ -0,0 +1,9 @@
|
||||
[gd_resource type="Resource" script_class="Databank" load_steps=3 format=3 uid="uid://d4e5f6g7h8jaj"]
|
||||
|
||||
[ext_resource type="Script" path="res://scenes/ship/computer/shards/nav_path_projection.gd" id="1_proj"]
|
||||
[ext_resource type="Script" uid="uid://osk1l75vlikn" path="res://scenes/ship/computer/databank.gd" id="2_data"]
|
||||
|
||||
[resource]
|
||||
script = ExtResource("2_data")
|
||||
logic_script = ExtResource("1_proj")
|
||||
metadata/_custom_type_script = "uid://osk1l75vlikn"
|
||||
23
scenes/ship/computer/shards/nav_selection_databank.gd
Normal file
23
scenes/ship/computer/shards/nav_selection_databank.gd
Normal 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)
|
||||
@ -0,0 +1 @@
|
||||
uid://t12etsdx2h38
|
||||
9
scenes/ship/computer/shards/nav_selection_databank.tres
Normal file
9
scenes/ship/computer/shards/nav_selection_databank.tres
Normal 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
Reference in New Issue
Block a user