Compare commits
63 Commits
refactor/o
...
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 | |||
| c2a0b6c026 | |||
| 09a4003839 | |||
| 7c689f6023 |
@ -1,33 +1,246 @@
|
||||
# Project "Stardust Drifter" Status Summary
|
||||
# Project "Millimeters of Aluminium" Development Log
|
||||
## Overview
|
||||
|
||||
## Implemented Systems
|
||||
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.
|
||||
|
||||
- Core Physics Simulation: The OrbitalMechanics singleton successfully handles a robust N-body gravity simulation. All celestial bodies and the ship correctly inherit from OrbitalBody2D, ensuring they are part of the same physics simulation.
|
||||
## I. Fully Implemented & Stable Systems
|
||||
|
||||
- Procedural World Generation: The StarSystemGenerator creates a dynamic solar system with stars, planets, moons, and stations in stable, nested orbits.
|
||||
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 Ship Systems: A foundation for modular ships is in place. The Spaceship class is a central hub for subsystems like FuelSystem and LifeSupport. The ThrusterController is highly advanced, capable of self-calibrating to a ship's unique mass and inertia.
|
||||
Modular Ship Construction:
|
||||
|
||||
- Navigation & Map UI: A functional map UI exists, with zoom-to-cursor and click-and-drag panning. It includes dynamic culling and can be locked onto any celestial body. The NavigationComputer can calculate and execute Hohmann transfers.
|
||||
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.
|
||||
|
||||
- Editor Plugin (WIP): We've started building an editor plugin to handle ship construction. The core functionality for creating a custom dock and listening for editor input has been implemented, but the full UI and piece-placement logic still needs to be completed.
|
||||
Builder Plugin: The editor plugin is updated to work with this new architecture, allowing the placement of StructuralPiece nodes directly onto Module nodes.
|
||||
|
||||
## Designed but Unimplemented Systems
|
||||
Character & Interaction Foundation:
|
||||
|
||||
- Free-Form Module Construction: The core building system is designed but not yet fully implemented. The Module and StructuralPiece scripts are in place, but the physics recalculation and room sealing logic is not yet finished.
|
||||
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.
|
||||
|
||||
- Unified RigidBody2D Character Controller: The CrewMember.tscn scene and the logic for Spaceship.gd to simulate G-forces still need to be created and integrated.
|
||||
Generic Station Component: A StationComponent class has been implemented. It serves as a generic hardware terminal that characters can interact with.
|
||||
|
||||
- Economy, Missions, and Factions: The high-level design for a multi-faction world, a mission system, and an economy exists, but no code or assets have been created for these features yet.
|
||||
Data-Driven UI Architecture:
|
||||
|
||||
- Multi-Depth Modules: Your idea for a 2.5D building system with multiple depth layers and a "flip" function is designed but has been tabled for later.
|
||||
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.
|
||||
|
||||
### Planned Systems: Asymmetric N-Body Physics Simulation
|
||||
## II. Work-In-Progress (WIP) and Planned Systems
|
||||
|
||||
To handle the vast difference in scale between celestial bodies and player ships, the physics simulation will be refactored to use an asymmetric N-body approach. This means that while all celestial bodies will affect each other, smaller bodies like ships and stations will only be affected by the gravity of larger celestial bodies. This will allow the use of realistic masses for ship components while maintaining stable and predictable orbits for planets and moons.
|
||||
This list details systems we have designed but are not yet fully implemented in the code.
|
||||
|
||||
- Objective: Modify the OrbitalMechanics singleton to apply a scaling factor to gravitational calculations for player-controlled ships and components.
|
||||
System Migration to Databanks:
|
||||
|
||||
- Implementation: Introduce const SCALE_FACTOR = 100000.0 to the OrbitalMechanics.gd script. When calculating gravity for ships and other scaled bodies, multiply the masses of celestial bodies by this factor.
|
||||
Helm/Flight Controls: The logic from the old ThrusterController.gd needs to be moved into a HelmUI.tscn scene and driven by a HelmDatabank.
|
||||
|
||||
- New Class: A new abstract class, ScaledOrbitalBody2D, will be created that inherits from OrbitalBody2D and handles the new scaled physics.
|
||||
Navigation Computer: The UI has been moved, but the extensive planning and calculation logic from NavigationComputer.gd needs to be transferred to NavUI.gd.
|
||||
|
||||
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).
|
||||
|
||||
Component Wiring System:
|
||||
|
||||
Signal/Socket Advertising: Components and Databanks need to be updated with get_input_sockets() and get_output_signals() functions.
|
||||
|
||||
Wiring Data Storage: The Module class needs a wiring_data array to store the connections made in the builder.
|
||||
|
||||
Builder UI: A visual wiring interface needs to be added to the module builder plugin.
|
||||
|
||||
Orbital Stability Test:
|
||||
|
||||
Ghost Simulator: A GhostSimulator class needs to be created to run predictive, in-memory physics calculations.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
### ✅ 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.
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
#### 2. Player Control & Multiplayer Foundation
|
||||
PlayerController/Pawn Architecture: A multiplayer-ready control scheme has been implemented.
|
||||
|
||||
The PlayerController class is responsible for capturing raw input and sending it to the server via RPCs.
|
||||
|
||||
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.
|
||||
|
||||
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)`)
|
||||
@ -28,14 +28,25 @@ var save_button: Button
|
||||
var builder_scene_root: Node2D
|
||||
var builder_camera: Camera2D
|
||||
|
||||
# --- State Management Enum ---
|
||||
enum BuilderState {
|
||||
IDLE,
|
||||
PLACING_STRUCTURAL,
|
||||
PLACING_COMPONENT,
|
||||
WIRING # For future use
|
||||
}
|
||||
var current_state: BuilderState = BuilderState.IDLE
|
||||
|
||||
# --- State Variables ---
|
||||
var preview_piece: StructuralPiece = null
|
||||
var active_piece_scene: PackedScene = null
|
||||
var preview_node = null # Can be either StructuralPiece or Component
|
||||
var active_scene: PackedScene = null # Can be either StructuralPiece or Component
|
||||
var rotation_angle: float = 0.0
|
||||
var grid_size: float = 50.0
|
||||
|
||||
var undo_redo: EditorUndoRedoManager
|
||||
|
||||
# --- Most of the setup functions remain the same ---
|
||||
|
||||
func _enter_tree():
|
||||
main_screen = MAIN_EDITOR_SCENE.instantiate()
|
||||
EditorInterface.get_editor_main_screen().add_child(main_screen)
|
||||
@ -87,13 +98,11 @@ func _setup_docks():
|
||||
add_control_to_dock(DOCK_SLOT_RIGHT_UL, construction_inspector_dock)
|
||||
|
||||
func switch_to_dock_tab(dock_control: Control, tab_name: String):
|
||||
# Find the TabContainer within the dock's control node.
|
||||
var tab_container = dock_control.find_child("TabContainer")
|
||||
if not is_instance_valid(tab_container):
|
||||
print("Error: TabContainer not found in dock control.")
|
||||
return
|
||||
|
||||
# Iterate through the tabs to find the one with the correct name.
|
||||
for i in range(tab_container.get_tab_count()):
|
||||
if tab_container.get_tab_title(i) == tab_name:
|
||||
tab_container.current_tab = i
|
||||
@ -159,7 +168,7 @@ func _update_ui_labels():
|
||||
func _process(_delta):
|
||||
_update_ui_labels()
|
||||
_refresh_tree_display()
|
||||
|
||||
|
||||
func _on_viewport_input(event: InputEvent) -> void:
|
||||
if event is InputEventMouseMotion and event.button_mask & MOUSE_BUTTON_MASK_RIGHT:
|
||||
builder_camera.position -= event.relative / builder_camera.zoom
|
||||
@ -168,56 +177,97 @@ func _on_viewport_input(event: InputEvent) -> void:
|
||||
builder_camera.zoom *= 1.1
|
||||
elif event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
|
||||
builder_camera.zoom /= 1.1
|
||||
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
|
||||
if is_instance_valid(preview_piece):
|
||||
on_clear_preview_piece()
|
||||
else:
|
||||
_remove_piece_under_mouse()
|
||||
|
||||
|
||||
if event is InputEventMouseMotion:
|
||||
_update_preview_position()
|
||||
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
|
||||
_place_piece_from_preview()
|
||||
match current_state:
|
||||
BuilderState.IDLE:
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
|
||||
_remove_piece_under_mouse()
|
||||
|
||||
BuilderState.PLACING_STRUCTURAL:
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
|
||||
_place_piece_from_preview()
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
|
||||
on_clear_preview()
|
||||
|
||||
BuilderState.PLACING_COMPONENT:
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
|
||||
_place_component_from_preview()
|
||||
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
|
||||
on_clear_preview()
|
||||
|
||||
BuilderState.WIRING:
|
||||
pass
|
||||
|
||||
|
||||
func _unhandled_key_input(event: InputEvent):
|
||||
if not event.is_pressed(): return
|
||||
if event is InputEventKey and event.as_text() == "R":
|
||||
if event is InputEventKey and event.as_text().to_lower() == "r":
|
||||
_on_rotate_button_pressed()
|
||||
get_tree().set_input_as_handled()
|
||||
|
||||
|
||||
func on_active_piece_set(scene: PackedScene):
|
||||
if is_instance_valid(preview_piece):
|
||||
preview_piece.queue_free()
|
||||
if is_instance_valid(preview_node):
|
||||
preview_node.queue_free()
|
||||
|
||||
active_piece_scene = scene
|
||||
preview_piece = scene.instantiate() as StructuralPiece
|
||||
preview_piece.is_preview = true
|
||||
builder_scene_root.add_child(preview_piece)
|
||||
current_state = BuilderState.PLACING_STRUCTURAL
|
||||
active_scene = scene
|
||||
preview_node = scene.instantiate() as StructuralPiece
|
||||
preview_node.is_preview = true
|
||||
builder_scene_root.add_child(preview_node)
|
||||
_update_preview_position()
|
||||
|
||||
func on_clear_preview_piece():
|
||||
if is_instance_valid(preview_piece):
|
||||
preview_piece.queue_free()
|
||||
preview_piece = null
|
||||
active_piece_scene = null
|
||||
func _on_component_selected(component_scene: PackedScene):
|
||||
if is_instance_valid(preview_node):
|
||||
preview_node.queue_free()
|
||||
|
||||
current_state = BuilderState.PLACING_COMPONENT
|
||||
active_scene = component_scene
|
||||
preview_node = component_scene.instantiate() as Component
|
||||
builder_scene_root.add_child(preview_node)
|
||||
|
||||
print("Now placing component: ", component_scene.resource_path)
|
||||
|
||||
func on_clear_preview():
|
||||
if is_instance_valid(preview_node):
|
||||
preview_node.queue_free()
|
||||
preview_node = null
|
||||
active_scene = null
|
||||
current_state = BuilderState.IDLE
|
||||
|
||||
|
||||
func _update_preview_position():
|
||||
if not is_instance_valid(preview_piece):
|
||||
if not is_instance_valid(preview_node):
|
||||
return
|
||||
|
||||
var viewport: SubViewport = main_screen.find_child("SubViewport")
|
||||
if not viewport: return
|
||||
|
||||
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
|
||||
var snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
|
||||
preview_piece.global_position = snapped_pos
|
||||
preview_piece.rotation = rotation_angle
|
||||
|
||||
match current_state:
|
||||
BuilderState.PLACING_STRUCTURAL:
|
||||
var snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
|
||||
preview_node.global_position = snapped_pos
|
||||
preview_node.rotation = rotation_angle
|
||||
BuilderState.PLACING_COMPONENT:
|
||||
var target_module = _find_first_module()
|
||||
if target_module:
|
||||
var closest_point = _find_closest_attachment_point(target_module, world_mouse_pos)
|
||||
if closest_point:
|
||||
preview_node.global_position = closest_point.position
|
||||
else:
|
||||
preview_node.global_position = world_mouse_pos
|
||||
else:
|
||||
preview_node.global_position = world_mouse_pos
|
||||
|
||||
|
||||
# --- REFACTORED: Piece Placement ---
|
||||
func _place_piece_from_preview():
|
||||
if not is_instance_valid(preview_piece):
|
||||
if not is_instance_valid(preview_node) or not is_instance_valid(active_scene):
|
||||
return
|
||||
|
||||
var viewport: SubViewport = main_screen.find_child("SubViewport")
|
||||
@ -233,38 +283,69 @@ func _place_piece_from_preview():
|
||||
target_module.global_position = snapped_pos
|
||||
target_module.owner = builder_scene_root
|
||||
|
||||
var piece_to_place = active_piece_scene.instantiate()
|
||||
target_module.structural_container.add_child(piece_to_place)
|
||||
var piece_to_place = active_scene.instantiate()
|
||||
|
||||
# --- The main change: Add as a direct child of the module ---
|
||||
target_module.add_child(piece_to_place)
|
||||
piece_to_place.owner = target_module
|
||||
piece_to_place.rotation = rotation_angle
|
||||
piece_to_place.global_position = snapped_pos
|
||||
|
||||
# --- The Undo/Redo Logic ---
|
||||
undo_redo.create_action("Place Structural Piece")
|
||||
|
||||
# DO method: adds the piece to the scene.
|
||||
undo_redo.add_do_method(target_module.structural_container, "add_child", piece_to_place)
|
||||
undo_redo.add_do_method(target_module, "add_child", piece_to_place)
|
||||
undo_redo.add_do_method(piece_to_place, "set_owner", target_module)
|
||||
|
||||
# DO method: recalculates physics.
|
||||
undo_redo.add_do_method(target_module, "_recalculate_collision_shape")
|
||||
|
||||
# UNDO method: removes the piece from the scene parent.
|
||||
undo_redo.add_undo_method(target_module.structural_container, "remove_child", piece_to_place)
|
||||
# UNDO method: recalculates physics.
|
||||
undo_redo.add_undo_method(target_module, "remove_child", piece_to_place)
|
||||
undo_redo.add_undo_method(target_module, "_recalculate_collision_shape")
|
||||
|
||||
undo_redo.commit_action()
|
||||
|
||||
# --- Component Placement remains the same ---
|
||||
func _place_component_from_preview():
|
||||
if not is_instance_valid(preview_node) or not is_instance_valid(active_scene):
|
||||
push_error("Cannot place component: Invalid preview or scene.")
|
||||
return
|
||||
|
||||
var viewport: SubViewport = main_screen.find_child("SubViewport")
|
||||
if not viewport: return
|
||||
|
||||
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
|
||||
|
||||
var target_module = _find_first_module()
|
||||
if not target_module:
|
||||
push_error("No module found to attach component to.")
|
||||
return
|
||||
|
||||
var closest_point = _find_closest_attachment_point(target_module, world_mouse_pos)
|
||||
if not closest_point:
|
||||
print("No valid attachment point nearby.")
|
||||
return
|
||||
|
||||
var component_to_place = active_scene.instantiate() as Component
|
||||
|
||||
target_module.attach_component(component_to_place, closest_point.position, closest_point.piece)
|
||||
|
||||
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, "recalculate_physical_properties")
|
||||
undo_redo.add_undo_method(target_module, "recalculate_physical_properties")
|
||||
undo_redo.commit_action()
|
||||
|
||||
preview_node.global_position = closest_point.position
|
||||
|
||||
# --- Find Nearby Modules remains the same ---
|
||||
func _find_nearby_modules(position: Vector2) -> Module:
|
||||
# Define a margin for the overlap check.
|
||||
const OVERLAP_MARGIN = 20.0
|
||||
|
||||
# Get the shape from the active piece scene.
|
||||
var piece_shape = active_piece_scene.instantiate().find_child("CollisionShape2D").shape
|
||||
if not active_scene or not active_scene.can_instantiate(): return null
|
||||
var piece_instance = active_scene.instantiate()
|
||||
var shape_node = piece_instance.find_child("CollisionShape2D")
|
||||
if not shape_node:
|
||||
piece_instance.queue_free()
|
||||
return null
|
||||
var piece_shape = shape_node.shape
|
||||
piece_instance.queue_free()
|
||||
|
||||
# Create a temporary, slightly larger shape for the overlap check.
|
||||
var enlarged_shape
|
||||
if piece_shape is RectangleShape2D:
|
||||
enlarged_shape = RectangleShape2D.new()
|
||||
@ -274,63 +355,78 @@ func _find_nearby_modules(position: Vector2) -> Module:
|
||||
enlarged_shape.radius = piece_shape.radius + OVERLAP_MARGIN
|
||||
enlarged_shape.height = piece_shape.height + OVERLAP_MARGIN
|
||||
else:
|
||||
# Fallback for other shapes
|
||||
return null
|
||||
|
||||
# Use a PhysicsShapeQuery to find overlapping pieces.
|
||||
var space_state = builder_world.direct_space_state
|
||||
var query = PhysicsShapeQueryParameters2D.new()
|
||||
query.set_shape(enlarged_shape)
|
||||
query.transform = Transform2D(0, position)
|
||||
|
||||
var result = space_state.intersect_shape(query, 1) # Limit to a single result
|
||||
var result = space_state.intersect_shape(query, 1)
|
||||
|
||||
if not result.is_empty():
|
||||
var collider = result[0].get("collider")
|
||||
if collider is StructuralPiece:
|
||||
if collider.get_parent() and collider.get_parent().get_parent() is Module:
|
||||
return collider.get_parent().get_parent()
|
||||
# --- REFACTORED: The module is now the direct parent/owner ---
|
||||
if is_instance_valid(collider.owner) and collider.owner is Module:
|
||||
return collider.owner
|
||||
|
||||
return null
|
||||
|
||||
|
||||
func _find_first_module() -> Module:
|
||||
for node in builder_scene_root.get_children():
|
||||
if node is Module:
|
||||
return node
|
||||
return null
|
||||
|
||||
func _remove_piece_under_mouse():
|
||||
var viewport: SubViewport = main_screen.find_child("SubViewport")
|
||||
if not viewport: return
|
||||
var world_mouse_pos = viewport.get_canvas_transform().affine_inverse() * viewport.get_mouse_position()
|
||||
var snapped_pos = world_mouse_pos.snapped(Vector2(grid_size, grid_size))
|
||||
|
||||
var space_state = builder_world.direct_space_state
|
||||
var query = PhysicsPointQueryParameters2D.new()
|
||||
query.position = world_mouse_pos
|
||||
var result = space_state.intersect_point(query, 1)
|
||||
|
||||
for node in builder_scene_root.get_children():
|
||||
if node is Module:
|
||||
for piece in node.structural_container.get_children():
|
||||
if piece is StructuralPiece and piece.global_position == snapped_pos:
|
||||
_remove_piece_with_undo_redo(piece)
|
||||
return
|
||||
if not result.is_empty():
|
||||
var collider = result[0].get("collider")
|
||||
if collider is StructuralPiece:
|
||||
_remove_piece_with_undo_redo(collider)
|
||||
elif collider is Component:
|
||||
pass
|
||||
|
||||
|
||||
# --- REFACTORED: Piece Removal ---
|
||||
func _remove_piece_with_undo_redo(piece: StructuralPiece):
|
||||
var module = piece.owner as Module
|
||||
var parent = piece.get_parent()
|
||||
if not is_instance_valid(module) or not module is Module:
|
||||
return
|
||||
|
||||
undo_redo.create_action("Remove Structural Piece")
|
||||
print(module.structural_container.get_child_count(false))
|
||||
if module.structural_container.get_child_count(false) >= 1:
|
||||
|
||||
# If this is the last structural piece of the module...
|
||||
if module.get_structural_pieces().size() == 1:
|
||||
# ...remove the entire module.
|
||||
undo_redo.add_do_method(builder_scene_root, "remove_child", module)
|
||||
undo_redo.add_undo_method(builder_scene_root, "add_child", module)
|
||||
undo_redo.add_undo_method(module, "set_owner", builder_scene_root)
|
||||
else:
|
||||
undo_redo.add_do_method(parent, "remove_child", piece)
|
||||
# Otherwise, just remove the single piece from its parent (the module).
|
||||
undo_redo.add_do_method(module, "remove_child", piece)
|
||||
undo_redo.add_do_method(module, "_recalculate_collision_shape")
|
||||
|
||||
undo_redo.add_undo_method(parent, "add_child", piece)
|
||||
undo_redo.add_undo_method(module, "add_child", piece)
|
||||
undo_redo.add_undo_method(piece, "set_owner", module) # Re-assign owner on undo
|
||||
undo_redo.add_undo_method(module, "_recalculate_collision_shape")
|
||||
|
||||
undo_redo.commit_action()
|
||||
|
||||
# --- Toolbar Button Functions ---
|
||||
# --- Toolbar Button Functions (No changes needed) ---
|
||||
func _on_rotate_button_pressed():
|
||||
rotation_angle = wrapf(rotation_angle + PI / 2, 0, TAU)
|
||||
if preview_piece:
|
||||
preview_piece.rotation = rotation_angle
|
||||
if is_instance_valid(preview_node):
|
||||
preview_node.rotation = rotation_angle
|
||||
_update_preview_position()
|
||||
|
||||
func _on_center_button_pressed():
|
||||
@ -341,52 +437,33 @@ func _on_pressurise_button_pressed():
|
||||
pass
|
||||
|
||||
func _on_save_button_pressed():
|
||||
# Find a module to save. We'll prioritize the selected one.
|
||||
var module_to_save: Module
|
||||
var selected_nodes = EditorInterface.get_selection().get_selected_nodes()
|
||||
if not selected_nodes.is_empty() and selected_nodes[0] is Module:
|
||||
module_to_save = selected_nodes[0]
|
||||
elif builder_scene_root.get_child_count() > 0:
|
||||
for node in builder_scene_root.get_children():
|
||||
if node is Module:
|
||||
module_to_save = node
|
||||
break
|
||||
|
||||
else:
|
||||
module_to_save = _find_first_module()
|
||||
|
||||
if not is_instance_valid(module_to_save):
|
||||
push_error("Error: No Module node found or selected to save.")
|
||||
return
|
||||
|
||||
# Create and configure the save dialog
|
||||
var save_dialog = EditorFileDialog.new()
|
||||
save_dialog.file_mode = EditorFileDialog.FILE_MODE_SAVE_FILE
|
||||
save_dialog.add_filter("*.tscn; Godot Scene")
|
||||
save_dialog.current_path = "res://modules/" + module_to_save.name + ".tscn"
|
||||
|
||||
# FIX: Add the dialog to the main editor screen, not the plugin's control node
|
||||
EditorInterface.get_editor_main_screen().add_child(save_dialog)
|
||||
save_dialog.popup_centered_ratio()
|
||||
|
||||
# Connect the signal to our new save function
|
||||
save_dialog.file_selected.connect(Callable(self, "_perform_save").bind(module_to_save))
|
||||
|
||||
func _perform_save(file_path: String, module_to_save: Module):
|
||||
# Make sure the directory exists before attempting to save.
|
||||
var save_dir = file_path.get_base_dir()
|
||||
var dir = DirAccess.open("res://")
|
||||
if not dir.dir_exists(save_dir):
|
||||
dir.make_dir_recursive(save_dir)
|
||||
#
|
||||
## FIX: Manually get the structural container reference from the duplicated module.
|
||||
#var duplicate_structural_container = duplicate_module.find_child("StructuralContainer")
|
||||
#
|
||||
#if is_instance_valid(duplicate_structural_container):
|
||||
## FIX: Correctly set the owner of all child nodes to the new root.
|
||||
## This is the crucial step to ensure the children are packed correctly.
|
||||
#for piece in duplicate_structural_container.get_children():
|
||||
#print(piece)
|
||||
#piece.owner = duplicate_module
|
||||
|
||||
# Pack the node into a PackedScene.
|
||||
var packed_scene = PackedScene.new()
|
||||
var error = packed_scene.pack(module_to_save)
|
||||
|
||||
@ -394,10 +471,6 @@ func _perform_save(file_path: String, module_to_save: Module):
|
||||
push_error("Error packing scene: ", error_string(error))
|
||||
return
|
||||
|
||||
# FIX: Reset the duplicated module's position so it's centered in its own scene.
|
||||
#duplicate_module.global_position = Vector2.ZERO
|
||||
|
||||
# Save the PackedScene to a file.
|
||||
var save_result = ResourceSaver.save(packed_scene, file_path)
|
||||
|
||||
if save_result == OK:
|
||||
@ -409,7 +482,8 @@ func _perform_save(file_path: String, module_to_save: Module):
|
||||
|
||||
func _on_undo_redo_action_committed():
|
||||
_refresh_tree_display()
|
||||
|
||||
|
||||
# --- REFACTORED: Tree Display ---
|
||||
func _refresh_tree_display():
|
||||
if not is_instance_valid(tree_control):
|
||||
return
|
||||
@ -425,8 +499,26 @@ func _refresh_tree_display():
|
||||
module_item.set_text(0, module.name)
|
||||
module_item.set_meta("node", module)
|
||||
|
||||
for piece in module.structural_container.get_children():
|
||||
if piece is StructuralPiece:
|
||||
var piece_item = tree_control.create_item(module_item)
|
||||
piece_item.set_text(0, piece.name)
|
||||
piece_item.set_meta("node", piece)
|
||||
# Use the module's helper functions to find children
|
||||
for piece in module.get_structural_pieces():
|
||||
var piece_item = tree_control.create_item(module_item)
|
||||
piece_item.set_text(0, piece.name)
|
||||
piece_item.set_meta("node", piece)
|
||||
|
||||
for component in module.get_components():
|
||||
var component_item = tree_control.create_item(module_item)
|
||||
component_item.set_text(0, component.name)
|
||||
component_item.set_meta("node", component)
|
||||
|
||||
|
||||
func _find_closest_attachment_point(module: Module, world_pos: Vector2):
|
||||
var min_distance_sq = module.COMPONENT_GRID_SIZE * module.COMPONENT_GRID_SIZE * 0.5
|
||||
var closest_point = null
|
||||
|
||||
for point in module.get_attachment_points():
|
||||
var dist_sq = point.position.distance_squared_to(world_pos)
|
||||
if dist_sq < min_distance_sq:
|
||||
min_distance_sq = dist_sq
|
||||
closest_point = point
|
||||
|
||||
return closest_point
|
||||
|
||||
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")
|
||||
|
||||
31
modules/Module.tscn
Normal file
31
modules/Module.tscn
Normal file
@ -0,0 +1,31 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://b1kpyek60vyof"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_1abiy"]
|
||||
[ext_resource type="PackedScene" uid="uid://bho8x10x4oab7" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_risxe"]
|
||||
|
||||
[node name="Module" type="RigidBody2D"]
|
||||
position = Vector2(-50, 50)
|
||||
mass = null
|
||||
center_of_mass_mode = 1
|
||||
center_of_mass = Vector2(-50, 0)
|
||||
inertia = null
|
||||
linear_velocity = null
|
||||
angular_velocity = null
|
||||
script = ExtResource("1_1abiy")
|
||||
base_mass = null
|
||||
inertia = null
|
||||
|
||||
[node name="StructuralContainer" type="Node2D" parent="."]
|
||||
|
||||
[node name="Hullplate" parent="StructuralContainer" instance=ExtResource("2_risxe")]
|
||||
base_mass = null
|
||||
inertia = null
|
||||
|
||||
[node name="@StaticBody2D@23989" parent="StructuralContainer" instance=ExtResource("2_risxe")]
|
||||
position = Vector2(-100, 0)
|
||||
base_mass = null
|
||||
inertia = null
|
||||
|
||||
[node name="HullVolumeContainer" type="Node2D" parent="."]
|
||||
|
||||
[node name="AtmosphereVisualizer" type="Node2D" parent="."]
|
||||
31
modules/New_module.tscn
Normal file
31
modules/New_module.tscn
Normal file
@ -0,0 +1,31 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://baeikwxkh26fh"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_1rae4"]
|
||||
[ext_resource type="PackedScene" uid="uid://bho8x10x4oab7" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_fbnt1"]
|
||||
|
||||
[node name="Module" type="RigidBody2D"]
|
||||
position = Vector2(-50, 50)
|
||||
mass = null
|
||||
center_of_mass_mode = 1
|
||||
center_of_mass = Vector2(-50, 0)
|
||||
inertia = null
|
||||
linear_velocity = null
|
||||
angular_velocity = null
|
||||
script = ExtResource("1_1rae4")
|
||||
base_mass = null
|
||||
inertia = null
|
||||
|
||||
[node name="StructuralContainer" type="Node2D" parent="."]
|
||||
|
||||
[node name="Hullplate" parent="StructuralContainer" instance=ExtResource("2_fbnt1")]
|
||||
base_mass = null
|
||||
inertia = null
|
||||
|
||||
[node name="@StaticBody2D@23989" parent="StructuralContainer" instance=ExtResource("2_fbnt1")]
|
||||
position = Vector2(-100, 0)
|
||||
base_mass = null
|
||||
inertia = null
|
||||
|
||||
[node name="HullVolumeContainer" type="Node2D" parent="."]
|
||||
|
||||
[node name="AtmosphereVisualizer" type="Node2D" parent="."]
|
||||
145
modules/Tube.tscn
Normal file
145
modules/Tube.tscn
Normal file
@ -0,0 +1,145 @@
|
||||
[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="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"
|
||||
|
||||
@ -1,71 +1,205 @@
|
||||
extends CharacterBody2D
|
||||
class_name PilotBall
|
||||
|
||||
# Local movement speed when unattached (e.g., inside a ship or during EVA)
|
||||
const LOCAL_SPEED = 200.0
|
||||
# --- Movement Constants (Friction Simulation) ---
|
||||
# When in open space (no module overlap), movement is zeroed out quickly.
|
||||
const EXTERIOR_DRAG_FACTOR: float = 0.05
|
||||
|
||||
var attached_to_station: Node2D = null
|
||||
var owning_ship: RigidBody2D = null # The ship the character is currently anchored to.
|
||||
# When pushing off hullplates (low friction, slow acceleration)
|
||||
const INTERIOR_SLUGGISH_SPEED: float = 100.0
|
||||
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 = 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,
|
||||
IN_STATION
|
||||
}
|
||||
|
||||
var current_state: MovementState = MovementState.NO_CONTROL
|
||||
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) ---
|
||||
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():
|
||||
# Assume the ship is a parent somewhere up the tree for now.
|
||||
# This should be set upon spawning inside a ship.
|
||||
owning_ship = get_parent().find_parent("Spaceship")
|
||||
# Set up overlap signals if they aren't already connected in the scene file
|
||||
# You must have an Area2D child on PilotBall to detect overlaps.
|
||||
overlap_area.body_entered.connect(on_body_entered)
|
||||
overlap_area.body_exited.connect(on_body_exited)
|
||||
overlap_area.area_entered.connect(_on_station_area_entered)
|
||||
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
|
||||
if body is Ladder:
|
||||
ladder_area = body.find_child("ClimbArea") # Assuming the Ladder has a specific Area2D for climbing
|
||||
|
||||
func on_body_exited(body: Node2D):
|
||||
if body is StructuralPiece:
|
||||
overlapping_modules -= 1
|
||||
|
||||
if body is Ladder:
|
||||
if body.find_child("ClimbArea") == ladder_area:
|
||||
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 exit_station_state():
|
||||
# When leaving, transition to a sensible default state.
|
||||
current_state = MovementState.ZERO_G_INTERIOR
|
||||
|
||||
func _physics_process(delta):
|
||||
# If attached, do not move locally. The station handles movement control (thrusters).
|
||||
if is_attached():
|
||||
velocity = Vector2.ZERO
|
||||
# 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
|
||||
|
||||
|
||||
# Local movement: Use A/D/W/S to move the ball around.
|
||||
var input_direction = Input.get_vector("move_left", "move_right", "move_up", "move_down")
|
||||
velocity = input_direction * LOCAL_SPEED
|
||||
_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.ZERO_G_INTERIOR:
|
||||
_sluggish_movement(input_dir, delta)
|
||||
MovementState.LADDER_GRIP:
|
||||
_ladder_movement(input_dir, delta)
|
||||
|
||||
# Reset input for the next frame
|
||||
_movement_input = Vector2.ZERO
|
||||
|
||||
move_and_slide()
|
||||
|
||||
func is_attached():
|
||||
return attached_to_station != null
|
||||
|
||||
func attach(station: Node2D):
|
||||
if is_attached(): return
|
||||
# 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
|
||||
|
||||
attached_to_station = station
|
||||
# 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
|
||||
|
||||
# 1. Store the global position before changing parent.
|
||||
var old_global_pos = global_position
|
||||
|
||||
# 2. Change the parent to the station/ship (Anchoring)
|
||||
# This makes the ball's movement purely relative to the ship.
|
||||
reparent(station)
|
||||
|
||||
# 3. Reset local position. Its new global position is defined relative to the ship/station.
|
||||
global_position = old_global_pos
|
||||
|
||||
# 4. Notify the station it is now in control
|
||||
station.set_pilot(self)
|
||||
|
||||
func detach():
|
||||
if not is_attached(): return
|
||||
|
||||
var station = attached_to_station
|
||||
attached_to_station = null
|
||||
|
||||
# 1. Notify the station to release control
|
||||
station.set_pilot(null)
|
||||
|
||||
# 2. Store the current global transform (which is relative to the ship)
|
||||
var new_global_pos = global_position
|
||||
|
||||
# 3. Reparent back to the main world (or a dedicated 'space' node)
|
||||
# This is the critical moment: the node re-enters the high-velocity global space.
|
||||
if owning_ship and owning_ship.get_parent():
|
||||
reparent(owning_ship.get_parent())
|
||||
# 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
|
||||
|
||||
# 4. Restore global position and inherit the ship's massive orbital velocity
|
||||
global_position = new_global_pos
|
||||
velocity = owning_ship.linear_velocity
|
||||
else:
|
||||
# Fallback if ship structure is unknown
|
||||
reparent(get_tree().root)
|
||||
global_position = new_global_pos
|
||||
velocity = Vector2.ZERO
|
||||
# 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
|
||||
# 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
|
||||
|
||||
# Priority 2: Interior Zero-G (must overlap a module/piece AND not be grabbing)
|
||||
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
|
||||
current_state = MovementState.ZERO_G_INTERIOR
|
||||
return
|
||||
|
||||
current_state = MovementState.ZERO_G_INTERIOR
|
||||
return
|
||||
|
||||
# Priority 3: No Control (floating free)
|
||||
is_grabbing_ladder = false
|
||||
current_state = MovementState.NO_CONTROL
|
||||
|
||||
|
||||
# --- Movement Implementations ---
|
||||
|
||||
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_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 * 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)
|
||||
|
||||
func _on_station_area_exited(area: Area2D):
|
||||
if area.get_parent() == nearby_station:
|
||||
nearby_station = null
|
||||
|
||||
# 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
|
||||
@ -1,48 +1,114 @@
|
||||
@tool
|
||||
class_name Module
|
||||
extends OrbitalBody2D
|
||||
|
||||
@onready var structural_container: Node2D = $StructuralContainer
|
||||
@onready var hull_volume_container: Node2D = $HullVolumeContainer
|
||||
@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
|
||||
|
||||
# The function name is updated to reflect its new, limited scope.
|
||||
func _recalculate_collision_shape():
|
||||
# This logic should typically be on the main Spaceship node,
|
||||
# but for modularity, the Module can trigger it on its children.
|
||||
|
||||
# 1. Clear any existing combined collision shape on this module.
|
||||
# (You would likely have a central CollisionShape2D node for the combined shape)
|
||||
# var combined_shape_node = find_child("CombinedCollisionShape")
|
||||
# if combined_shape_node:
|
||||
# for child in combined_shape_node.get_children(): child.queue_free()
|
||||
|
||||
|
||||
# 2. Iterate through all StructuralPiece children (which are now OrbitalBody2D)
|
||||
# and gather their global collision transforms/shapes.
|
||||
var combined_polygons = []
|
||||
|
||||
const COMPONENT_GRID_SIZE = 64.0
|
||||
|
||||
# --- NEW: Helper functions to get children by type ---
|
||||
func get_structural_pieces() -> Array[StructuralPiece]:
|
||||
var pieces: Array[StructuralPiece]
|
||||
for child in get_children():
|
||||
# StructuralPiece now inherits OrbitalBody2D, so this check is valid
|
||||
if child is StructuralPiece:
|
||||
# You would use logic here to transform the piece's local shape
|
||||
# into the Module's local space and add it to the list.
|
||||
|
||||
# Example Placeholder (requires full implementation):
|
||||
# var piece_collision_shape = child.find_child("CollisionShape2D")
|
||||
# if piece_collision_shape:
|
||||
# combined_polygons.append(piece_collision_shape.shape.points)
|
||||
pass
|
||||
pieces.append(child)
|
||||
return pieces
|
||||
|
||||
# 3. Create a new shape (e.g., ConcavePolygonShape2D) and assign it.
|
||||
# This part is complex and usually requires an external library or custom code
|
||||
# to merge multiple 2D shapes efficiently.
|
||||
func get_components() -> Array[Component]:
|
||||
var components: Array[Component]
|
||||
for child in get_children():
|
||||
if child is Component:
|
||||
components.append(child)
|
||||
return components
|
||||
|
||||
# --- UPDATED: Logic now uses the helper function ---
|
||||
func get_attachment_points() -> Array:
|
||||
var points = []
|
||||
|
||||
# After implementation, you may want to signal the change:
|
||||
# SignalBus.module_structure_changed.emit(self)
|
||||
# Iterate through all StructuralPiece children directly
|
||||
for piece in get_structural_pieces():
|
||||
var piece_center = piece.global_position
|
||||
|
||||
# --- Hullplates (Interior Grid) ---
|
||||
if piece is Hullplate:
|
||||
for i in range(-1, 2, 2):
|
||||
for j in range(-1, 2, 2):
|
||||
var offset = Vector2(i, j) * (COMPONENT_GRID_SIZE / 2.0)
|
||||
points.append({
|
||||
"position": piece_center + offset,
|
||||
"type": Component.AttachmentType.INTERIOR_WALL,
|
||||
"piece": piece
|
||||
})
|
||||
|
||||
# --- Bulkheads (Interior and Exterior Edge Attachments) ---
|
||||
elif piece is Bulkhead:
|
||||
var interior_point = piece_center + piece.transform.y * (COMPONENT_GRID_SIZE / 2.0)
|
||||
points.append({
|
||||
"position": interior_point,
|
||||
"type": Component.AttachmentType.INTERIOR_WALL,
|
||||
"piece": piece
|
||||
})
|
||||
|
||||
var exterior_point = piece_center - piece.transform.y * (COMPONENT_GRID_SIZE / 2.0)
|
||||
points.append({
|
||||
"position": exterior_point,
|
||||
"type": Component.AttachmentType.EXTERIOR_HULL,
|
||||
"piece": piece
|
||||
})
|
||||
|
||||
# NOTE: The OrbitalBody2D's _update_mass_and_inertia() takes care of mass and center of mass!
|
||||
return points
|
||||
|
||||
# --- This function remains largely the same ---
|
||||
func attach_component(component: Component, global_pos: Vector2, parent_piece: StructuralPiece):
|
||||
component.position = global_pos - global_position
|
||||
component.attached_piece = parent_piece
|
||||
add_child(component)
|
||||
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 = []
|
||||
|
||||
for piece in get_structural_pieces():
|
||||
# You would use logic here to transform the piece's local shape
|
||||
# into the Module's local space and add it to the list.
|
||||
# Example Placeholder (requires full implementation):
|
||||
# var piece_collision_shape = piece.find_child("CollisionShape2D")
|
||||
# if piece_collision_shape:
|
||||
# combined_polygons.append(piece_collision_shape.shape.points)
|
||||
pass
|
||||
|
||||
# NOTE: The OrbitalBody2D's _update_mass_and_inertia() takes care of mass!
|
||||
pass
|
||||
|
||||
# --- UPDATED: Clear module now iterates over all relevant children ---
|
||||
func clear_module():
|
||||
for piece in structural_container.get_children():
|
||||
# We queue_free both structural pieces and components
|
||||
for piece in get_structural_pieces():
|
||||
piece.queue_free()
|
||||
for component in get_components():
|
||||
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
scenes/ship/builder/pieces/bulkhead.gd
Normal file
6
scenes/ship/builder/pieces/bulkhead.gd
Normal file
@ -0,0 +1,6 @@
|
||||
@tool
|
||||
class_name Bulkhead
|
||||
extends StructuralPiece
|
||||
|
||||
# This piece represents a wall or edge.
|
||||
# No additional logic is needed right now, we just need the class_name.
|
||||
1
scenes/ship/builder/pieces/bulkhead.gd.uid
Normal file
1
scenes/ship/builder/pieces/bulkhead.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://b4g288mje38nj
|
||||
@ -1,12 +1,13 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://d3hitk62fice4"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b7f8x2qimvn37" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_1wp2n"]
|
||||
[ext_resource type="Script" uid="uid://b4g288mje38nj" path="res://scenes/ship/builder/pieces/bulkhead.gd" id="1_1wp2n"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_1wp2n"]
|
||||
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"
|
||||
|
||||
|
||||
6
scenes/ship/builder/pieces/hullplate.gd
Normal file
6
scenes/ship/builder/pieces/hullplate.gd
Normal file
@ -0,0 +1,6 @@
|
||||
@tool
|
||||
class_name Hullplate
|
||||
extends StructuralPiece
|
||||
|
||||
# This piece represents an interior surface.
|
||||
# No additional logic is needed right now, we just need the class_name.
|
||||
1
scenes/ship/builder/pieces/hullplate.gd.uid
Normal file
1
scenes/ship/builder/pieces/hullplate.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://crmwm623rh1ps
|
||||
@ -1,6 +1,6 @@
|
||||
[gd_scene load_steps=3 format=3 uid="uid://bho8x10x4oab7"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b7f8x2qimvn37" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_ecow4"]
|
||||
[ext_resource type="Script" uid="uid://crmwm623rh1ps" path="res://scenes/ship/builder/pieces/hullplate.gd" id="1_ecow4"]
|
||||
|
||||
[sub_resource type="RectangleShape2D" id="RectangleShape2D_1wp2n"]
|
||||
size = Vector2(100, 100)
|
||||
|
||||
@ -1,2 +1,35 @@
|
||||
class_name ShipComponent
|
||||
extends OrbitalBody2D
|
||||
class_name Component
|
||||
|
||||
# Defines the size of the component in terms of the grid (e.g., 1x1, 1x2, 2x2)
|
||||
@export_range(1, 4, 1, "suffix:x") var grid_size_x: int = 1
|
||||
@export_range(1, 4, 1, "suffix:x") var grid_size_y: int = 1
|
||||
|
||||
# Specifies the type of structural piece surface this component can attach to.
|
||||
# Used by the editor to validate placement.
|
||||
enum AttachmentType { INTERIOR_WALL, EXTERIOR_HULL, FLOOR_OR_CEILING }
|
||||
@export var attachment_type: AttachmentType = AttachmentType.INTERIOR_WALL
|
||||
|
||||
# Reference to the StructuralPiece this component is physically attached to
|
||||
var attached_piece: StructuralPiece = null
|
||||
|
||||
func _ready():
|
||||
super()
|
||||
# OrbitalBody2D will handle mass initialization and physics setup.
|
||||
pass
|
||||
|
||||
# Components can implement activation logic here (e.g., Thruster fires, Life Support starts)
|
||||
func activate():
|
||||
pass
|
||||
|
||||
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)
|
||||
33
scenes/ship/components/hardware/ladder.gd
Normal file
33
scenes/ship/components/hardware/ladder.gd
Normal file
@ -0,0 +1,33 @@
|
||||
extends Component
|
||||
class_name Ladder
|
||||
|
||||
# --- Component Properties ---
|
||||
# A standard ladder is typically one grid unit wide.
|
||||
# We make the height variable to allow for multi-story climbing in one piece.
|
||||
@export var ladder_grid_height: int = 4 # Height in grid units (e.g., 4x64px)
|
||||
|
||||
# --- 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
|
||||
|
||||
# Set inherited Component properties
|
||||
grid_size_x = 1
|
||||
grid_size_y = ladder_grid_height
|
||||
attachment_type = AttachmentType.INTERIOR_WALL
|
||||
|
||||
# Call the parent's _ready to ensure mass calculation is triggered
|
||||
super._ready()
|
||||
|
||||
# You would add logic here to dynamically resize the ladder's visual and collision
|
||||
# shape to match 'ladder_grid_height' if necessary.
|
||||
|
||||
if Engine.is_editor_hint():
|
||||
# Hide mass from the inspector if the base class doesn't need to see it
|
||||
# You can also set a custom icon for the editor here.
|
||||
pass
|
||||
|
||||
# The player character's script will be responsible for checking if it overlaps
|
||||
# this component and entering a 'climbing' state, using the ladder's position and height.
|
||||
1
scenes/ship/components/hardware/ladder.gd.uid
Normal file
1
scenes/ship/components/hardware/ladder.gd.uid
Normal file
@ -0,0 +1 @@
|
||||
uid://bh1t0cqdjm5ye
|
||||
13
scenes/ship/components/hardware/ladder.tscn
Normal file
13
scenes/ship/components/hardware/ladder.tscn
Normal file
@ -0,0 +1,13 @@
|
||||
[gd_scene load_steps=2 format=3 uid="uid://dxtxb2p7lpt51"]
|
||||
|
||||
[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")
|
||||
metadata/_custom_type_script = "uid://bh1t0cqdjm5ye"
|
||||
|
||||
[node name="ClimbArea" type="Area2D" parent="."]
|
||||
|
||||
[node name="CollisionShape2D" type="CollisionShape2D" parent="ClimbArea"]
|
||||
|
||||
[node name="Sprite2D" type="Sprite2D" parent="."]
|
||||
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"
|
||||
@ -1,13 +1,10 @@
|
||||
# Thruster.gd
|
||||
class_name Thruster
|
||||
extends ShipComponent
|
||||
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
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user