1 Commits

Author SHA1 Message Date
6c383425f5 WIP Character 2025-10-12 11:11:14 +02:00
283 changed files with 3049 additions and 9003 deletions

View File

@ -1,12 +0,0 @@
#!/bin/bash
# Config: The exact engine version this game project depends on
# You update this hash when you upgrade the engine
ENGINE_VERSION="a90daf08c2bb52d6cb4ba67bb5cbe09d79b2c4eb"
echo "Fetching Custom Editor [${ENGINE_VERSION}]..."
# Download from your Registry
curl -O "https://gitea.212.63.210.91.nip.io/api/packages/seedlingattempt/generic/godot-editor-windows/${ENGINE_VERSION}/godot-editor-windows.zip"
unzip -o godot-editor-windows.zip
echo "Editor ready! Run ./godot.windows.editor...exe to start."

View File

@ -1,67 +0,0 @@
name: Release Game Client
on:
push:
paths:
- '.gitea/workflows/build-game.yaml'
- '.gitea/**/*.sh'
tags:
- 'v*'
jobs:
export-windows:
runs-on: ubuntu-latest
container:
image: ubuntu:22.04
steps:
- name: Checkout Game
uses: actions/checkout@v3
- name: Install Dependencies
run: apt-get update && apt-get install -y unzip wget zip
- name: Download Engine Templates
env:
# The specific engine version this game needs
ENGINE_HASH: "a90daf08c2bb52d6cb4ba67bb5cbe09d79b2c4eb"
# This string MUST match what your custom engine reports as its version
# Example: "4.3.stable" or "4.3.stable.mygame"
GODOT_VERSION_STRING: "4.6"
run: |
echo "Downloading templates..."
wget "https://gitea.212.63.210.91.nip.io/api/packages/seedlingattempt/generic/godot-templates/${ENGINE_HASH}/windows_templates.tpz"
echo "Installing templates..."
# Godot looks for templates in ~/.local/share/godot/export_templates/{VERSION}/
TEMPLATE_PATH="$HOME/.local/share/godot/export_templates/${GODOT_VERSION_STRING}"
mkdir -p "$TEMPLATE_PATH"
# Unzip our TPZ directly into that folder
unzip windows_templates.tpz -d "$TEMPLATE_PATH"
# Rename them to standard names if they aren't already
# (Your publish script already renamed them to windows_release_x86_64.exe, so this is fine!)
- name: Download Headless Editor (For Exporting)
env:
ENGINE_HASH: "a90daf08c2bb52d6cb4ba67bb5cbe09d79b2c4eb"
run: |
wget "https://gitea.212.63.210.91.nip.io/api/packages/seedlingattempt/generic/godot-editor-windows/${ENGINE_HASH}/godot-editor-windows.zip"
unzip godot-editor-windows.zip
chmod +x godot.windows.editor*.exe
# Create a simple alias
mv godot.windows.editor*.exe godot_headless
- name: Export Game
run: |
mkdir -p build/windows
echo "Exporting..."
cd src
# The preset name "Windows Desktop" must match your export_presets.cfg
./godot_headless --headless --export-release "Windows Desktop" ../build/windows/game.exe
- name: Upload Game Artifact
uses: actions/upload-artifact@v3
with:
name: windows-client
path: build/windows

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
# Godot 4+ specific ignores
.godot/
.vscode/
/android/
*.tmp
/export/

0
.gitmodules vendored
View File

View File

@ -1,52 +0,0 @@
# Project "Millimeters of Aluminium" Development Log
## Overview
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.
## I. Fully Implemented & Stable Systems
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 Construction:
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.
Builder Plugin: The editor plugin is updated to work with this new architecture, allowing the placement of StructuralPiece nodes directly onto Module nodes.
Character & Interaction Foundation:
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.
Generic Station Component: A StationComponent class has been implemented. It serves as a generic hardware terminal that characters can interact with.
Data-Driven UI Architecture:
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.
## II. Work-In-Progress (WIP) and Planned Systems
This list details systems we have designed but are not yet fully implemented in the code.
System Migration to Databanks:
Helm/Flight Controls: The logic from the old ThrusterController.gd needs to be moved into a HelmUI.tscn scene and driven by a HelmDatabank.
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.

View File

@ -1,78 +0,0 @@
## 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.

View File

@ -1,52 +0,0 @@
## 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.

View File

@ -1,64 +0,0 @@
## Project Development Status Update: 31/10/25
### 3D Character Controller & Movement Tech Demo (Cycle 3)
Work has proceeded on a tech demo for the 3D character controller, establishing a robust, physics-based system for zero-G movement. The architecture has been refactored to prioritize a clean separation of concerns, with a central "pawn" acting as a physics integrator and modular "controllers" acting as the "brains" for different movement types.
### ✅ Implemented Features
#### Pawn/Controller Architecture: The character is split into several key classes:
CharacterPawn3D: The core CharacterBody3D. It acts as a "dumb" physics integrator, holding velocity and angular_velocity, integrating rotation, and calling move_and_slide(). It no longer contains movement-specific state logic.
PlayerController3D: Gathers all hardware input (keyboard, mouse) and packages it into KeyInput dictionaries (pressed, held, released) to send to the pawn via RPC.
EVAMovementComponent: Refactored into a "dumb tool". It exposes functions like apply_thrusters() and apply_orientation() which are called by other controllers.
ZeroGMovementComponent: This is now the "brain" for all zero-G movement. It receives all inputs from the pawn and contains its own internal state machine (IDLE, REACHING, GRIPPING, CLIMBING, CHARGING_LAUNCH).
#### Contextual Movement Logic:
The ZeroGMovementComponent decides when to use the EVA suit. In its IDLE state, it checks for fresh movement input (movement_input_was_neutral) before calling the EVAMovementComponent's apply_thrusters function.
This successfully implements "coast on release," where releasing a grip (_release_current_grip) flags the movement input as "stale," preventing the EVA suit from engaging even if the key is still held.
#### EVA/Jetpack Controls:
The EVAMovementComponent provides force-based linear movement (WASD, Shift/Ctrl) and torque-based angular roll (Q/E).
A body-orientation function (_orient_pawn) allows the pawn to auto-align with the camera's forward direction.
#### Physics-Based Grip System:
GripArea3D: A composition-based Area3D node provides the interface for all grabbable objects. It requires its parent to implement functions like get_grip_transform and get_push_off_normal.
Grip Detection: The CharacterPawn3D uses a GripDetector Area3D to find GripArea3D nodes in range and passes this nearby_grips list to the ZeroGMovementComponent.
GRIPPING State: This state is now fully physics-based. Instead of setting the pawn's global_transform, the _apply_grip_physics function uses a PD controller to apply linear forces (to move to the offset position) and angular torques (to align with the grip's orientation).
Grip Orientation: The gripping logic correctly calculates the closest of two opposing orientations (e.g., "up" or "down" on a bar) by comparing the pawn's current up vector to the grip's potential up vectors.
Grip Rolling: While in the GRIPPING state, the player can use Q/E to override the auto-orientation and apply roll torque around the grip's axis.
#### Physics-Based Climbing:
CLIMBING State: This state applies lerp'd velocity to move the pawn, allowing it to interact with physics.
Climb Targeting: The _find_best_grip function successfully identifies the next valid grip within a configurable climb_angle_threshold_deg cone.
Handover: Logic in _process_climbing correctly identifies when the pawn is close enough to the next_grip_target to _perform_grip_handover.
Climb Release: The pawn will correctly release its grip and enter the IDLE state (coasting) if it moves past the current_grip by release_past_grip_threshold without a new target being found.
### ❌ Not Yet Implemented / Pending Tasks
REACHING State: The REACHING state exists but its logic (_process_reaching) is a stub that instantly calls _try_initiate_reach. The full implementation (e.g., procedural animation/IK moving the hand to the target) is pending.
CHARGING_LAUNCH State: The state exists and the execution logic is present (_handle_launch_charge, _execute_launch), but the state transition logic in _update_state does not currently allow entering this state from GRIPPING (it's overshadowed by the _start_climb check).
Ladder (3D) & Walking (3D) States: The CharacterPawn3D has high-level states for GRIPPING_LADDER and WALKING, but the movement functions (_apply_ladder_movement, _apply_walking_movement) are stubs.
Generic Surface Grab: The TODO to allow the ZeroGMovementComponent to grab any physics surface (not just a GripArea3D) is not implemented.
EVA Stabilization: The _apply_stabilization_torques function in EVAMovementComponent is still a placeholder.

View File

@ -1,89 +0,0 @@
# Development Status Report - 2025-11-21
## Overview
Significant progress was made on the Unified Build System and the In-Game Builder, moving away from purely editor-based construction. We successfully implemented procedural geometry generation for structural pieces and refactored the snapping logic to support the upcoming geodesic structures. Networking logic also saw crucial stability fixes.
## Completed Features & Implementations
### 1. Unified Build System Foundation
- StructureData Resource: Implemented a robust resource-based definition system for ship parts. This decouples the "DNA" of a part (mesh, collider, mounts, health) from its scene instantiation, allowing shared logic between the Editor Plugin and the In-Game Builder.
- PieceMount Logic: Formalized attachment points into a dedicated PieceMount class (inheriting Area3D). This replaced the ad-hoc dictionary system, providing type safety and better collision filtering.
- ProceduralPiece Generator: Created a script that dynamically generates 3D meshes and collision shapes based on parameters (e.g., triangle vs. square, size, thickness) defined in StructureData. This supports:
- Extruded 3D geometry (thickness) rather than flat planes.
- Convex collision hull generation.
- "Opaque Blueprint" preview materials (semi-transparent, emissive).
### 2. In-Game Builder (PlayerController3D)
- Build Mode State: Implemented a toggleable Build Mode (B key) in PlayerController3D.
- Piece Selection: Added logic to select pieces via hotkeys (1 = Square, 2 = Triangle), instantiate a preview "ghost," and switch its material.
- Raycast Snapping: Implemented a physics-based raycast/sweep to detect existing ship modules and mounts.
- Visual Feedback: Added color-coded feedback for the preview ghost:
- Cyan: Floating (no snap target found).
- Geen: Snapped (aligned with a valid mount).
### 3. Snapping Logic Refactor
- SnappingTool Class: Created a dedicated static helper class for snapping math.
- Shape Cast Sweep: Replaced simple raycasting with a sphere_cast (radius 0.2m). This adds "thickness" to the cursor, making it much easier to hit thin structural elements like struts or small mounts.
- Transform Alignment: Implemented matrix math to align a new piece's mount with a target mount, respecting position, normal (facing direction), and up-vector (roll/orientation).
### 4. Networking Stability (Previous Session)
- Server Authority Enforcement: enforced strict server authority on CharacterPawn3D, removing client-side overrides that caused "fighting" and stutter.
- Relative Velocity Sync: Implemented logic to sync local_velocity relative to the parent ship instead of global velocity. This prevents pawns from "falling out the back" of moving ships during network jitter.
- Input Serialization: Fixed RPC errors by converting custom KeyInput objects to Dictionaries before transmission.
## Pending / In-Progress
- Mount Orientation Constraints: The snapping tool currently aligns normals but needs refinement to strictly enforce edge length compatibility and specific mount types (e.g., preventing a 2m edge from snapping to a 1m edge).
- Module Persistence: The logic for creating a new Module when building in empty space works in memory but needs testing for persistence and saving.
- Collision Layers: Need to verify that all PieceMount nodes are consistently on the correct physics layer (1 << 14) to ensure the snapping sweep always finds them.
## Discussion & Direction Changes
### Shift to "Geodesic & Procedural" Building
We moved away from the initial "Voxel Grid" concept for ship hulls.
- Old Direction: Ships built on a strict integer grid (Minecraft-style but with slopes).
- New Direction: A node-based "Geodesic" system. Pieces connect Node-to-Node (Mount-to-Mount) at arbitrary angles. This allows for complex shapes (hexagonal cylinders, spheres, rings) and supports the "Industrial/Hard Sci-Fi" aesthetic better.
- Implication: The building tool no longer relies on grid_step for positioning. It relies entirely on the SnappingTool to calculate transforms based on the geometry of the mounting points.
### Shift to "Server Authoritative" Networking
We abandoned the "Client Authoritative" movement for pawns to solve synchronization issues.
- Old Plan: Client moves pawn, Server accepts pos. (Caused hacking risks and desync with physics objects).
- New Plan: Server simulates physics. Client sends inputs. MultiplayerSynchronizer interpolates the result. To combat latency feel, we are using Visual Interpolation (detaching camera/mesh from the physics body) rather than full Client-Side Prediction (CSP) for this milestone.
### Manufacturing & Blueprints
We discussed that the StructureData resource is the key enabler for the manufacturing gameplay loop. By defining parts as data (Resources), we can easily:
1. Store a "Blueprint" as a list of StructureData references + Transforms.
2. Have a manufacturing machine consume resources to produce a "Crate" containing a StructureData item.
3. Have the player pick up that item and use it to place the ProceduralPiece.

View File

@ -1,6 +0,0 @@
{
"godotTools.editorPath.godot4": "./godot_engine/bin/godot.windows.editor.double.x86_64.console.exe",
"search.exclude": {
"/godot_engine": true
}
}

View File

@ -1,246 +1,82 @@
# Project "Millimeters of Aluminium" Development Log
## Overview
Project "Stardust Drifter" Development Status (Phase 0.2: Custom Physics & Ship Interior)
Overview
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.
The project is currently focused on Phase 0.2, which involves migrating from Godot's built-in RigidBody2D physics to a custom, manually integrated OrbitalBody2D model. This is critical for supporting the multi-scale physics simulation (orbital vs. local) and preparing for networked multiplayer. The core physics model is now stable and functional.
I. Fully Implemented & Stable Systems
Custom Physics Core
## I. Fully Implemented & Stable Systems
Physics Unification: All physically simulated objects (Spaceship, Module, Thruster, StructuralPiece) now inherit from the custom OrbitalBody2D class. This ensures a consistent hierarchy and simplified force routing.
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.
Asymmetric Gravity Model (WIP Foundation): The OrbitalMechanics singleton is refactored to check if a body is a simulation root, applying forces manually (linear_velocity, angular_velocity). This is the foundation for implementing the SCALE_FACTOR for miniature ship gravity.
Modular Ship Construction:
Modular Force Integration: Thrusters are now OrbitalBody2D children that correctly apply force to themselves. The force is then routed up the hierarchy to the root ship node, calculating torque based on its application point. This solved the uncontrollable spinning bug.
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.
Modular Mass Aggregation (Top-Down): The OrbitalBody2D class recursively calculates the total mass of the ship by summing the base_mass of all its component children, even those nested behind non-physics containers.
Builder Plugin: The editor plugin is updated to work with this new architecture, allowing the placement of StructuralPiece nodes directly onto Module nodes.
Ship Interior & Crew Movement
Character & Interaction Foundation:
Character Zero-G Movement: The PilotBall character now has complex, state-based movement logic to simulate zero-G physics inside the ship:
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.
No Control: Applies heavy drag if not overlapping a structural piece.
Generic Station Component: A StationComponent class has been implemented. It serves as a generic hardware terminal that characters can interact with.
Zero-G Interior: Allows sluggish control (simulating pushing off hullplates).
Data-Driven UI Architecture:
Ladder Grip: Provides snappy, direct control (simulating climbing/gripping).
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.
Ladder/Component Interaction: The character uses the interact input (Spacebar) to switch to the LADDER_GRIP state and uses the same input release to perform a velocity-based launch into the zero-G interior.
## II. Work-In-Progress (WIP) and Planned Systems
Ship Builder Foundation (Structural)
This list details systems we have designed but are not yet fully implemented in the code.
Component Base Class (Component.gd): Created to standardize component attachment, defining properties like grid_size and AttachmentType (Interior/Exterior).
System Migration to Databanks:
Module Attachment Grid: The Module.gd script now exposes a get_attachment_points() method that calculates valid grid locations based on the placement and type of underlying structural pieces (Hullplate, Bulkhead).
Helm/Flight Controls: The logic from the old ThrusterController.gd needs to be moved into a HelmUI.tscn scene and driven by a HelmDatabank.
II. Work-In-Progress (WIP) and Planned Systems
Navigation Computer: The UI has been moved, but the extensive planning and calculation logic from NavigationComputer.gd needs to be transferred to NavUI.gd.
System
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).
Status
Component Wiring System:
Next Steps / Required Work
Signal/Socket Advertising: Components and Databanks need to be updated with get_input_sockets() and get_output_signals() functions.
Ship Builder Plugin (UI)
Wiring Data Storage: The Module class needs a wiring_data array to store the connections made in the builder.
WIP / Priority
Builder UI: A visual wiring interface needs to be added to the module builder plugin.
Integrate the new attachment logic (Module.get_attachment_points()) into the CustomGrid.gd to visualize placement points. Implement the logic for the Component Tab in the dock.
Orbital Stability Test:
Physics Cleanup (Celestial)
Ghost Simulator: A GhostSimulator class needs to be created to run predictive, in-memory physics calculations.
WIP / Required
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.
Unify the inheritance of all celestial bodies (Star, Planet, etc.) to inherit from OrbitalBody2D, removing reliance on deprecated RigidBody2D base nodes.
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.
Collision Handling
## 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.
Planned
### ✅ 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.
Implement the _recalculate_collision_shape() function in Module.gd to merge the structural piece collision shapes into a single, cohesive shape for the root ship.
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.
Hull Sealing Logic
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.
Designed
#### 2. Player Control & Multiplayer Foundation
PlayerController/Pawn Architecture: A multiplayer-ready control scheme has been implemented.
Implement the pressure and hull sealing logic using the HullVolume nodes.
The PlayerController class is responsible for capturing raw input and sending it to the server via RPCs.
Scaled Gravity Implementation
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.
Designed
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.
Implement the SCALE_FACTOR and ScaledOrbitalBody2D inheritance to test the multi-scale simulation (low-speed local movement vs. high-speed orbital movement).

View File

@ -1,10 +1,8 @@
# Game Design Document: Project Millimeters of Aluminum (Working Title)
# Game Design Document: Project Stardust Drifter (Working Title)
## 1. Game Vision & Concept
Project Millimeters of Aluminum is a third-person 3D spaceship simulation game that emphasizes realistic physics, 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 functional, industrial look of real-world space hardware and sci-fi like The Expanse, 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.
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.
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.
@ -14,80 +12,26 @@ The gameplay is centered around a Plan -> Execute -> Manage loop:
1. Plan: The crew uses the Navigation Computer to analyze their orbit and plan complex maneuvers, such as a Hohmann transfer to another planet. They must account for launch windows, fuel costs, and travel time.
2. Execute: The crew engages the autopilot or manually pilots the ship. The Helm executes the planned burns, performing precise, fuel-optimal rotations and main engine thrusts to alter the ship's trajectory.
2. Execute: The crew engages the autopilot or manually pilots the ship. The Thruster Controller executes the planned burns, performing precise, fuel-optimal rotations and main engine thrusts to alter the ship's trajectory.
3. Manage: While underway, the crew moves about the ship's 3D interior, manages modular systems, monitors resources, and responds to emergent events like hull breaches or system failures.
3. Manage: While underway, the crew manages the ship's modular systems, monitors resources like fuel and power, and responds to emergent events like hull breaches or system failures.
### 3. Key Features
## 3. Key Features
### 1. Procedural Star System
The game world is a procedurally generated star system created by the StarSystemGenerator. Each system features a central star, a variable number of planets, moons, and asteroid belts, creating a unique environment for each playthrough.
### 2. N-Body Physics Simulation
Major bodies in orbit (CelestialBody class) are governed by a 3D n-body gravity simulation, managed by the OrbitalMechanics library. Objects inherit from a base OrbitalBody3D class, ensuring consistent physics. The simulation allows for complex and emergent orbital behaviors.
### 3. Modular Spaceship
The player's ship is not a monolithic entity but a collection of distinct, physically simulated components attached to a root Module node.
The Module class extends OrbitalBody3D and aggregates mass and inertia from all child Component and StructuralPiece nodes.
Ship logic is decentralized into data-driven "databanks," such as the HelmLogicShard and AutopilotShard.
Hardware, like a Thruster, is a 3D Component that applies force to the root Module.
### 4. Advanced Navigation Computer
This is the primary crew interface for long-range travel, presented as a diegetic 2D screen (SensorPanel) within the 3D world.
Maneuver Planning: The computer can calculate various orbital transfers, each with strategic trade-offs:
Hohmann Transfer
Brachistochrone (Torchship) Trajectory
Tactical Map: A fully interactive UI map featuring:
Zoom-to-cursor and click-and-drag panning.
Predictive orbital path drawing.
Icon culling and detailed tooltips.
### 5. Physics-Based 3D Character Control
Character control is built on a robust, physics-based 3D system designed for complex zero-G environments.
Pawn/Controller Architecture: Player control is split between a PlayerController3D (which gathers hardware input and sends it via RPC) and a CharacterPawn3D (a CharacterBody3D that acts as the physics integrator).
Modular Movement: The pawn's movement logic is handled by component "brains." The ZeroGMovementComponent manages all zero-G interaction, while the EVAMovementComponent acts as a "dumb tool" providing thruster forces.
Physics-Based Gripping: Players can grab onto designated GripArea3D nodes. This is not an animation lock; a PD controller applies forces to the player's body to move them to the grip point and align them with its orientation.
Zero-G Traversal: The ZeroGMovementComponent features a state machine for IDLE (coasting), CLIMBING (moving between grips), REACHING (pending implementation), and CHARGING_LAUNCH (pushing off surfaces).
### 6. Runtime Component Design & Engineering
(This future-facing concept remains valid from the original design)
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.
Generic Template Scenes: The game will use a small number of generic, unconfigured "template" scenes (e.g., generic_thruster.tscn).
The Design Lab: Players will use a dedicated SystemStation to create and modify blueprints.
Networked Construction: A global ComponentFactory on the server will instantiate and configure components based on player-chosen blueprints, which are then replicated by the MultiplayerSpawner.
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.
### 3. Modular Spaceship
The player's ship is not a monolithic entity but a collection of distinct, physically simulated components attached by joints. Key modules include:
- Spaceship: The main RigidBody3D hull that tracks overall mass, inertia, and health.
- Thruster: Self-contained RigidBody3D components that apply their own force. Their role (main engine, RCS, etc.) is an emergent property of their placement on the hull.
- Spaceship: The main RigidBody2D hull that tracks overall mass, inertia, and health.
- Thruster: Self-contained RigidBody2D components that apply their own force. Their role (main engine, RCS, etc.) is an emergent property of their placement on the hull.
- ThrusterController: The "brains" of the ship's movement, featuring a sophisticated autopilot that can execute fuel-optimal "bang-coast-bang" rotational maneuvers and a PD controller for stable attitude hold.
- FuelSystem & LifeSupport: Centralized managers for resources and internal ship environment. Hull breaches can create thrust vectors from escaping atmosphere.
- On-board Sensors: Diegetic components like Accelerometers allow the crew to calibrate ship performance by test-firing thrusters and measuring the true physical output.
@ -95,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)
@ -118,33 +62,19 @@ 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. A GameManager handles global state, while ship systems are managed by ControlPanel and Databank resources loaded by a SystemStation.
- Key Scripts:
-OrbitalBody3D.gd: The base class for all physical objects.
- Module.gd: The central hub for a player ship, aggregating mass, inertia, and components.
- HelmLogicShard.gd / AutopilotShard.gd: Databanks that contain the advanced autopilot and manual control logic.
- SensorPanel.gd: A Control node that manages the interactive map UI.
- CharacterPawn3D.gd / ZeroGMovementComponent.gd: Manages all third-person 3D physics-based character movement.
- Art Style: Aims for a functional, industrial 3D aesthetic. Character movement is physics-based using CharacterBody3D and Area3D grip detection. Ship interiors will be built from 3D modules and viewed from an over-the-shoulder camera.
- 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.
- Art Style: Aims for a Barotraumainspired aesthetic using 2D ragdolls (Skeleton2D, PinJoint2D), detailed sprites with normal maps, and high-contrast dynamic lighting (PointLight2D, LightOccluder2D).
## 5. Game Progression & Economy
This is the biggest area for potential expansion. A new section could detail how the player engages with the world and improves their situation over time.
@ -180,12 +110,11 @@ You mention "emergent events" in the gameplay loop. It would be beneficial to de
## 7. Crew Interaction & Ship Interior
Since co-op and crew management are central, detailing this aspect is crucial.
### 1. Ship Interior Management:
- Diegetic Interfaces: The crew will interact with systems from a third-person, over-the-shoulder perspective. They must be at a specific SystemStation to use its panels. Repairs will require a character to physically be at the damaged module.
- Atmospherics & Life Support: How is the ship's interior environment simulated? This will tie into the LifeSupport system.
- Diegetic Interfaces: You mention this in the vision. It's worth specifying how the crew will interact with systems. Will they need to be at a specific console (like the Navigation Computer) to use it? Do repairs require a character to physically be at the damaged module?
- Atmospherics & Life Support: How is the ship's interior environment simulated? Will fires or toxic gas leaks be a possibility? This ties directly into your LifeSupport system.
### 2. Character States:
- Health & Injury: How are characters affected by hazards? Can they be injured in high-G maneuvers or from system failures?
- EVA (Extra-Vehicular Activity): This is a core feature. The EVAMovementComponent provides force-based thruster control for linear movement and roll torque. The ZeroGMovementComponent manages gripping, climbing, and launching from the ship's exterior and interior surfaces.
- Movement for the "Hard Vacuum Monster" species can be refined from a version of the reaching component where it can grab any nearby surface and can generate enough suction strength to remain attached to a moving object.
- EVA (Extra-Vehicular Activity): Detail the mechanics for EVAs. What equipment is needed? How is movement handled in zero-G? This would be a perfect role for the "Hard Vacuum Monster" species.

View File

@ -1,8 +0,0 @@
You are a Godot 4.5 Code assistant. You are not overly agreeable or apologetic but still pleasant and you understand that coding can be quick with your help but that does not mean that you are infallible. Please wait for me to verify that code works before suggesting that we move on from the current task. Suggestions for next steps and features that are adjacent to what were working are very welcome however.
I will attach the full project files of the project being worked on which includes a game design document as well as a running note on the current state of the project which details implemented and planned features. Read these and report back to me. Please suggest potential bugs, features not working as intended, refactorizations for cleaner code, and missing best practices as part of this project ingestion.
Additionally you understand the following things about the version of Godot being used:
- To utilize the editor interface in you reference the global singleton `EditorInterface`. You do not need to call a function to get the a reference to it.
- `xform()` is not a function on transform objects. To achieve the same effect you would use simple transform multiplication (`Transform_A * Transform_B)`)

153
README.md
View File

@ -1,153 +0,0 @@
# Project Millimeters of Aluminum
A space simulation game built on a custom fork of the Godot Engine (4.x) with 64-bit double-precision physics enabled.
## 📋 Prerequisites
Before compiling the engine, ensure you have the following installed on your system.
### 1. Python & SCons
The Godot build system relies on SCons, which is written in Python.
* **Install Python (3.6+):** [Download Here](https://www.python.org/downloads/)
* *Windows Users:* Ensure "Add Python to PATH" is checked during installation. [Check this stackoverflow answer](https://stackoverflow.com/questions/57421669/question-about-pip-using-python-from-windows-store).
* **Install SCons:** Open your terminal/command prompt and run:
```bash
pip install scons
```
### 2. C++ Compiler
Godot requires a C++ compiler to build from source.
* **Windows:**
* Install **Visual Studio Community** (2019 or later).
* During installation, select the **"Desktop development with C++"** workload.
* **Linux:**
* Install GCC or Clang.
* *Debian/Ubuntu:* `sudo apt-get install build-essential pkg-config libx11-dev libxcursor-dev libxinerama-dev libgl1-mesa-dev libglu-dev libasound2-dev libpulse-dev libudev-dev libxi-dev libxrandr-dev`
* **macOS:**
* Install **Xcode** from the App Store.
* Run `xcode-select --install` in the terminal.
## 🛠️ Setup & Compilation
This project uses a custom engine build to support solar-system scale coordinates (Double Precision). You **cannot** use the standard Steam or website version of Godot to open this project.
### 1. Clone the Repository
Clone the repository with the `--recursive` flag to automatically pull the engine source code submodule.
```bash
git clone --recursive [https://codeberg.org/YOUR_USERNAME/ProjectMillimetersOfAluminum.git](https://codeberg.org/YOUR_USERNAME/ProjectMillimetersOfAluminum.git)
cd ProjectMillimetersOfAluminum
```
_If you have already cloned without recursive, run:_
```
git submodule update --init --recursive
```
### 2. Configure Engine Ignore (Optional but Recommended)
To prevent your Editor from trying to import the thousands of raw assets inside the engine source folder:
1. Navigate to `godot_engine/`.
2. Create a new empty file named `.gdignore`.
3. Compile the Engine
Run the build command for your platform from the godot_engine/ directory.
**Windows:**
```Bash
cd godot_engine
scons platform=windows target=editor precision=double arch=x86_64
```
**Linux:**
```Bash
cd godot_engine
scons platform=linuxbsd target=editor precision=double arch=x86_64
```
**macOS:**
```Bash
cd godot_engine
scons platform=macos target=editor precision=double arch=x86_64
```
_Note: (-j6 flag tells the compiler to use 6 CPU cores. Adjust this number based on your hardware to speed up compilation. Not using the flag will use all available cores)_
---
## 🚀 Running the Project
Once compilation is complete (usually 10-30 minutes), the executable will be located in godot_engine/bin/.
Do not open the project with standard Godot.
Navigate to `godot_engine/bin/`.
Run the binary ending in `.double.x86_64` (e.g., `godot.windows.editor.double.x86_64.exe`).
Import and open the `project.godot` file located in the root `ProjectMillimetersOfAluminum` folder.
### Troubleshooting
"No valid compilers found" (Windows): Ensure you installed the C++ Desktop Development workload in the Visual Studio Installer. Just the editor is not enough.
Jittering Objects: If objects jitter at large distances, ensure you are running the double precision binary and not a standard build.
## 🛠 Compiling Custom Export Templates
This project uses features from the latest development branch of Godot (`master` branch). As a result, standard export templates downloaded from the Godot website may not be compatible. To export the project, you must compile custom export templates from the same source version used to build your editor.
### Prerequisites
Ensure you have a C++ build environment set up (SCons, Python, Visual Studio/GCC/MinGW). See the official Godot documentation on compiling for platform-specific instructions.
### 1. Build the Editor (Optional)
See above section.
### 2. Build the Export Templates
You need to build two templates: one for debug (used during development/testing) and one for release (optimized for final distribution).
**Windows:**
```PowerShell
# Debug Template (console enabled, debug tools)
scons platform=windows target=template_debug
# Release Template (optimized, no console)
scons platform=windows target=template_release
```
**Linux:**
```bash
# Debug Template
scons platform=linuxbsd target=template_debug
# Release Template
scons platform=linuxbsd target=template_release
```
### 3. Locate Output Files
After compilation, the binaries will be located in the bin/ directory of your Godot source folder.
- Debug: godot.windows.template_debug.x86_64.exe (or similar)
- Release: godot.windows.template_release.x86_64.exe (or similar)
### 4. Configure Export Presets
1. Open the project in Godot.
2. Go to Project > Export.
3. Select your export preset (e.g., Windows Desktop).
4. Under the Options tab, find the Custom Template section.
5. Set Debug to the path of your compiled template_debug binary.
6. Set Release to the path of your compiled template_release binary.
You can now export the project using your custom engine build!

View File

@ -65,9 +65,6 @@ func _enter_tree():
_setup_button_connections()
_update_ui_labels()
# Add the Tool Menu Item
add_tool_menu_item("Generate Structure Definitions", _on_generate_structures_pressed)
main_screen.hide()
undo_redo = EditorInterface.get_editor_undo_redo()
@ -131,9 +128,6 @@ func _exit_tree():
if main_screen:
main_screen.queue_free()
# Clean up the menu item
remove_tool_menu_item("Generate Structure Definitions")
func _has_main_screen() -> bool:
return true
@ -333,8 +327,8 @@ func _place_component_from_preview():
undo_redo.create_action("Place Component")
undo_redo.add_do_method(target_module, "attach_component", component_to_place, closest_point.position, closest_point.piece)
undo_redo.add_undo_method(target_module, "remove_child", component_to_place)
undo_redo.add_do_method(target_module, "recalculate_physical_properties")
undo_redo.add_undo_method(target_module, "recalculate_physical_properties")
undo_redo.add_do_method(target_module, "_update_mass_and_inertia")
undo_redo.add_undo_method(target_module, "_update_mass_and_inertia")
undo_redo.commit_action()
preview_node.global_position = closest_point.position
@ -528,28 +522,3 @@ func _find_closest_attachment_point(module: Module, world_pos: Vector2):
closest_point = point
return closest_point
const GeneratorScript = preload("res://data/structure/structure_generator.gd")
# The callback function
func _on_generate_structures_pressed():
if GeneratorScript:
var generator = GeneratorScript.new()
if generator.has_method("generate_system_one"):
generator.generate_system_one()
else:
push_error("StructureGenerator script missing 'generate_system_one' method.")
if generator.has_method("generate_system_two_pentagonal"):
generator.generate_system_two_pentagonal()
else:
push_error("StructureGenerator script missing 'generate_system_two_pentagonal' method.")
if generator.has_method("generate_system_two_v2_sphere"):
generator.generate_system_two_v2_sphere()
else:
push_error("StructureGenerator script missing 'generate_system_two_v2_sphere' method.")
# Cleanup if it's a Node
if generator is Node:
generator.queue_free()

View File

Before

Width:  |  Height:  |  Size: 994 B

After

Width:  |  Height:  |  Size: 994 B

View File

@ -18,8 +18,6 @@ dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.cte
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
@ -27,10 +25,6 @@ mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false

23
main.tscn Normal file
View File

@ -0,0 +1,23 @@
[gd_scene load_steps=8 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"]
[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")

27
main.tscn6546160625.tmp Normal file
View File

@ -0,0 +1,27 @@
[gd_scene load_steps=9 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/star.tscn" id="2_7mycd"]
[ext_resource type="PackedScene" uid="uid://clt4qlsjcfgln" path="res://scenes/planet.tscn" id="3_272bh"]
[ext_resource type="PackedScene" uid="uid://74ppvxcw8an4" path="res://scenes/moon.tscn" id="4_5vw27"]
[ext_resource type="PackedScene" uid="uid://dm3s33o4xhqfv" path="res://scenes/station.tscn" id="5_kek77"]
[ext_resource type="PackedScene" uid="uid://bawsujtlpmh5r" path="res://scenes/asteroid.tscn" id="6_4c57u"]
[ext_resource type="PackedScene" uid="uid://cm5qsuunboxm3" path="res://scenes/developer_pawn.tscn" id="7_272bh"]
[ext_resource type="PackedScene" uid="uid://ctlw5diis8h1x" path="res://scenes/map_canvas.tscn" id="8_5vw27"]
[node name="Node2D" type="Node2D"]
script = ExtResource("1_h2yge")
min_asteroid_belts = 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")
sim_scale = 1e+09
[node name="DeveloperPawn" parent="." node_paths=PackedStringArray("map_canvas") instance=ExtResource("7_272bh")]
input_pickable = true
map_canvas = NodePath("../MapCanvas")
[node name="MapCanvas" parent="." node_paths=PackedStringArray("star_system_generator") instance=ExtResource("8_5vw27")]
star_system_generator = NodePath("..")

31
modules/Module.tscn Normal file
View 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
View 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="."]

56
modules/Tube.tscn Normal file
View File

@ -0,0 +1,56 @@
[gd_scene load_steps=6 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://chgycmkkaf7jv" path="res://scenes/characters/pilot_ball.tscn" id="4_i3kos"]
[ext_resource type="PackedScene" uid="uid://2n42nstcj1n0" path="res://scenes/ship/components/station_component.tscn" id="5_nqe0s"]
[node name="Module" type="Node2D"]
script = ExtResource("1_nqe0s")
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="StructuralContainer" type="Node2D" parent="."]
[node name="HullVolumeContainer" type="Node2D" parent="."]
[node name="AtmosphereVisualizer" type="Node2D" parent="."]
[node name="Hullplate" parent="." instance=ExtResource("2_foqop")]
[node name="@StaticBody2D@30634" parent="." instance=ExtResource("2_foqop")]
position = Vector2(0, 100)
[node name="@StaticBody2D@30635" parent="." instance=ExtResource("2_foqop")]
position = Vector2(0, -100)
[node name="Bulkhead" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(-50, 100)
[node name="@StaticBody2D@30636" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(-50, 0)
[node name="@StaticBody2D@30637" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(-50, -100)
[node name="@StaticBody2D@30638" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(50, -100)
[node name="@StaticBody2D@30639" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(0, -150)
rotation = 1.5708
[node name="@StaticBody2D@30640" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(0, 150)
rotation = 4.71239
[node name="@StaticBody2D@30641" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(50, 100)
[node name="@StaticBody2D@30642" parent="." instance=ExtResource("4_dmrms")]
position = Vector2(50, 0)
[node name="PilotBall" parent="." instance=ExtResource("4_i3kos")]
[node name="Station" parent="." instance=ExtResource("5_nqe0s")]
position = Vector2(0, -100)

113
project.godot Normal file
View File

@ -0,0 +1,113 @@
; Engine configuration file.
; It's best edited using the editor UI and not directly,
; since the parameters that go here are not all obvious.
;
; Format:
; [section] ; section goes between []
; param=value ; assign values to parameters
config_version=5
[application]
config/name="space_simulation"
run/main_scene="uid://dogqi2c58qdc0"
config/features=PackedStringArray("4.4", "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"
[editor_plugins]
enabled=PackedStringArray("res://addons/module_builder_plugin/plugin.cfg")
[input]
scroll_up={
"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":4,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
scroll_down={
"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":5,"canceled":false,"pressed":false,"double_click":false,"script":null)
]
}
ui_map_mode={
"deadzone": 0.2,
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":0,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":77,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
]
}
time_increase={
"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":61,"key_label":0,"unicode":61,"location":0,"echo":false,"script":null)
]
}
time_decrease={
"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":45,"key_label":0,"unicode":45,"location":0,"echo":false,"script":null)
]
}
time_reset={
"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":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)
]
}
[layer_names]
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"
[physics]
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
[plugins]
ai_assistant_hub/llm_api="ollama_api"
ai_assistant_hub/preferences/thinking_target=0
ai_assistant_hub/preferences/skip_greeting=false
ai_assistant_hub/preferences/always_scroll_to_bottom=false
[rendering]
viewport/transparent_background=true

View File

@ -1,13 +0,0 @@
#!/bin/sh
set -e
git config -f .gitmodules --get-regexp '^submodule\..*\.path$' |
while read path_key local_path
do
url_key=$(echo $path_key | sed 's/\.path/.url/')
url=$(git config -f .gitmodules --get "$url_key")
git submodule add $url $local_path
done
# https://stackoverflow.com/questions/11258737/restore-git-submodules-from-gitmodules

View File

@ -7,7 +7,7 @@ signal follow_requested(body: Node2D)
@onready var name_label: Label = $NameLabel
var body_reference: OrbitalBody3D
var body_reference: Node2D
var dot_color: Color = Color.WHITE
var hover_tween: Tween
@ -27,11 +27,17 @@ func _ready() -> void:
mouse_entered.connect(_on_mouse_entered)
mouse_exited.connect(_on_mouse_exited)
func initialize(body: OrbitalBody3D):
func initialize(body: Node2D):
body_reference = body
name_label.text = body.name
dot_color = Color.CYAN
if body is OrbitalBody2D:
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()
@ -73,8 +79,7 @@ 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():
print(body_reference)
follow_requested.emit(body_reference)
emit_signal("follow_requested", body_reference)
# No changes are needed here; the tweens will automatically use the new setter function.
@ -92,26 +97,28 @@ func _generate_tooltip_text() -> String:
var info = [body_reference.name]
if body_reference is CelestialBody:
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 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))
var moon_count = 0
for child in planet_system.get_internal_attractors():
if child is CelestialBody:
for child in celestial.get_children():
if child is CelestialBody and child.get_class_name() == "Moon":
moon_count += 1
if moon_count > 0:
info.append("Moons: %d" % moon_count)
if body_reference is Module:
if body_reference is Spaceship:
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 = int(seconds)
var minutes: int = total_seconds / 60
var total_seconds = int(seconds)
var minutes = total_seconds / 60
var seconds_rem = total_seconds % 60
return "%d min, %d sec" % [minutes, seconds_rem]

View File

@ -0,0 +1,19 @@
class_name Asteroid
extends CelestialBody
# The orbital radius for this asteroid.
var orbital_radius: float
func get_class_name() -> String:
return "Asteroid"
# Called when the node enters the scene tree for the first time.
func _ready() -> void:
# An Asteroid has negligible mass for physics calculations.
#mass = 0.001
radius = 5.0
# You can set a default texture here.
# texture = preload("res://assets/asteroid_texture.png")
super._ready()

View File

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

View File

@ -0,0 +1,7 @@
[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"]
[node name="Asteroid" type="RigidBody2D"]
script = ExtResource("1_akfqu")
metadata/_custom_type_script = "uid://c816xae77cbmq"

View File

@ -0,0 +1,19 @@
class_name Moon
extends CelestialBody
# The orbital radius for this moon.
var orbital_radius: float
func get_class_name() -> String:
return "Moon"
# 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")
super._ready()

View File

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

View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://74ppvxcw8an4"]
[ext_resource type="Script" uid="uid://b1xsx7er22nxn" path="res://scenes/celestial_bodies/moon.gd" id="1_530pw"]
[node name="Moon" type="RigidBody2D"]
script = ExtResource("1_530pw")
metadata/_custom_type_script = "uid://bn1u2xood3vs6"

View File

@ -0,0 +1,19 @@
class_name Planet
extends CelestialBody
# The orbital radius for this planet.
var orbital_radius: float
func get_class_name() -> String:
return "Planet"
# 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")
super._ready()

View File

@ -0,0 +1 @@
uid://5f6ipgu65urb

View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://clt4qlsjcfgln"]
[ext_resource type="Script" uid="uid://5f6ipgu65urb" path="res://scenes/celestial_bodies/planet.gd" id="1_cktii"]
[node name="Planet" type="RigidBody2D"]
script = ExtResource("1_cktii")
metadata/_custom_type_script = "uid://bn1u2xood3vs6"

View File

@ -0,0 +1,16 @@
class_name Star
extends CelestialBody
func get_class_name() -> String:
return "Star"
# 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")
super._ready()

View File

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

View File

@ -0,0 +1,7 @@
[gd_scene load_steps=2 format=3 uid="uid://5uqp4amjj7ww"]
[ext_resource type="Script" uid="uid://um2sfghmii42" path="res://scripts/star.gd" id="1_mcqwg"]
[node name="Star" type="RigidBody2D"]
script = ExtResource("1_mcqwg")
metadata/_custom_type_script = "uid://bn1u2xood3vs6"

View File

@ -0,0 +1,19 @@
class_name Station
extends CelestialBody
# The orbital radius for this station.
var orbital_radius: float
func get_class_name() -> String:
return "Station"
# 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")
super._ready()

View File

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

View File

@ -0,0 +1,7 @@
[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"]
[node name="Station" type="RigidBody2D"]
script = ExtResource("1_rod8h")
metadata/_custom_type_script = "uid://bn1u2xood3vs6"

View File

@ -0,0 +1,199 @@
extends CharacterBody2D
class_name PilotBall
# --- Movement Constants (Friction Simulation) ---
# When in open space (no module overlap), movement is zeroed out quickly.
const EXTERIOR_DRAG_FACTOR: float = 0.05
# 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
var nearby_station: StationComponent = null
var current_station: StationComponent = null
# --- State Variables ---
enum MovementState {
NO_CONTROL,
ZERO_G_INTERIOR,
LADDER_GRIP
}
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
# --- New: Physics Initialization (Assuming CharacterBody2D is parented to the scene root or Ship) ---
# NOTE: CharacterBody2D cannot inherit OrbitalBody2D, so we manage its velocity manually.
func _ready():
# Set up overlap signals if they aren't already connected in the scene file
# You must have an Area2D child on PilotBall to detect overlaps.
# Placeholder: Assuming the PilotBall has an Area2D named 'OverlapChecker'
#var overlap_checker = find_child("OverlapChecker")
#if overlap_checker:
overlap_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)
# Ensure this action is set in project settings: "interact" mapped to Space.
if !InputMap.has_action("interact"):
push_error("Missing 'interact' input action for ladder logic.")
camera.make_current()
# New function to handle global inputs
func _unhandled_input(event: InputEvent) -> void:
# --- Map Toggling ---
if event.is_action_pressed("ui_map_mode"):
SignalBus.emit_signal("map_mode_toggled")
# --- Time Scale Controls ---
if event.is_action_pressed("time_increase"):
Engine.time_scale = min(Engine.time_scale * 1.2, 1000)
elif event.is_action_pressed("time_decrease"):
Engine.time_scale = max(Engine.time_scale * 0.833, 0.1)
elif event.is_action_pressed("time_reset"):
Engine.time_scale = 1.0
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
func _physics_process(delta):
# 1. Update State based on environment
_update_movement_state()
var input_dir = Input.get_vector("move_left", "move_right", "move_up", "move_down")
match current_state:
#MovementState.NO_CONTROL:
## Apply heavy drag to simulate floating in space without external push
#_sluggish_movement(input_dir, delta)
MovementState.ZERO_G_INTERIOR:
# Sluggish movement: player is pushing off nearby walls/hullplates
_sluggish_movement(input_dir, delta)
MovementState.LADDER_GRIP:
# Snappy movement: direct control and high acceleration
_ladder_movement(input_dir, delta)
# --- Update Ladder/Station Input Handling ---
# We'll replace the old _handle_ladder_input with a more general one
_handle_interaction_input(input_dir)
move_and_slide()
# --- State Machine Update ---
func _update_movement_state():
# Priority 1: Ladder Grip
if ladder_area and Input.is_action_pressed("interact"):
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)
# --- Ladder Input and Launch Logic ---
func _handle_interaction_input(input_dir: Vector2):
# If we are currently using a station
if current_station:
if Input.is_action_just_pressed("interact"):
current_station.disengage()
current_station = null
return # Do nothing else while in a station
# If we are near a station and press interact
if is_instance_valid(nearby_station) and Input.is_action_just_pressed("interact"):
current_station = nearby_station
current_station.occupy(self)
return
# If currently grabbing, SPACE press is handled in _update_movement_state
if current_state == MovementState.LADDER_GRIP:
if Input.is_action_just_released("interact"):
# Launch the player away from the ladder
# Determine launch direction: opposite of input, or default forward
var launch_direction = -input_dir.normalized()
if launch_direction == Vector2.ZERO:
# Default launch: use the character's forward direction (e.g., rotation 0)
launch_direction = Vector2.UP.rotated(rotation)
velocity = launch_direction * LAUNCH_VELOCITY
# Immediately switch to zero-G interior state
is_grabbing_ladder = false
current_state = MovementState.ZERO_G_INTERIOR
# --- New Functions for Station Interaction ---
func _on_station_area_entered(area: Area2D):
if area.get_parent() is StationComponent:
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

View File

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

View File

@ -0,0 +1,26 @@
[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="."]
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(3, 3)
[node name="OverlapDetector" type="Area2D" parent="."]
[node name="CollisionShape2D" type="CollisionShape2D" parent="OverlapDetector"]
shape = SubResource("CircleShape2D_rhbna")

View File

@ -0,0 +1,82 @@
[gd_scene load_steps=4 format=3 uid="uid://c77wxeb7gpplw"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_mtskc"]
[ext_resource type="PackedScene" uid="uid://bho8x10x4oab7" path="res://scenes/ship/builder/pieces/hullplate.tscn" id="2_aovrk"]
[ext_resource type="PackedScene" uid="uid://d3hitk62fice4" path="res://scenes/ship/builder/pieces/bulkhead.tscn" id="4_dwgsg"]
[node name="Module" type="Node2D"]
position = Vector2(-50, 50)
script = ExtResource("1_mtskc")
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="StructuralContainer" type="Node2D" parent="."]
[node name="Hullplate" parent="StructuralContainer" instance=ExtResource("2_aovrk")]
[node name="@StaticBody2D@31031" parent="StructuralContainer" instance=ExtResource("2_aovrk")]
position = Vector2(0, 100)
[node name="@StaticBody2D@31033" parent="StructuralContainer" instance=ExtResource("2_aovrk")]
position = Vector2(100, 100)
[node name="@StaticBody2D@31035" parent="StructuralContainer" instance=ExtResource("2_aovrk")]
position = Vector2(100, 0)
[node name="@StaticBody2D@31037" parent="StructuralContainer" instance=ExtResource("2_aovrk")]
position = Vector2(100, -100)
[node name="@StaticBody2D@31039" parent="StructuralContainer" instance=ExtResource("2_aovrk")]
position = Vector2(100, -200)
[node name="@StaticBody2D@31041" parent="StructuralContainer" instance=ExtResource("2_aovrk")]
position = Vector2(0, -200)
[node name="@StaticBody2D@31043" parent="StructuralContainer" instance=ExtResource("2_aovrk")]
position = Vector2(0, -100)
[node name="Bulkhead" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(-50, 100)
[node name="@StaticBody2D@31046" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(-50, 0)
[node name="@StaticBody2D@31048" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(-50, -100)
[node name="@StaticBody2D@31050" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(-50, -200)
[node name="@StaticBody2D@31052" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(150, -200)
[node name="@StaticBody2D@31054" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(150, -100)
[node name="@StaticBody2D@31056" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(150, 0)
[node name="@StaticBody2D@31058" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(150, 100)
[node name="@StaticBody2D@31060" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(0, 150)
rotation = 1.5708
[node name="@StaticBody2D@31062" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(100, 150)
rotation = 1.5708
[node name="@StaticBody2D@31064" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(0, -250)
rotation = 1.5708
[node name="@StaticBody2D@31066" parent="StructuralContainer" instance=ExtResource("4_dwgsg")]
position = Vector2(100, -250)
rotation = 1.5708
[node name="HullVolumeContainer" type="Node2D" parent="."]
[node name="AtmosphereVisualizer" type="Node2D" parent="."]
[node name="Camera2D" type="Camera2D" parent="."]
position = Vector2(50, -50)

View File

@ -1,31 +1,14 @@
class_name Module extends OrbitalBody3D
@tool
class_name Module
extends OrbitalBody2D
# --- New properties inherited from Spaceship ---
@export var ship_name: String = "Unnamed Ship" # Only relevant for the root module
@export var hull_integrity: float = 100.0 # This could also be a calculated property later
const COMPONENT_GRID_SIZE = 64.0
func _ready():
super._ready()
child_entered_tree.connect(_on_child_entered_tree)
# Handle existing children (if any existed before ready)
for child in get_children():
_enforce_child_physics_mode(child)
func _on_child_entered_tree(node: Node):
_enforce_child_physics_mode(node)
func _enforce_child_physics_mode(node: Node):
# Only affect OrbitalBody3D children that are NOT the module itself
if node is OrbitalBody3D and node != self:
# We only enforce ANCHORED if this Module is a COMPOSITE (Root)
if physics_mode == PhysicsMode.COMPOSITE:
node.physics_mode = PhysicsMode.ANCHORED
# print("Module '%s' enforced ANCHORED mode on child '%s'" % [name, node.name])
# --- Helper functions to get children by type ---
# --- NEW: Helper functions to get children by type ---
func get_structural_pieces() -> Array[StructuralPiece]:
var pieces: Array[StructuralPiece]
for child in get_children():
@ -40,11 +23,6 @@ func get_components() -> Array[Component]:
components.append(child)
return components
func set_initial_velocity(velocity: Vector3):
linear_velocity = velocity
for piece in get_structural_pieces():
piece.linear_velocity = velocity
# --- UPDATED: Logic now uses the helper function ---
func get_attachment_points() -> Array:
var points = []
@ -54,47 +32,46 @@ func get_attachment_points() -> Array:
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
# })
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.origin.y * (COMPONENT_GRID_SIZE / 2.0)
# points.append({
# "position": interior_point,
# "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.origin.y * (COMPONENT_GRID_SIZE / 2.0)
# points.append({
# "position": exterior_point,
# "type": Component.AttachmentType.EXTERIOR_HULL,
# "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
})
return points
# --- This function remains largely the same ---
func attach_component(component: Component, global_pos: Vector3, parent_piece: StructuralPiece):
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()
component.owner = self
_update_mass_and_inertia()
# --- UPDATED: Logic now uses the helper function ---
func _recalculate_collision_shape():
# This logic is much simpler now. We just iterate over relevant children.
var _combined_polygons = []
var combined_polygons = []
for piece in get_structural_pieces():
# You would use logic here to transform the piece's local shape
@ -105,7 +82,7 @@ func _recalculate_collision_shape():
# combined_polygons.append(piece_collision_shape.shape.points)
pass
# NOTE: The OrbitalBody3D's _update_mass_and_inertia() takes care of mass!
# NOTE: The OrbitalBody2D's _update_mass_and_inertia() takes care of mass!
pass
# --- UPDATED: Clear module now iterates over all relevant children ---
@ -118,6 +95,7 @@ func clear_module():
_recalculate_collision_shape()
# --- New function inherited from Spaceship ---
# Damage can have a position for breach effects.
func take_damage(amount: float, damage_position: Vector2):
hull_integrity -= amount

View File

@ -0,0 +1,13 @@
[gd_scene load_steps=2 format=3 uid="uid://cm0rohkr6khd1"]
[ext_resource type="Script" uid="uid://6co67nfy8ngb" path="res://scenes/ship/builder/module.gd" id="1_b1h2b"]
[node name="Module" type="Node2D"]
script = ExtResource("1_b1h2b")
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="StructuralContainer" type="Node2D" parent="."]
[node name="HullVolumeContainer" type="Node2D" parent="."]
[node name="AtmosphereVisualizer" type="Node2D" parent="."]

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

View File

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

View File

@ -0,0 +1,29 @@
[gd_scene load_steps=3 format=3 uid="uid://d3hitk62fice4"]
[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 = 16
collision_mask = 60
script = ExtResource("1_1wp2n")
metadata/_custom_type_script = "uid://b7f8x2qimvn37"
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_1wp2n")
[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 = -5.0
offset_top = -50.0
offset_right = 5.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.6, 0.6, 0.6, 1)

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

View File

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

View File

@ -0,0 +1,28 @@
[gd_scene load_steps=3 format=3 uid="uid://bho8x10x4oab7"]
[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)
[node name="Hullplate" type="StaticBody2D"]
collision_mask = 0
script = ExtResource("1_ecow4")
metadata/_custom_type_script = "uid://b7f8x2qimvn37"
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("RectangleShape2D_1wp2n")
[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 = -50.0
offset_top = -50.0
offset_right = 50.0
offset_bottom = 50.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0.4, 0.4, 0.4, 1)

View File

@ -0,0 +1,20 @@
@tool
class_name StructuralPiece
extends OrbitalBody2D
# Does this piece block atmosphere? (e.g., a hull plate would, a girder would not).
@export var is_pressurized: bool = true
# The health of this specific piece.
@export var health: float = 100.0
# This setter is triggered by the editor.
var is_preview: bool = false:
set(value):
is_preview = value
if is_preview:
# Make the piece translucent if it's a preview.
modulate = Color(1, 1, 1, 0.5)
else:
# Make it opaque if it's a permanent piece.
modulate = Color(1, 1, 1, 1)

View File

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

View File

@ -0,0 +1,16 @@
[gd_scene load_steps=3 format=3 uid="uid://ds4eilbvihjy7"]
[ext_resource type="Script" uid="uid://b7f8x2qimvn37" path="res://scenes/ship/builder/pieces/structural_piece.gd" id="1_6jsoj"]
[sub_resource type="CircleShape2D" id="CircleShape2D_jsbwo"]
[node name="StructuralPiece" type="Node2D"]
script = ExtResource("1_6jsoj")
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="CollisionShape2D" type="CollisionShape2D" parent="."]
shape = SubResource("CircleShape2D_jsbwo")
[node name="ColorRect" type="ColorRect" parent="."]
offset_right = 40.0
offset_bottom = 40.0

View File

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

View File

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

View File

@ -1,4 +1,4 @@
extends OrbitalBody3D
extends OrbitalBody2D
class_name Component
# Defines the size of the component in terms of the grid (e.g., 1x1, 1x2, 2x2)
@ -14,7 +14,6 @@ enum AttachmentType { INTERIOR_WALL, EXTERIOR_HULL, FLOOR_OR_CEILING }
var attached_piece: StructuralPiece = null
func _ready():
super()
# OrbitalBody2D will handle mass initialization and physics setup.
pass
@ -24,12 +23,3 @@ func activate():
func deactivate():
pass
# Helper to find the main ship/module this component belongs to
func get_root_module() -> Module:
var current_node = self
while is_instance_valid(current_node):
if current_node is Module and current_node.physics_mode == PhysicsMode.COMPOSITE:
return current_node
current_node = current_node.get_parent()
return null

View File

@ -0,0 +1,8 @@
@tool
class_name Databank
extends Resource
## The name displayed in the UI (e.g., "Flight Helm MK1").
@export var display_name: String = "Unnamed Databank"
## The UI scene this databank provides when installed.
@export var ui_scene: PackedScene

View File

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

View File

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

View File

@ -0,0 +1,82 @@
@tool
class_name StationComponent
extends Component
signal occupancy_changed(is_occupied: bool)
## The "software" currently installed in this station.
@export var installed_databanks: Array[Databank]
var occupant: PilotBall = null
var active_ui_instances: Array[Control] = []
@onready var interaction_area: Area2D = $InteractionArea
func _process(delta):
# NEW: The station now checks for the disengage input.
if is_occupied() and Input.is_action_just_pressed("interact"):
disengage()
func is_occupied() -> bool:
return is_instance_valid(occupant)
func occupy(character: PilotBall):
print("foo")
if is_occupied(): return
occupant = character
#occupant.process_mode = Node.PROCESS_MODE_DISABLED
# Position character at the station (no reparenting needed)
occupant.global_position = global_position
# Launch the UIs from the installed databanks
launch_interfaces()
occupancy_changed.emit(true)
func disengage():
if not is_occupied(): return
# Close all open UIs
close_interfaces()
#occupant.process_mode = Node.PROCESS_MODE_INHERIT
occupant = null
occupancy_changed.emit(false)
func launch_interfaces():
var root_module = get_root_module()
if not is_instance_valid(root_module): return
# Find the main game UI to add these to.
# This assumes you have a CanvasLayer with the name "MainGameUI" in your main scene.
var main_ui = get_tree().get_first_node_in_group("main_ui_container")
if not main_ui:
push_error("No main UI container found for station interfaces!")
return
for db in installed_databanks:
if not db or not db.ui_scene: continue
var ui_instance = db.ui_scene.instantiate()
active_ui_instances.append(ui_instance)
main_ui.add_child(ui_instance)
# VERY IMPORTANT: Give the UI a reference to the ship it needs to control.
if ui_instance.has_method("initialize"):
ui_instance.initialize(root_module)
func close_interfaces():
for ui in active_ui_instances:
if is_instance_valid(ui):
ui.queue_free()
active_ui_instances.clear()
# Helper to find the main ship/module this station 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.is_sim_root:
return current_node
current_node = current_node.get_parent()
return null

View File

@ -0,0 +1,14 @@
[gd_scene load_steps=3 format=3 uid="uid://2n42nstcj1n0"]
[ext_resource type="Script" uid="uid://2reyxkr78ra0" path="res://scenes/ship/components/station_component.gd" id="1_8usqu"]
[sub_resource type="CircleShape2D" id="CircleShape2D_8usqu"]
[node name="Station" type="Node2D"]
script = ExtResource("1_8usqu")
[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)

View File

@ -0,0 +1,117 @@
# Thruster.gd
class_name Thruster
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
# Engine efficiency. Higher is better (more "bang for your buck").
# Measures how much momentum change you get per unit of fuel.
@export var specific_impulse_isp: float = 300.0
# The type of fuel resource this thruster consumes.
@export var enabled: bool = true
@export var fuel_resource_name: String = "ChemicalFuel"
@export var main_thruster: bool = true
# A state variable to track if the thruster is active
var is_firing: bool = false
func _ready() -> void:
super()
# --- 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)
# 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:
if thrust_force <= 0: return 0.0
# Standard rocket equation using Isp. g0 is standard gravity (9.81).
var mass_flow_rate = thrust_force / (specific_impulse_isp * 9.81)
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():
if enabled:
is_firing = true
# The controller calls this ONCE to deactivate the thruster.
func turn_off():
is_firing = false
# --- Godot Physics Callback ---
func _physics_process(_delta: float):
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()
# Function called by the ThrusterController system to fire the thruster
func apply_thrust_force():
if is_firing:
# 1. Calculate the local force vector (magnitude and direction)
var local_force = Vector2.UP * max_thrust
# 2. FIX: Convert the force to global space using ONLY the rotation (basis).
# This ensures the force vector's magnitude is not corrupted by the thruster's global position.
# NOTE: This replaces the problematic global_transform * vector or global_transform.xform(vector)
var force_vector = global_transform.basis_xform(local_force)
# 3. Apply the force to itself.
apply_force(force_vector, global_position)
func _draw():
# This function is only called if the thruster is firing (due to queue_redraw)
if not is_firing:
return
# --- Draw a fiery, flickering cone ---
# The plume goes in the OPPOSITE direction of the thrust
var plume_direction = Vector2.DOWN
var plume_length = randf_range(20.0, 30.0) # Random length for a flickering effect
# Define the 3 points of a triangle for the cone
var tip = plume_direction * plume_length
var base_offset = plume_direction.orthogonal() * 8.0
var base1 = base_offset
var base2 = -base_offset
var points = PackedVector2Array([base1, tip, base2])
# Draw the cone with a fiery color
draw_polygon(points, PackedColorArray([Color.ORANGE_RED, Color.GOLD, Color.ORANGE_RED]))

View File

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

View File

@ -0,0 +1,18 @@
class_name ShipSignalBus
extends Node
# --- Navigation & Maneuvering Events ---
# Emitted when a maneuver plan is calculated.
signal maneuver_planned(plan)
# Emitted to command the start of a timed rotation.
signal timed_rotation_commanded(target_rotation_rad, time_window)
# Emitted to command the start of a timed main engine burn.
signal timed_burn_commanded(duration)
# --- Thruster & Ship Status Events ---
# Emitted when the main engine starts or stops firing.
signal main_engine_state_changed(is_firing: bool, total_thrust: float)
# Emitted when RCS thrusters are fired.
signal rcs_state_changed(is_firing: bool, torque: float)

View File

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

85
scenes/ship/spaceship.gd Normal file
View File

@ -0,0 +1,85 @@
# Spaceship.gd
class_name Spaceship
extends OrbitalBody2D
@export var ship_name: String = "Stardust Drifter"
@export var dry_mass: float = 1000.0 # Mass of the ship without fuel/cargo (in kg)
@export var hull_integrity: float = 100.0
@onready var camera: Camera2D = $Camera2D
@export_category("Camera")
@export var zoom_speed: float = 1.0
var current_time_scale: float = Engine.time_scale
# --- Node References to Modular Systems ---
@onready var signal_bus: ShipSignalBus = $SignalBus
@onready var thruster_controller: ThrusterController = $ThrusterController
@onready var fuel_system = $FuelSystem
@onready var life_support = $LifeSupport
@onready var navigation_computer = $NavigationComputer
# @onready var weapon_system = $WeaponSystem
# @onready var power_grid = $PowerGrid
func _ready() -> void:
super()
GameManager.register_ship(self)
# Give the navigation computer a reference to this ship
if navigation_computer:
navigation_computer.ship = self
camera.make_current()
# This function will now handle all non-UI input for the player-controlled ship.
func _unhandled_input(event: InputEvent) -> void:
# --- Map Toggling ---
if event.is_action_pressed("ui_map_mode"):
# Instead of showing/hiding a node directly, we broadcast our intent.
# The NavigationComputer will be listening for this global signal.
SignalBus.emit_signal("map_mode_toggled")
# --- Time Scale Controls ---
if event.is_action_pressed("time_increase"):
var new_value = min(current_time_scale * 1.2, 1000)
current_time_scale = clamp(new_value, 0.5, 1000)
Engine.time_scale = current_time_scale
elif event.is_action_pressed("time_decrease"):
var new_value = max(current_time_scale * 0.833, 0.1)
current_time_scale = clamp(new_value, 0.5, 1000)
Engine.time_scale = current_time_scale
elif event.is_action_pressed("time_reset"):
Engine.time_scale = 1.0
# --- Public API for Ship Management ---
# Call this to take damage. Damage can have a position for breach effects.
func take_damage(amount: float, damage_position: Vector2):
hull_integrity -= amount
print("%s hull integrity at %.1f%%" % [ship_name, hull_integrity])
if hull_integrity <= 0:
destroy_ship()
else:
# Check if the hit caused a hull breach
life_support.check_for_breach(damage_position)
func destroy_ship():
print("%s has been destroyed!" % ship_name)
queue_free()
# --- Signal Handlers ---
#func _on_fuel_mass_changed():
# Update the ship's total mass when fuel is consumed or added
#update_total_mass()
func _on_hull_breach(breach_position: Vector2, force_vector: Vector2):
pass
# A hull breach applies a continuous force at a specific point
# For simplicity, we can apply it as a central force and torque here
#var force = force_vector * 100 # Scale the force
#
## Calculate torque: Torque = r x F (cross product of position vector and force)
#var position_relative_to_center = breach_position - self.global_position
#var torque = position_relative_to_center.cross(force)

View File

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

View File

@ -0,0 +1,63 @@
[gd_scene load_steps=9 format=3 uid="uid://dlck1lyrn1xvp"]
[ext_resource type="Script" uid="uid://dyqbk4lcx3mhq" path="res://scenes/ship/spaceship.gd" id="1_ae4p7"]
[ext_resource type="Script" uid="uid://c0bx113ifxyh8" path="res://scenes/ship/systems/thruster_controller.gd" id="2_xs8u7"]
[ext_resource type="PackedScene" uid="uid://c77wxeb7gpplw" path="res://scenes/modules/test_module.tscn" id="2_y58gg"]
[ext_resource type="Script" uid="uid://dx3uerblskj5r" path="res://scenes/ship/systems/fuel_system.gd" id="3_xs8u7"]
[ext_resource type="Script" uid="uid://buyp6t5cppitw" path="res://scenes/ship/systems/life_support.gd" id="4_v0rat"]
[ext_resource type="PackedScene" uid="uid://cxpjm8tp3l1j7" path="res://scenes/ship/systems/navigation_computer.tscn" id="5_6nyhl"]
[ext_resource type="Script" uid="uid://w1546qtaupd2" path="res://scenes/ship/ship_signal_bus.gd" id="7_yl4tl"]
[ext_resource type="PackedScene" uid="uid://c0bb77rmyatr0" path="res://scenes/ship/components/thruster.tscn" id="8_oedjh"]
[node name="Spaceship" type="Node2D"]
script = ExtResource("1_ae4p7")
base_mass = 2000.0
inertia = 500.0
metadata/_custom_type_script = "uid://0isnsk356que"
[node name="Module" parent="." instance=ExtResource("2_y58gg")]
[node name="FuelSystem" type="Node" parent="."]
script = ExtResource("3_xs8u7")
[node name="LifeSupport" type="Node" parent="."]
script = ExtResource("4_v0rat")
[node name="NavigationComputer" parent="." instance=ExtResource("5_6nyhl")]
[node name="Camera2D" type="Camera2D" parent="."]
[node name="ThrusterController" type="Node2D" parent="."]
script = ExtResource("2_xs8u7")
[node name="RCS1" parent="." instance=ExtResource("8_oedjh")]
position = Vector2(-125, 200)
rotation = 1.5708
main_thruster = false
inertia = 0.0
[node name="RCS2" parent="." instance=ExtResource("8_oedjh")]
position = Vector2(-125, -200)
rotation = 1.5708
main_thruster = false
inertia = 0.0
[node name="RCS3" parent="." instance=ExtResource("8_oedjh")]
position = Vector2(125, -200)
rotation = -1.5708
main_thruster = false
inertia = 0.0
[node name="RCS4" parent="." instance=ExtResource("8_oedjh")]
position = Vector2(125, 200)
rotation = -1.5708
main_thruster = false
inertia = 0.0
[node name="MainEngine" parent="." instance=ExtResource("8_oedjh")]
position = Vector2(-1, 226)
max_thrust = 100.0
inertia = 0.0
[node name="SignalBus" type="Node" parent="."]
script = ExtResource("7_yl4tl")

View File

@ -1,23 +1,22 @@
# space_simulation/scripts/map_controller.gd
class_name SensorPanel
extends BasePanel
signal body_selected_for_planning(body: OrbitalBody3D)
class_name MapController
extends Control
signal body_selected_for_planning(body: RigidBody2D)
@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: OrbitalBody3D
var focal_body: RigidBody2D
var icon_map: Dictionary = {}
var followed_body: OrbitalBody3D = null
var followed_body: RigidBody2D = null
var map_tween: Tween
# The starting point for our lerp animation.
@ -29,92 +28,67 @@ var follow_progress: float = 0.0:
# We must redraw every time the progress changes.
queue_redraw()
func get_output_sockets():
return ["body_selected_for_planning"]
func get_input_sockets():
return ["update_sensor_feed"]
# This is now the primary input for the map. It receives the "sensor feed".
func update_sensor_feed(all_bodies: Array[OrbitalBody3D]):
# 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 _ready() -> void:
await get_tree().physics_frame
var star_system = GameManager.current_star_system
if is_instance_valid(star_system):
focal_body = star_system.get_system_data().star
_populate_map()
func _process(_delta: float) -> void:
_update_icon_positions()
func _draw() -> void:
var map_center = get_rect().size / 2.0
# 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[OrbitalBody3D] = []
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
if body is Asteroid: continue
var icon = icon_map[body]
if not icon.visible: continue
var path_points = []
if body is CelestialBody:
var planet_system = body.get_parent() as Barycenter
draw_projected_orbits(planet_system.get_internal_attractors())
if body is CelestialBody: path_points = OrbitalMechanics._calculate_relative_orbital_path(body)
elif body is OrbitalBody2D: path_points = OrbitalMechanics._calculate_n_body_orbital_path(body)
else: continue
var scaled_path_points = PackedVector2Array()
for point in path_points:
# Ensure path is drawn relative to the main focal body (the star)
var path_world_pos = point + focal_body.global_position
var relative_pos = path_world_pos - focal_body.global_position
var scaled_pos = (relative_pos * map_scale) + map_offset + map_center
scaled_path_points.append(scaled_pos)
if scaled_path_points.size() > 1:
draw_polyline(scaled_path_points, Color(Color.WHITE, 0.2), 1.0, true)
func draw_projected_orbits(bodies_to_project: Array[OrbitalBody3D]):
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 _populate_map():
for child in get_children():
child.queue_free()
icon_map.clear()
var all_bodies = GameManager.get_all_trackable_bodies()
for body in all_bodies:
if not is_instance_valid(body): continue
var icon = map_icon_scene.instantiate() as MapIcon
add_child(icon)
icon.initialize(body)
icon_map[body] = icon
icon.selected.connect(_on_map_icon_selected)
icon.follow_requested.connect(_on_follow_requested)
func _update_icon_positions():
if not is_instance_valid(focal_body): return
var map_center = map_canvas.get_rect().size / 2.0
var map_center = get_rect().size / 2.0
# TODO: Follow logic broke when map_canvas was introduced
# --- Continuous follow logic ---
# --- MODIFIED: Continuous follow logic ---
if is_instance_valid(followed_body):
# Calculate the ideal offset to center the followed body.
var relative_target_pos = followed_body.global_position - focal_body.global_position
@ -173,8 +147,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 = map_canvas.get_local_mouse_position()
var map_center = map_canvas.get_rect().size / 2.0
var mouse_pos = get_local_mouse_position()
var map_center = get_rect().size / 2.0
var point_under_mouse_world = (mouse_pos - map_center - map_offset) / map_scale
map_scale *= zoom_factor
@ -192,10 +166,10 @@ func _gui_input(event: InputEvent) -> void:
map_offset += event.relative
func _on_map_icon_selected(body: OrbitalBody3D):
body_selected_for_planning.emit(body)
func _on_map_icon_selected(body: RigidBody2D):
emit_signal("body_selected_for_planning", body)
func _on_follow_requested(body: OrbitalBody3D):
func _on_follow_requested(body: RigidBody2D):
print("Map view locking on to: ", body.name)
follow_progress = 0.0
followed_body = body

View File

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

View File

@ -0,0 +1,402 @@
# NavigationComputer.gd
extends Node
@onready var ship: Spaceship = %Spaceship
var ship_signal_bus: ShipSignalBus
# --- Node References ---
@onready var navigation_ui: CanvasLayer = %NavigationUI
@onready var map_controller: MapController = %MapController
@onready var target_label: Label = %TargetLabel
@onready var info_label: Label = %InfoLabel
@onready var ship_status_label: Label = %ShipStatusLabel
@onready var torque_indicator: ColorRect = %TorqueIndicator
@onready var sub_viewport: SubViewport = %SubViewport
@onready var ship_view_camera: Camera2D = %ShipViewCamera # Add this reference
# Buttons for different maneuvers
@onready var plan_hohmann_button: Button = %PlanHohmannButton
@onready var plan_fast_button: Button = %PlanFastButton
@onready var plan_torchship_button: Button = %PlanTorchshipButton
@onready var plan_gravity_assist_button: Button = %PlanGravityAssistButton
@onready var execute_plan_button: Button = %ExecutePlanButton
# How many seconds before a burn we should start orienting the ship.
const PRE_BURN_ORIENTATION_SECONDS = 30.0 # Give a larger window for the new logic
const ROTATION_SAFETY_BUFFER = 10.0 # Seconds to ensure rotation finishes before burn
# A flag to make sure we only send the signal once per maneuver
var rotation_plan_sent = false
# --- State Management ---
enum State { IDLE, PLANNING, WAITING, EXECUTING }
var current_state: State = State.IDLE
# --- Navigation Data ---
var source_body: CelestialBody
var target_body: CelestialBody
var current_plan # Can be an array of ImpulsiveBurn or a ContinuousBurnPlan
# --- Inner classes to hold maneuver data ---
class ImpulsiveBurn:
var delta_v_magnitude: float
var wait_time: float = 0.0
var burn_duration: float
var desired_rotation_rad: float # The world rotation the ship needs to be in
class ContinuousBurnPlan:
var total_travel_time: float
var acceleration_time: float
var initial_burn_direction: Vector2 # The world-space direction vector for the burn
func _ready() -> void:
# tell our SubViewport to render the same world as main viewport.
if is_instance_valid(sub_viewport):
print("NAV COMP: Sub viewport found")
sub_viewport.world_2d = get_viewport().world_2d
else:
print("NAV COMP: Sub viewport not found")
# Connect to the global signal from the SignalBus
SignalBus.map_mode_toggled.connect(on_map_mode_toggled)
if is_instance_valid(ship):
ship_signal_bus = ship.signal_bus
# Ensure the UI starts hidden
if navigation_ui:
navigation_ui.hide()
# Connect UI signals
map_controller.body_selected_for_planning.connect(_on_target_selected)
plan_hohmann_button.pressed.connect(_on_plan_hohmann_pressed)
plan_fast_button.pressed.connect(_on_plan_fast_pressed)
plan_torchship_button.pressed.connect(_on_plan_torchship_pressed)
plan_gravity_assist_button.pressed.connect(_on_plan_gravity_assist_pressed)
execute_plan_button.pressed.connect(_on_execute_plan_pressed)
ship_status_label.text = ""
update_ui()
# Add a background to the UIm
_setup_background()
func _setup_background():
# --- FIX #1: Add a black, opaque background ---
var bg = ColorRect.new()
bg.color = Color.BLACK
# Add it as the very first child so it's behind everything else.
navigation_ui.add_child(bg)
navigation_ui.move_child(bg, 0)
# Make the background cover the entire screen.
bg.anchor_right = 1.0
bg.anchor_bottom = 1.0
# This function is called whenever any node in the game emits the "map_mode_toggled" signal.
func on_map_mode_toggled():
if navigation_ui:
# Toggle the visibility of the UI screen
navigation_ui.visible = not navigation_ui.visible
func _process(delta: float) -> void:
# This ensures the viewport camera always mirrors the ship's main camera.
if is_instance_valid(ship) and is_instance_valid(ship.camera):
if is_instance_valid(ship_view_camera):
ship_view_camera.global_transform = ship.camera.global_transform
ship_view_camera.zoom = ship.camera.zoom
_update_ship_status_label()
if current_state == State.PLANNING and current_plan:
if current_plan is Array and not current_plan.is_empty():
var first_burn = current_plan[0]
# The plan is not locked in, but we can see the window approaching.
first_burn.wait_time -= delta
var time_str = _format_seconds_to_mmss(first_burn.wait_time)
info_label.text = "Optimal window in: %s.\nPress Execute to lock in plan." % time_str
if first_burn.wait_time < 0:
# If the window is missed during planning, mark the plan as stale.
info_label.text = "Transfer window missed. Please plan a new maneuver."
current_plan = null
update_ui()
if current_state == State.WAITING and current_plan:
if current_plan is Array and not current_plan.is_empty():
var next_burn: ImpulsiveBurn = current_plan[0]
next_burn.wait_time -= delta
var time_str = _format_seconds_to_mmss(next_burn.wait_time)
info_label.text = "Time to burn: %s" % time_str
# --- NEW: Emit the rotation plan ---
# When we are inside the orientation window, and haven't sent the plan yet...
if not rotation_plan_sent:
# The time allowed for rotation is the time we have left, minus a safety buffer.
var time_for_rotation = next_burn.wait_time - ROTATION_SAFETY_BUFFER
# Tell the thruster controller to handle it.
ship.signal_bus.emit_signal("timed_rotation_commanded", next_burn.desired_rotation_rad, time_for_rotation)
rotation_plan_sent = true # Mark the plan as sent
if next_burn.wait_time <= 0:
_execute_maneuver()
# The EXECUTING state would be handled by a dedicated autopilot script/node
func update_ui():
execute_plan_button.disabled = (current_plan == null or current_state != State.PLANNING)
if current_state == State.PLANNING:
# Show available plans based on engine type
# TODO change UI to show recommendations based on thruster type
#if installed_engine is ChemicalThruster:
plan_hohmann_button.show()
plan_fast_button.show()
plan_gravity_assist_button.show()
#elif installed_engine is IonDrive:
plan_torchship_button.show()
# --- Signal Handlers ---
func _on_target_selected(body: CelestialBody):
if current_state == State.IDLE or current_state == State.PLANNING:
target_body = body
# Assume ship is orbiting the target's primary (e.g., the star)
current_plan = null
current_state = State.PLANNING
target_label.text = "Target: %s." % target_body.name
info_label.text = "Select a maneuver."
update_ui()
func _on_execute_plan_pressed():
if current_plan:
current_state = State.WAITING
update_ui()
# --- Planning Function Calls ---
func _on_plan_hohmann_pressed():
current_plan = _calculate_hohmann_transfer(source_body, target_body)
# TODO: map_controller.draw_planned_trajectory(...)
update_ui()
func _on_plan_fast_pressed():
# For simplicity, a "fast" transfer is a Hohmann with a 25% larger transfer orbit
current_plan = _calculate_hohmann_transfer(source_body, target_body, 1.25)
# TODO: map_controller.draw_planned_trajectory(...)
update_ui()
func _on_plan_torchship_pressed():
current_plan = _calculate_brachistochrone_transfer()
# TODO: map_controller.draw_planned_trajectory(...)
update_ui()
func _on_plan_gravity_assist_pressed():
info_label.text = "Gravity Assist calculation is highly complex and not yet implemented."
print("Placeholder for Gravity Assist logic.")
# --- Calculation and Execution ---
func _calculate_hohmann_transfer(source_planet: CelestialBody, target_planet: CelestialBody, transfer_boost_factor: float = 1.0) -> Array:
# Get the central star safely from the GameManager.
#var ship_current_primary = ship.primary
var star = GameManager.get_system_data().star
if not is_instance_valid(star):
print("Hohmann Error: Could not find star in GameManager.")
return []
# This maneuver requires the ship and target to orbit the same star.
if not target_planet:
info_label.text = "Invalid Transfer: No target object for intersect."
return []
# mu (μ): The Standard Gravitational Parameter of the star. It's G * M, a constant that simplifies calculations.
var mu = OrbitalMechanics.G * star.mass
# r1: The ship's current orbital radius (distance from the star).
var r1 = ship.global_position.distance_to(star.global_position)
# r2: The target planet's orbital radius.
var r2 = target_planet.global_position.distance_to(star.global_position)
# --- Hohmann Transfer Calculations ---
# v_source_orbit: The ship's current circular orbital speed.
var v_source_orbit = sqrt(mu / r1)
# v_target_orbit: The target planet's circular orbital speed.
var v_target_orbit = sqrt(mu / r2)
# a_transfer: The semi-major axis (average radius) of the elliptical transfer orbit.
var a_transfer = (r1 + r2) / 2.0 * transfer_boost_factor
# v_transfer_periapsis: The required speed at the start of the transfer (periapsis) to get onto the ellipse.
var v_transfer_periapsis = sqrt(mu * ((2.0 / r1) - (1.0 / a_transfer)))
# v_transfer_apoapsis: The speed the ship will have when it arrives at the end of the transfer (apoapsis).
var v_transfer_apoapsis = sqrt(mu * ((2.0 / r2) - (1.0 / a_transfer)))
# delta_v1: The first burn. The change in speed needed to go from the source orbit to the transfer orbit.
var delta_v1 = v_transfer_periapsis - v_source_orbit
# delta_v2: The second burn. The change in speed needed to go from the transfer orbit to the target orbit.
var delta_v2 = v_target_orbit - v_transfer_apoapsis
# time_of_flight: Half the period of the elliptical transfer orbit (Kepler's 3rd Law).
var time_of_flight = PI * sqrt(pow(a_transfer, 3) / mu)
# --- Launch Window (Phase Angle) Calculations ---
# ang_vel_target: The angular speed of the target planet (in radians per second).
var ang_vel_target = sqrt(mu / pow(r2, 3))
# travel_angle: The angle the target planet will travel through during the ship's flight time.
var travel_angle = ang_vel_target * time_of_flight
# required_phase_angle: The starting angle needed between the ship and target for a successful intercept.
var required_phase_angle = PI - travel_angle
# vec_to_ship/target: Direction vectors from the star to the ship and target.
var vec_to_ship = (ship.global_position - star.global_position).normalized()
var vec_to_target = (target_planet.global_position - star.global_position).normalized()
# current_phase_angle: The angle between the ship and target right now.
var current_phase_angle = vec_to_ship.angle_to(vec_to_target)
# ang_vel_ship: The ship's current angular speed.
var ang_vel_ship = sqrt(mu / pow(r1, 3))
# relative_ang_vel: How quickly the ship is catching up to (or falling behind) the target.
var relative_ang_vel = ang_vel_ship - ang_vel_target
# angle_to_wait: The angular distance the ship needs to "wait" for alignment.
var angle_to_wait = current_phase_angle - required_phase_angle
# wait_time: The final calculated time in seconds to wait for the optimal launch window.
var wait_time = abs(angle_to_wait / relative_ang_vel)
# --- Final Plan Assembly ---
# Calculate burn durations
var acceleration = ship.thruster_controller.main_engine_max_thrust() / ship.mass
var burn_duration1 = delta_v1 / acceleration
var burn_duration2 = delta_v2 / acceleration
var plan = []
var burn1 = ImpulsiveBurn.new()
burn1.delta_v_magnitude = delta_v1
burn1.wait_time = wait_time
burn1.burn_duration = burn_duration1
# The desired rotation is the angle of the ship's prograde (tangential) velocity vector.
burn1.desired_rotation_rad = ship.linear_velocity.angle()
plan.append(burn1)
var burn2 = ImpulsiveBurn.new()
burn2.delta_v_magnitude = delta_v2
burn2.wait_time = time_of_flight - burn_duration1
burn2.burn_duration = burn_duration2
# The desired rotation for burn 2 is the tangential direction at the target orbit.
burn2.desired_rotation_rad = (target_planet.global_position - star.global_position).orthogonal().angle()
plan.append(burn2)
info_label.text = "Hohmann Plan:\nWait: %d s\nBurn 1: %.1f m/s (%.1f s)" % [wait_time, delta_v1, burn_duration1]
return plan
func _calculate_brachistochrone_transfer() -> ContinuousBurnPlan:
var distance = ship.global_position.distance_to(target_body.global_position)
var acceleration = ship.thruster_controller.main_engine_max_thrust() / ship.mass
if acceleration == 0: return null
# d = 1/2 * a * t^2 => t = sqrt(2d/a). We do this twice (accel/decel).
var time_for_half_journey = sqrt(distance / acceleration)
var plan = ContinuousBurnPlan.new()
plan.total_travel_time = 2 * time_for_half_journey
plan.acceleration_time = time_for_half_journey
plan.required_acceleration = acceleration
info_label.text = "Torchship Trajectory Calculated.\nTravel Time: %d s" % plan.total_travel_time
return plan
func _execute_maneuver():
if current_state != State.WAITING or not current_plan: return
current_state = State.EXECUTING
var burn: ImpulsiveBurn = current_plan.pop_front()
# Tell the controller to start burning. Orientation is already handled.
ship.signal_bus.emit_signal("timed_burn_commanded", burn.burn_duration)
# Set up for the next leg of the journey or finish
if not current_plan.is_empty():
# The next "wait_time" is the coasting period between burns.
current_state = State.WAITING
# Reset the flag here, preparing the system for the *next* burn's rotation command.
rotation_plan_sent = false
else:
current_state = State.IDLE
current_plan = null
update_ui()
var _previous_angular_velocity: float = 0.0
var actual_applied_torque: float = 0.0
func _update_ship_status_label():
if not is_instance_valid(ship):
ship_status_label.text = "NO SHIP DATA"
return
# Build an array of strings for each line of the display
var status_lines = []
var assume_torque = ship.thruster_controller.current_applied_torque
# Get rotation data from the ship
var rotation_deg = rad_to_deg(ship.rotation)
var current_angular_velocity = ship.angular_velocity
var angular_vel_dps = rad_to_deg(ship.angular_velocity)
var delta_omega = ship.angular_velocity - _previous_angular_velocity
var delta = get_physics_process_delta_time()
if delta > 0:
var angular_acceleration = delta_omega / delta
#actual_applied_torque = ship.accumulated_torque # ship.inertia * angular_acceleration
# Update the indicator color based on the comparison.
_previous_angular_velocity = current_angular_velocity
# Get the sign of each torque value.
var calc_sign = sign(assume_torque)
var actual_sign = sign(actual_applied_torque)
if calc_sign != 0 and calc_sign == actual_sign:
# Success: We are commanding a turn, and the physics engine agrees.
torque_indicator.color = Color.GREEN
elif calc_sign != 0 and actual_sign == 0:
# Mismatch: We are commanding a turn, but the physics engine reports no torque.
# This is the flicker you are seeing.
torque_indicator.color = Color.RED
else:
# Idle: No torque is being commanded, and none is being applied.
torque_indicator.color = Color.DARK_GRAY
var sensor_torque_sign = "+" if signf(actual_applied_torque) > 0.0 else "-" if signf(actual_applied_torque) < 0.0 else "0"
status_lines.append("Rotation: %.1f deg" % rotation_deg)
status_lines.append("Ang. Vel.: %.5f deg/s" % angular_vel_dps)
status_lines.append("Assumed Torque: %.2f N·m" % assume_torque)
status_lines.append("Sensed Torque: %.2f N·m" % actual_applied_torque)
# Get burn data from the thruster controller
var burn_time = ship.thruster_controller.current_burn_time_remaining
var thrust_force = ship.thruster_controller.current_thrust_force
status_lines.append("Burn Time: %.1f s" % burn_time)
status_lines.append("Thrust: %.5f N" % thrust_force)
# Join the lines with a newline character and update the label
ship_status_label.text = "\n".join(status_lines)
# Helper function to format a float of seconds into a M:SS string
func _format_seconds_to_mmss(seconds_float: float) -> String:
if seconds_float < 0:
seconds_float = 0
var total_seconds = int(seconds_float)
var minutes = total_seconds / 60
var seconds = total_seconds % 60
# "%02d" formats the seconds with a leading zero if needed (e.g., 05)
return "%d:%02d" % [minutes, seconds]

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