1
0
Fork 0

GDScript to C#

This commit is contained in:
Fabio Iotti 2024-10-06 16:13:47 +02:00
parent aa25860d6d
commit 10e5f5bd63
Signed by: bruce965
GPG key ID: 4EC13D9158A96B4C
38 changed files with 1170 additions and 709 deletions

7
SpaceCapture.csproj Normal file
View file

@ -0,0 +1,7 @@
<Project Sdk="Godot.NET.Sdk/4.3.0">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<RootNamespace>SpaceCapture</RootNamespace>
</PropertyGroup>
</Project>

19
SpaceCapture.sln Normal file
View file

@ -0,0 +1,19 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2012
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpaceCapture", "SpaceCapture.csproj", "{BDE35DF8-5DC8-4B5F-85AE-56A80323ED08}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
ExportDebug|Any CPU = ExportDebug|Any CPU
ExportRelease|Any CPU = ExportRelease|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BDE35DF8-5DC8-4B5F-85AE-56A80323ED08}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BDE35DF8-5DC8-4B5F-85AE-56A80323ED08}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BDE35DF8-5DC8-4B5F-85AE-56A80323ED08}.ExportDebug|Any CPU.ActiveCfg = ExportDebug|Any CPU
{BDE35DF8-5DC8-4B5F-85AE-56A80323ED08}.ExportDebug|Any CPU.Build.0 = ExportDebug|Any CPU
{BDE35DF8-5DC8-4B5F-85AE-56A80323ED08}.ExportRelease|Any CPU.ActiveCfg = ExportRelease|Any CPU
{BDE35DF8-5DC8-4B5F-85AE-56A80323ED08}.ExportRelease|Any CPU.Build.0 = ExportRelease|Any CPU
EndGlobalSection
EndGlobal

View file

@ -1,62 +1,62 @@
[gd_scene load_steps=19 format=3 uid="uid://cdaf4bh0jaa45"]
[ext_resource type="Script" path="res://scripts/game_logic.gd" id="1_uj8ti"]
[ext_resource type="Script" path="res://scripts/player.gd" id="2_plgh6"]
[ext_resource type="Script" path="res://scripts/PlayerLocal.cs" id="1_3h84q"]
[ext_resource type="Script" path="res://scripts/Planet.cs" id="1_rrxte"]
[ext_resource type="Script" path="res://scripts/Player.cs" id="1_tr3cj"]
[ext_resource type="Script" path="res://scripts/PlayerAI.cs" id="1_u42tr"]
[ext_resource type="Texture2D" uid="uid://bydmcc5d53ldx" path="res://icons/paw_print.svg" id="3_mxakn"]
[ext_resource type="Script" path="res://scripts/GameLogic.cs" id="3_q0fqo"]
[ext_resource type="Texture2D" uid="uid://1xh0qil16bkh" path="res://icons/character.svg" id="3_qxmev"]
[ext_resource type="Script" path="res://scripts/player_local.gd" id="4_7rlmh"]
[ext_resource type="Texture2D" uid="uid://dx1wjviioa8u5" path="res://icons/chip.svg" id="4_pgo63"]
[ext_resource type="Script" path="res://scripts/ui/planets_ui.gd" id="7_khbdl"]
[ext_resource type="PackedScene" uid="uid://dtlatmtuggp6x" path="res://scenes/backgrounds/nebula.tscn" id="7_sv4lv"]
[ext_resource type="Script" path="res://scripts/player_ai.gd" id="7_v73fw"]
[ext_resource type="Script" path="res://scripts/planet.gd" id="8_ve3b1"]
[ext_resource type="PackedScene" uid="uid://b0ec8jbenscpp" path="res://scenes/backgrounds/stars.tscn" id="9_lum1l"]
[ext_resource type="Script" path="res://scripts/ui/PlanetsUI.cs" id="11_u25me"]
[sub_resource type="Resource" id="Resource_mxp7y"]
script = ExtResource("2_plgh6")
color = Color(0.745, 0.745, 0.745, 1)
icon = ExtResource("3_mxakn")
script = ExtResource("1_tr3cj")
Color = Color(0.745, 0.745, 0.745, 1)
Icon = ExtResource("3_mxakn")
[sub_resource type="Resource" id="Resource_1dtpf"]
script = ExtResource("4_7rlmh")
color = Color(0.236993, 0.373968, 1, 1)
icon = ExtResource("3_qxmev")
script = ExtResource("1_3h84q")
Color = Color(0.236993, 0.373968, 1, 1)
Icon = ExtResource("3_qxmev")
[sub_resource type="Resource" id="Resource_7wtc7"]
script = ExtResource("7_v73fw")
color = Color(0.835294, 0.160784, 0, 1)
icon = ExtResource("4_pgo63")
script = ExtResource("1_u42tr")
Color = Color(0.835294, 0.160784, 0, 1)
Icon = ExtResource("4_pgo63")
[sub_resource type="Resource" id="Resource_50eg2"]
script = ExtResource("8_ve3b1")
location = Vector2(200, 200)
type = 0
[sub_resource type="Resource" id="Resource_1nuc3"]
script = ExtResource("1_rrxte")
Location = Vector2(200, 200)
Type = 0
[sub_resource type="Resource" id="Resource_2525o"]
script = ExtResource("8_ve3b1")
location = Vector2(952, 448)
type = 0
[sub_resource type="Resource" id="Resource_qh3gq"]
script = ExtResource("1_rrxte")
Location = Vector2(952, 448)
Type = 0
[sub_resource type="Resource" id="Resource_irtww"]
script = ExtResource("8_ve3b1")
location = Vector2(450, 234)
type = 0
[sub_resource type="Resource" id="Resource_u1yeo"]
script = ExtResource("1_rrxte")
Location = Vector2(450, 234)
Type = 0
[sub_resource type="Resource" id="Resource_wdycy"]
script = ExtResource("8_ve3b1")
location = Vector2(702, 414)
type = 0
[sub_resource type="Resource" id="Resource_c2fhs"]
script = ExtResource("1_rrxte")
Location = Vector2(702, 414)
Type = 0
[node name="Main" type="Node"]
[node name="GameLogic" type="Node" parent="." node_paths=PackedStringArray("planets_container", "trails_container", "fleets_container", "planets_ui")]
script = ExtResource("1_uj8ti")
planets_container = NodePath("../Planets")
trails_container = NodePath("../Trails")
fleets_container = NodePath("../Fleets")
planets_ui = NodePath("../PlanetsUI")
players = Array[ExtResource("2_plgh6")]([SubResource("Resource_mxp7y"), SubResource("Resource_1dtpf"), SubResource("Resource_7wtc7")])
planets = Array[ExtResource("8_ve3b1")]([SubResource("Resource_50eg2"), SubResource("Resource_2525o"), SubResource("Resource_irtww"), SubResource("Resource_wdycy")])
[node name="GameLogic" type="Node" parent="." node_paths=PackedStringArray("PlanetsContainer", "TrailsContainer", "FleetsContainer", "PlanetsUI")]
script = ExtResource("3_q0fqo")
PlanetsContainer = NodePath("../Planets")
TrailsContainer = NodePath("../Trails")
FleetsContainer = NodePath("../Fleets")
PlanetsUI = NodePath("../PlanetsUI")
Players = Array[ExtResource("1_tr3cj")]([SubResource("Resource_mxp7y"), SubResource("Resource_1dtpf"), SubResource("Resource_7wtc7")])
Planets = Array[Object]([SubResource("Resource_1nuc3"), SubResource("Resource_qh3gq"), SubResource("Resource_u1yeo"), SubResource("Resource_c2fhs")])
[node name="Stars" parent="." instance=ExtResource("9_lum1l")]
@ -68,7 +68,7 @@ planets = Array[ExtResource("8_ve3b1")]([SubResource("Resource_50eg2"), SubResou
[node name="Fleets" type="Node2D" parent="."]
[node name="PlanetsUI" type="Control" parent="." node_paths=PackedStringArray("trails_container")]
[node name="PlanetsUI" type="Control" parent="." node_paths=PackedStringArray("TrailsContainer")]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
@ -76,5 +76,5 @@ anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
script = ExtResource("7_khbdl")
trails_container = NodePath("../Trails")
script = ExtResource("11_u25me")
TrailsContainer = NodePath("../Trails")

View file

@ -12,9 +12,13 @@ config_version=5
config/name="Space Capture"
run/main_scene="res://main.tscn"
config/features=PackedStringArray("4.3", "GL Compatibility")
config/features=PackedStringArray("4.3", "C#", "GL Compatibility")
config/icon="res://icon.svg"
[dotnet]
project/assembly_name="SpaceCapture"
[rendering]
renderer/rendering_method="gl_compatibility"

View file

@ -0,0 +1,79 @@
using Godot;
namespace SpaceCapture;
public partial class ControlPlanet : Node2D
{
Player _player;
int _population;
/// <summary>
/// The player currenly controlling this planet.
/// </summary>
/// <value></value>
[Export]
public Player Player
{
get => _player;
set
{
_player = value;
Color color = _player?.Color ?? Colors.Magenta;
GetNode<ColorRect>("%Selection").Color = color;
GetNode<Label>("%PopulationLabel").Modulate = color;
GetNode<TextureRect>("%PopulationIcon").Modulate = color;
GetNode<TextureRect>("%PopulationIcon").Texture = Player?.Icon;
}
}
/// <summary>
/// Number of units currently stationed at the planet.
/// </summary>
/// <value></value>
[Export]
public int Population
{
get => _population;
set
{
_population = value;
GetNode<Label>("%PopulationLabel").Text = $"{value}";
}
}
public override void _Ready()
{
IsSelected = false;
}
[ExportCategory("UI")]
object _placeholder1;
[Signal]
public delegate void SelectedEventHandler(ControlPlanet planet);
[Signal]
public delegate void PointerEnteredEventHandler(ControlPlanet planet);
[Signal]
public delegate void PointerExitedEventHandler(ControlPlanet planet);
public bool IsSelected
{
get => GetNode<ColorRect>("%Selection").Visible;
set => GetNode<ColorRect>("%Selection").Visible = value;
}
void OnInputListenerInputEvent(Node _viewport, InputEvent inputEvent, int _shape_idx)
{
// TODO: handle touch appropriately (InputEventScreenTouch).
if (inputEvent is InputEventMouseButton e && e.ButtonIndex is MouseButton.Left && e.IsPressed())
EmitSignal(SignalName.Selected, this);
}
void OnInputListenerMouseEntered()
=> EmitSignal(SignalName.PointerEntered, this);
void OnInputListenerMouseExited()
=> EmitSignal(SignalName.PointerExited, this);
}

View file

@ -1,44 +0,0 @@
extends Node2D
class_name ControlPlanet
## The player currenly controlling this planet.
var player: Player :
set(value):
player = value
var color := Color.MAGENTA if player == null else player.color
%Selection.color = color
%PopulationLabel.modulate = color
%PopulationIcon.modulate = color
%PopulationIcon.texture = null if player == null else player.icon
## Number of units currently stationed at the planet.
@export var population: int :
set(value):
population = value
%PopulationLabel.text = "%s" % value
func _ready() -> void:
is_selected = false
@export_category("UI")
signal selected(planet: Planet)
signal pointer_entered(planet: Planet)
signal pointer_exited(planet: Planet)
var is_selected: bool :
get:
return %Selection.visible
set(value):
%Selection.visible = value
func _on_input_listener_input_event(_viewport: Node, event: InputEvent, _shape_idx: int) -> void:
# TODO: handle touch appropriately (InputEventScreenTouch).
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_pressed():
selected.emit(self)
func _on_input_listener_mouse_entered() -> void:
pointer_entered.emit(self)
func _on_input_listener_mouse_exited() -> void:
pointer_exited.emit(self)

View file

@ -1,7 +1,7 @@
[gd_scene load_steps=7 format=3 uid="uid://dq00mi6jwsa1f"]
[ext_resource type="PackedScene" uid="uid://dnerbdusnaofr" path="res://scenes/procedural_planet/procedural_planet.tscn" id="1_8mf3x"]
[ext_resource type="Script" path="res://scenes/control_planet/control_planet.gd" id="1_e208c"]
[ext_resource type="Script" path="res://scenes/control_planet/ControlPlanet.cs" id="1_gbjyd"]
[ext_resource type="Shader" path="res://scenes/control_planet/planet_selection.gdshader" id="2_bl05u"]
[ext_resource type="Texture2D" uid="uid://1xh0qil16bkh" path="res://icons/character.svg" id="2_kglxp"]
@ -13,7 +13,7 @@ shader = ExtResource("2_bl05u")
shader_parameter/size = 100.0
[node name="Planet" type="Node2D"]
script = ExtResource("1_e208c")
script = ExtResource("1_gbjyd")
[node name="InputListener" type="Area2D" parent="."]
monitoring = false
@ -72,7 +72,7 @@ mouse_filter = 2
texture = ExtResource("2_kglxp")
stretch_mode = 3
[connection signal="input_event" from="InputListener" to="." method="_on_input_listener_input_event"]
[connection signal="mouse_entered" from="InputListener" to="." method="_on_input_listener_mouse_entered"]
[connection signal="mouse_exited" from="InputListener" to="." method="_on_input_listener_mouse_exited"]
[connection signal="input_event" from="InputListener" to="." method="OnInputListenerInputEvent"]
[connection signal="mouse_entered" from="InputListener" to="." method="OnInputListenerMouseEntered"]
[connection signal="mouse_exited" from="InputListener" to="." method="OnInputListenerMouseExited"]
[connection signal="gui_input" from="Selection" to="." method="_on_selection_gui_input"]

View file

@ -0,0 +1,119 @@
using Godot;
namespace SpaceCapture;
public partial class ProceduralPlanet : Node2D
{
[ExportGroup("Planet")]
/// <summary>
/// Size of the planet in pixels.
/// </summary>
/// <value></value>
[Export(PropertyHint.Range, "30,500,.001,or_greater,or_less")]
public float Size
{
get => Scale.X;
set
{
Scale = value * Vector2.One;
((ShaderMaterial)Material).SetShaderParameter("size", value);
}
}
/// <summary>
/// Rotation speed of the planet in rad/sec.
/// </summary>
/// <value></value>
[Export(PropertyHint.Range, "0,1,.001,or_greater,or_less")]
public float RotationSpeed
{
get => (float)((ShaderMaterial)Material).GetShaderParameter("rotationSpeed").AsDouble();
set => ((ShaderMaterial)Material).SetShaderParameter("rotationSpeed", value);
}
[ExportGroup("Weather")]
/// <summary>
/// Size of clouds between 0 (no clouds) and 1 (covered in clouds completely).
/// </summary>
/// <value></value>
[Export(PropertyHint.Range, "0,1,.001,or_greater,or_less")]
public float CloudsSize
{
get => (float)((ShaderMaterial)Material).GetShaderParameter("cloudsSize").AsDouble();
set => ((ShaderMaterial)Material).SetShaderParameter("cloudsSize", value);
}
/// <summary>
/// Density of clouds between 0 (very thin) and 1 (very thick).
/// </summary>
/// <value></value>
[Export(PropertyHint.Range, "0,1,.001,or_greater,or_less")]
public float CloudDensity
{
get => (float)((ShaderMaterial)Material).GetShaderParameter("cloudsDensity").AsDouble();
set => ((ShaderMaterial)Material).SetShaderParameter("cloudsDensity", value);
}
/// <summary>
/// How often clouds change shape.
/// </summary>
/// <value></value>
[Export(PropertyHint.Range, "0,1,.001,or_greater")]
public float CloudTurbulence
{
get => (float)((ShaderMaterial)Material).GetShaderParameter("cloudsTurbulence").AsDouble();
set => ((ShaderMaterial)Material).SetShaderParameter("cloudsTurbulence", value);
}
/// <summary>
/// Wind speed in rad/sec.
/// </summary>
/// <value></value>
[Export(PropertyHint.Range, "0,1,.001,or_greater")]
public float WindSpeed
{
get => (float)((ShaderMaterial)Material).GetShaderParameter("windSpeed").AsDouble();
set => ((ShaderMaterial)Material).SetShaderParameter("windSpeed", value);
}
[ExportGroup("Atmosphere")]
/// <summary>
/// Size of the atmosphere halo around the planet.
/// </summary>
/// <value></value>
[Export(PropertyHint.Range, "0,1,.001")]
public float AtmosphereSize
{
get => (float)((ShaderMaterial)Material).GetShaderParameter("atmosphereSize").AsDouble();
set => ((ShaderMaterial)Material).SetShaderParameter("atmosphereSize", value);
}
/// <summary>
/// Size of the atmosphere halo around the planet.
/// </summary>
/// <value></value>
[Export(PropertyHint.Range, "0,1,.001")]
public Color AtmosphereColor
{
get => ((ShaderMaterial)Material).GetShaderParameter("atmosphereColor").AsColor();
set => ((ShaderMaterial)Material).SetShaderParameter("atmosphereColor", value);
}
public override void _Ready()
{
Size = 100f;
RotationSpeed = .05f;
CloudsSize = .05f;
CloudDensity = .22f;
CloudTurbulence = .01f;
WindSpeed = .22f;
AtmosphereSize = .3f;
AtmosphereColor = new(0f, .3f, 1f, .3f);
}
}

View file

@ -1,48 +0,0 @@
extends Node2D
@export_group("Planet")
## Size of the planet in pixels.
@export_range(30, 500, .001, "or_greater", "or_less") var size: float = 100 :
set(value):
scale = value * Vector2.ONE
material.set_shader_parameter('size', value)
# Rotation speed of the planet in rad/sec.
@export_range(0, 1, .001, "or_greater", "or_less") var rotation_speed: float = 0.05 :
set(value):
material.set_shader_parameter('rotationSpeed', value)
@export_group("Weather")
# Size of clouds between 0 (no clouds) and 1 (covered in clouds completely).
@export_range(0, 1, .001) var clouds_size: float = 0.05 :
set(value):
material.set_shader_parameter('cloudsSize', value)
# Density of clouds between 0 (very thin) and 1 (very thick).
@export_range(0, 1, .001) var cloud_density: float = 0.22 :
set(value):
material.set_shader_parameter('cloudsDensity', value)
# How often clouds change shape.
@export_range(0, 1, .001, "or_greater") var cloud_turbulence: float = 0.01 :
set(value):
material.set_shader_parameter('cloudsTurbulence', value)
# Wind speed in rad/sec.
@export_range(0, 1, .001, "or_greater") var wind_speed: float = 0.22 :
set(value):
material.set_shader_parameter('windSpeed', value)
@export_group("Atmosphere")
## Size of the atmosphere halo around the planet.
@export_range(0, 1, .001) var atmosphere_size: float = .3 :
set(value):
material.set_shader_parameter('atmosphereSize', value)
## Color of the atmosphere halo around the planet.
@export var atmosphere_color: Color = Color(0., .3, 1., .3) :
set(value):
material.set_shader_parameter('atmosphereColor', value)

View file

@ -1,6 +1,6 @@
[gd_scene load_steps=8 format=3 uid="uid://dnerbdusnaofr"]
[ext_resource type="Script" path="res://scenes/procedural_planet/procedural_planet.gd" id="1_2swpa"]
[ext_resource type="Script" path="res://scenes/procedural_planet/ProceduralPlanet.cs" id="3_dyl4c"]
[ext_resource type="Shader" path="res://scenes/procedural_planet/procedural_planet.gdshader" id="1_tl7a6"]
[ext_resource type="Texture2D" uid="uid://dr8pb2ip0k08j" path="res://scenes/procedural_planet/albedo.png" id="3_pc1s8"]
@ -35,4 +35,4 @@ size = Vector2(2, 2)
material = SubResource("ShaderMaterial_m4mhh")
scale = Vector2(100, 100)
texture = SubResource("PlaceholderTexture2D_ipvyw")
script = ExtResource("1_2swpa")
script = ExtResource("3_dyl4c")

View file

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
using Godot;
namespace SpaceCapture;
public partial class ShipsFleet : Node2D
{
const int RankSize = 7;
const float RanksDistance = 12f;
const float ShoulderDistance = 8f;
const float DisperseAtDistance = 100f;
int _count;
GameState _game;
[Export]
public int DepartedAt { get; set; } // Time.GetTicksMsec()
[Export]
public int ArrivesAt { get; set; } // Time.GetTicksMsec()
[Export]
public Vector2 From { get; set; }
[Export]
public Vector2 To { get; set; }
[Export]
public Player Player { get; set; }
[Export]
public int Count
{
get => _count;
set
{
_count = value;
UpdateShipsCount();
}
}
[Export]
public Trail Trail { get; set; }
public void RegisterGame(GameState game)
{
_game = game;
_game.GameTicked += UpdatePosition;
UpdatePosition(game.CurrentTick);
}
public override void _ExitTree()
{
if (_game is not null)
{
_game.GameTicked -= UpdatePosition;
_game = null;
}
}
#region Graphics
List<Ship> _ships = [];
[Export]
public PackedScene ShipTemplate { get; set; }
void UpdateShipsCount()
{
int diff = Count - _ships.Count;
for (int i = 0; i < diff; i++)
{
Ship ship = ShipTemplate.Instantiate<Ship>();
ship.Color = Player.Color;
AddChild(ship);
ship.Position = From;
ship.Rotation = Random.Shared.NextSingle() * (2f * MathF.PI);
ship.Velocity = (Random.Shared.NextSingle() * 60f + 60f) * Vector2.FromAngle(ship.Rotation);
_ships.Add(ship);
}
for (int i = 0; i < -diff; i++)
{
RemoveChild(_ships[^1]);
_ships.RemoveAt(_ships.Count - 1);
}
}
bool _arrived = false;
void UpdatePosition(int tick)
{
if (!_arrived)
{
int clampedTick = Math.Clamp(tick, DepartedAt, ArrivesAt);
float progress = (clampedTick - DepartedAt) / (float)(ArrivesAt - DepartedAt);
Vector2 fleetPosition = From.Lerp(To, Math.Clamp(progress, 0f, 1f));
float dispersiveness = Math.Clamp(MathF.Sqrt(Math.Min(fleetPosition.DistanceSquaredTo(From), fleetPosition.DistanceSquaredTo(To))) / DisperseAtDistance, 0f, 1f);
float angle = From.AngleToPoint(To);
int ranksCount = Mathf.CeilToInt((float)_ships.Count / RankSize);
for (int i = 0; i < _ships.Count; i++)
{
int rank = Mathf.FloorToInt((float)i / RankSize);
int positionInRank = i % RankSize;
var shipsInRank = rank != ranksCount - 1 ? RankSize : (_ships.Count - (ranksCount - 1) * RankSize);
Vector2 assignedPosition = new(rank * -RanksDistance - Math.Abs(positionInRank - shipsInRank / 2f) * 5f, (float)(positionInRank - (shipsInRank - 1) * .5f) * ShoulderDistance);
_ships[i].TargetPosition = fleetPosition + Vector2.Zero.Lerp(assignedPosition.Rotated(angle) + Vector2.FromAngle(MathF.Sin(i + tick / 10f)) * 2f, dispersiveness);
}
if (tick >= ArrivesAt)
{
_arrived = true;
Trail.ShowTrail = false;
// TODO: fade out ships.
GetTree().CreateTimer(5.0).Timeout += QueueFree;
}
}
}
#endregion
}

View file

@ -0,0 +1,58 @@
using System;
using Godot;
namespace SpaceCapture;
public partial class Ship : Node2D
{
const float Acceleration = 100f;
const float RotationSpeed = 3f;
const float MaxSpeed = 100f;
Color _color;
[Export]
public Color Color
{
get => _color;
set
{
_color = value;
UpdateColor(_color);
}
}
[Export]
public Vector2 TargetPosition { get; set; }
[Export]
public Vector2 Velocity { get; set; }
public override void _Process(double delta)
{
Vector2 targetVelocity = (TargetPosition - GlobalPosition).Normalized() * MaxSpeed;
Vector2 adjustedTargetVelocity = targetVelocity - Velocity;
Vector2 acceleration = adjustedTargetVelocity == Vector2.Zero ? Vector2.Zero : adjustedTargetVelocity.Normalized() * Acceleration;
float decelerateAtDistance = Velocity.LengthSquared() / (2f * Acceleration);
float distanceSquared = GlobalPosition.DistanceSquaredTo(TargetPosition);
bool slow_down = distanceSquared < decelerateAtDistance*decelerateAtDistance;
if (slow_down)
acceleration = Velocity.Normalized() * -Acceleration;
Velocity += acceleration * (float)delta;
float speedSquared = Velocity.LengthSquared();
if (speedSquared > MaxSpeed*MaxSpeed)
Velocity = Velocity / MathF.Sqrt(speedSquared) * MaxSpeed;
GlobalPosition += Velocity * (float)delta;
Rotation = Mathf.RotateToward(Rotation, GlobalPosition.AngleToPoint(TargetPosition), Math.Min(1f, (float)delta) * RotationSpeed);
}
void UpdateColor(Color shipColor)
{
((ShaderMaterial)GetNode<Line2D>("%LineTrail").Material).SetShaderParameter("color", shipColor);
}
}

View file

@ -1,39 +0,0 @@
class_name Ship
extends Node2D
const ACCELERATION = 100.
const ROTATION_SPEED = 3.
const MAX_SPEED = 100.
@export var color: Color :
set(value):
color = value
_update_color(color)
@export var target_position: Vector2
var velocity: Vector2
func _process(delta: float) -> void:
var target_velocity := (target_position - global_position).normalized() * MAX_SPEED
var adjusted_target_velocity := target_velocity - velocity
var acceleration := Vector2.ZERO if adjusted_target_velocity == Vector2.ZERO else adjusted_target_velocity.normalized() * ACCELERATION
var decelerate_at_distance := velocity.length_squared() / (2. * ACCELERATION)
var distance_squared := global_position.distance_squared_to(target_position)
var slow_down := distance_squared < decelerate_at_distance*decelerate_at_distance
if slow_down:
acceleration = velocity.normalized() * -ACCELERATION
velocity += acceleration * delta
var speed_squared := velocity.length_squared()
if speed_squared > MAX_SPEED*MAX_SPEED:
velocity = velocity / sqrt(speed_squared) * MAX_SPEED
global_position += velocity * delta
rotation = rotate_toward(rotation, global_position.angle_to_point(target_position), min(1., delta) * ROTATION_SPEED)
func _update_color(ship_color: Color) -> void:
%LineTrail.material.set_shader_parameter('color', ship_color)

View file

@ -1,6 +1,6 @@
[gd_scene load_steps=7 format=3 uid="uid://ctybb4niolni3"]
[ext_resource type="Script" path="res://scenes/ships_fleet/ship/ship.gd" id="1_h6q7s"]
[ext_resource type="Script" path="res://scenes/ships_fleet/ship/Ship.cs" id="1_qapu0"]
[ext_resource type="Shader" path="res://scenes/ships_fleet/ship/trail.gdshader" id="2_vgm83"]
[ext_resource type="Texture2D" uid="uid://dp0ugxgq8stt1" path="res://scenes/ships_fleet/ship/ship_trail.png" id="3_hk6vp"]
[ext_resource type="Script" path="res://scenes/line_trail/line_trail_2d.gd" id="4_wo8wd"]
@ -12,7 +12,7 @@ shader = ExtResource("2_vgm83")
shader_parameter/color = Color(1, 0, 1, 1)
[node name="Ship" type="Node2D"]
script = ExtResource("1_h6q7s")
script = ExtResource("1_qapu0")
[node name="LineTrail" type="Line2D" parent="." node_paths=PackedStringArray("tracking")]
unique_name_in_owner = true

View file

@ -1,87 +0,0 @@
extends Node2D
class_name ShipsFleet
const RANK_SIZE = 7
const RANKS_DISTANCE = 12.
const SHOULDER_DISTANCE = 8.
const DISPERSE_AT_DISTANCE = 100.
@export var departed_at: int # Time.get_ticks_msec()
@export var arrives_at: int # Time.get_ticks_msec()
@export var from: Vector2
@export var to: Vector2
@export var player: Player
@export var count: int :
set(value):
count = value
_update_ships_count()
@export var trail: Trail
var _game: GameState
func register_game(game: GameState) -> void:
_game = game
_game.game_ticked.connect(_update_position)
_update_position(game.current_tick)
func _exit_tree() -> void:
if _game != null:
_game.game_ticked.disconnect(_update_position)
_game = null
#region Graphics
var _ships: Array[Ship] = []
@export var ship_template: PackedScene
func _update_ships_count() -> void:
var diff = count - _ships.size()
for i in range(diff):
var ship: Ship = ship_template.instantiate()
ship.color = player.color
add_child(ship)
ship.position = from
ship.rotation = randf_range(0, 2 * PI)
ship.velocity = randf_range(60., 120.) * Vector2.from_angle(ship.rotation)
_ships.push_back(ship)
for i in range(-diff):
var ship: Ship = _ships.pop_back()
remove_child(ship)
var _arrived := false
func _update_position(tick: int) -> void:
if not _arrived:
var clamped_tick := clampi(tick, departed_at, arrives_at)
var progress := float(clamped_tick - departed_at) / float(arrives_at - departed_at)
var fleet_position: Vector2 = lerp(from, to, clampf(progress, 0., 1.))
var dispersiveness := clampf(sqrt(minf(fleet_position.distance_squared_to(from), fleet_position.distance_squared_to(to))) / DISPERSE_AT_DISTANCE, 0., 1.)
var angle = from.angle_to_point(to)
var ranks_count := ceili(float(_ships.size()) / RANK_SIZE)
for i in range(_ships.size()):
var rank := floori(float(i) / RANK_SIZE)
var position_in_rank := i % RANK_SIZE
var ships_in_rank := RANK_SIZE if rank != ranks_count - 1 else (_ships.size() - (ranks_count - 1) * RANK_SIZE)
var assigned_position := Vector2(rank * -RANKS_DISTANCE - abs(position_in_rank - ships_in_rank / 2.) * 5., float(position_in_rank - (ships_in_rank - 1) * .5) * SHOULDER_DISTANCE)
_ships[i].target_position = fleet_position + lerp(Vector2.ZERO, assigned_position.rotated(angle) + Vector2.from_angle(sin(i + tick / 10.)) * 2., dispersiveness)
if tick >= arrives_at:
_arrived = true
trail.show_trail = false
# TODO: fade out ships.
await get_tree().create_timer(5.).timeout
queue_free()
#endregion

View file

@ -1,8 +1,8 @@
[gd_scene load_steps=3 format=3 uid="uid://cpffyaoh8x5bp"]
[ext_resource type="Script" path="res://scenes/ships_fleet/ships_fleet.gd" id="1_qfy0m"]
[ext_resource type="Script" path="res://scenes/ships_fleet/ShipsFleet.cs" id="1_isl3a"]
[ext_resource type="PackedScene" uid="uid://ctybb4niolni3" path="res://scenes/ships_fleet/ship/ship.tscn" id="2_6eafk"]
[node name="ShipsFleet" type="Node2D"]
script = ExtResource("1_qfy0m")
ship_template = ExtResource("2_6eafk")
script = ExtResource("1_isl3a")
ShipTemplate = ExtResource("2_6eafk")

View file

@ -0,0 +1,18 @@
using Godot;
namespace SpaceCapture;
[GlobalClass]
public partial class SceneTemplates : Resource
{
public static SceneTemplates Scenes { get; } = ResourceLoader.Load<SceneTemplates>("res://scenes/templates/scene_templates.tres");
[Export]
public PackedScene ControlPlanet { get; set; }
[Export]
public PackedScene Trail { get; set; }
[Export]
public PackedScene ShipsFleet { get; set; }
}

View file

@ -1,10 +0,0 @@
class_name SceneTemplates
extends Resource
static var scenes = preload("res://scenes/templates/scene_templates.tres")
@export var control_planet: PackedScene
@export var trail: PackedScene
@export var ships_fleet: PackedScene

View file

@ -1,12 +1,12 @@
[gd_resource type="Resource" script_class="SceneTemplates" load_steps=5 format=3 uid="uid://cp0dudi0f5r62"]
[gd_resource type="Resource" load_steps=5 format=3 uid="uid://cp0dudi0f5r62"]
[ext_resource type="PackedScene" uid="uid://dq00mi6jwsa1f" path="res://scenes/control_planet/control_planet.tscn" id="1_3rbtf"]
[ext_resource type="Script" path="res://scenes/templates/scene_templates.gd" id="1_yvn3m"]
[ext_resource type="PackedScene" uid="uid://cpffyaoh8x5bp" path="res://scenes/ships_fleet/ships_fleet.tscn" id="3_x17ul"]
[ext_resource type="PackedScene" uid="uid://ckfk1xgxfk1c3" path="res://scenes/trail/trail.tscn" id="4_hiuw1"]
[ext_resource type="PackedScene" uid="uid://dq00mi6jwsa1f" path="res://scenes/control_planet/control_planet.tscn" id="1_a0tpk"]
[ext_resource type="Script" path="res://scenes/templates/SceneTemplates.cs" id="1_ybx2m"]
[ext_resource type="PackedScene" uid="uid://cpffyaoh8x5bp" path="res://scenes/ships_fleet/ships_fleet.tscn" id="2_ocr7v"]
[ext_resource type="PackedScene" uid="uid://ckfk1xgxfk1c3" path="res://scenes/trail/trail.tscn" id="3_gygkc"]
[resource]
script = ExtResource("1_yvn3m")
control_planet = ExtResource("1_3rbtf")
trail = ExtResource("4_hiuw1")
ships_fleet = ExtResource("3_x17ul")
script = ExtResource("1_ybx2m")
ControlPlanet = ExtResource("1_a0tpk")
Trail = ExtResource("3_gygkc")
ShipsFleet = ExtResource("2_ocr7v")

100
scenes/trail/Trail.cs Normal file
View file

@ -0,0 +1,100 @@
using Godot;
namespace SpaceCapture;
public partial class Trail : Sprite2D
{
static float s_invTextureWidth = float.NaN;
Color _color;
bool _showTrail = true;
Vector2 _startPosition;
Vector2 _endPosition;
float _opacity = 0f;
[Export]
public Color Color
{
get => _color;
set
{
_color = value;
UpdateColor(_color);
}
}
[Export]
public bool ShowTrail
{
get => _showTrail;
set
{
_showTrail = value;
Visible = true;
}
}
[Export]
public bool AutoFree { get; set; }
[Export]
public Vector2 StartPosition
{
get => _startPosition;
set
{
_startPosition = value;
UpdateTransform(value, _endPosition);
}
}
[Export]
public Vector2 EndPosition
{
get => _endPosition;
set
{
_endPosition = value;
UpdateTransform(_startPosition, value);
}
}
void UpdateTransform(Vector2 start, Vector2 end)
{
GlobalPosition = (start + end) * .5f;
LookAt(end);
if (float.IsNaN(s_invTextureWidth))
s_invTextureWidth = 1f / Texture.GetWidth();
Scale = new Vector2((end - start).Length() * s_invTextureWidth, 1f);
}
void UpdateColor(Color trailColor)
{
Color c = new(trailColor.R, trailColor.G, trailColor.B, trailColor.A * _opacity);
((ShaderMaterial)Material).SetShaderParameter("color", c);
}
public override void _Process(double delta)
{
float prevOpacity = _opacity;
_opacity = Utils.Damp(_opacity, ShowTrail ? 1f : 0f, 1e-4f, delta);
if (_opacity != prevOpacity)
{
if (_opacity < .01f)
{
_opacity = 0f;
Visible = false;
if (AutoFree)
QueueFree();
}
else if (_opacity > .99f)
{
_opacity = 1f;
}
UpdateColor(Color);
}
}
}

View file

@ -1,55 +0,0 @@
extends Sprite2D
class_name Trail
static var _inv_texture_width := NAN
@export var color: Color :
set(value):
color = value
_update_color(color)
@export var show_trail: bool = true :
set(value):
show_trail = value
visible = true
@export var auto_free: bool
var start_position: Vector2 :
set(value):
start_position = value
_update_transform(value, end_position)
var end_position: Vector2 :
set(value):
end_position = value
_update_transform(start_position, value)
var _opacity := 0.
func _update_transform(start: Vector2, end: Vector2) -> void:
global_position = (start + end) * .5
look_at(end)
if is_nan(_inv_texture_width):
_inv_texture_width = 1. / texture.get_width()
scale = Vector2((end - start).length() * _inv_texture_width, 1.)
func _update_color(trail_color: Color) -> void:
var c := Color(trail_color.r, trail_color.g, trail_color.b, trail_color.a * _opacity)
material.set_shader_parameter('color', c)
func _process(delta: float) -> void:
var prev_opacity := _opacity
_opacity = Utils.damp(_opacity, 1. if show_trail else 0., 1e-4, delta)
if _opacity != prev_opacity:
if _opacity < .01:
_opacity = 0.
visible = false
if auto_free:
queue_free()
elif _opacity > .99:
_opacity = 1.
_update_color(color)

View file

@ -2,7 +2,7 @@
[ext_resource type="Shader" path="res://scenes/trail/trail.gdshader" id="1_fjdrv"]
[ext_resource type="Texture2D" uid="uid://qb1fvyepkup2" path="res://scenes/trail/trail.png" id="2_nw1t2"]
[ext_resource type="Script" path="res://scenes/trail/trail.gd" id="3_cnvkj"]
[ext_resource type="Script" path="res://scenes/trail/Trail.cs" id="3_hnvmo"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_f7lnm"]
resource_local_to_scene = true
@ -14,4 +14,4 @@ texture_repeat = 2
material = SubResource("ShaderMaterial_f7lnm")
scale = Vector2(10, 1)
texture = ExtResource("2_nw1t2")
script = ExtResource("3_cnvkj")
script = ExtResource("3_hnvmo")

151
scripts/GameLogic.cs Normal file
View file

@ -0,0 +1,151 @@
using System.Collections.Generic;
using Godot;
using Godot.Collections;
namespace SpaceCapture;
public partial class GameLogic : Node
{
const float LogicTicksPerSecond = 20f;
const float LogicSecondsPerTick = 1f / LogicTicksPerSecond;
GameState _game;
List<ControlPlanet> _planetControls = [];
/// <summary>
/// Container for planets.
/// </summary>
/// <value></value>
[Export]
public Node2D PlanetsContainer { get; set; }
/// <summary>
/// Container for trails.
/// </summary>
/// <value></value>
[Export]
public Node2D TrailsContainer { get; set; }
/// <summary>
/// Container for fleets.
/// </summary>
/// <value></value>
[Export]
public Node2D FleetsContainer { get; set; }
/// <summary>
/// UI to interact with planets.
/// </summary>
/// <value></value>
[Export]
public PlanetsUI PlanetsUI { get; set; }
/// <summary>
/// Game players.
/// </summary>
/// <value></value>
[Export]
public Array<Player> Players { get; set; }
/// <summary>
/// Game planets.
/// </summary>
/// <value></value>
[Export]
public Array<Planet> Planets { get; set; }
double _extraTime;
public override void _Ready()
{
// New game.
_game = new();
// Add players.
for (int i = 0; i < Players.Count; i++)
_game.AddPlayer();
// Add planets.
for (int i = 0; i < Planets.Count; i++)
{
// Data.
GameState.PlanetData data = new();
data.Position = (Vector2I)Planets[i].Location;
data.GrowEveryTicks = 10;
data.PlayerId = i < Players.Count - 1 ? i + 1 : 0;
data.Population = 0;
_game.AddPlanet(data);
// UI control.
ControlPlanet control = SceneTemplates.Scenes.ControlPlanet.Instantiate<ControlPlanet>();
control.Position = Planets[i].Location;
control.Player = Players[data.PlayerId];
control.Population = data.Population;
_planetControls.Add(control);
PlanetsContainer.AddChild(control);
PlanetsUI.RegisterPlanet(control);
}
// Register UI signals.
_game.PlanetPlayerChanged += SetPlanetPlayer;
_game.PlanetPopulationChanged += SetPlanetPopulation;
_game.FleetDispatched += ShowDispatchedFleet;
PlanetsUI.FleetDispatched += DispatchFleet;
}
public override void _Process(double delta)
{
_extraTime += delta;
while (_extraTime > LogicSecondsPerTick)
{
_extraTime -= LogicSecondsPerTick;
_game.Tick();
}
}
#region Graphics
void SetPlanetPlayer(int planetId, int playerId)
=> _planetControls[planetId].Player = Players[playerId];
void SetPlanetPopulation(int planetId, int population)
=> _planetControls[planetId].Population = population;
void ShowDispatchedFleet(int fromPlanetId, int toPlanetId, int count, int playerId, int departedAtTick, int arrivesAtTick)
{
Trail trail = SceneTemplates.Scenes.Trail.Instantiate<Trail>();
trail.StartPosition = Planets[fromPlanetId].Location;
trail.EndPosition = Planets[toPlanetId].Location;
trail.Color = Players[playerId].Color;
trail.AutoFree = true;
TrailsContainer.AddChild(trail);
ShipsFleet fleet = SceneTemplates.Scenes.ShipsFleet.Instantiate<ShipsFleet>();
fleet.DepartedAt = departedAtTick;
fleet.ArrivesAt = arrivesAtTick;
fleet.From = Planets[fromPlanetId].Location;
fleet.To = Planets[toPlanetId].Location;
fleet.Player = Players[playerId];
fleet.Count = count;
fleet.Trail = trail;
FleetsContainer.AddChild(fleet);
fleet.RegisterGame(_game);
}
#endregion
#region Commands
void DispatchFleet(ControlPlanet from, ControlPlanet to)
{
int playerId = Players.IndexOf(from.Player);
int fromPlanetId = _planetControls.IndexOf(from);
int toPlanetId = _planetControls.IndexOf(to);
int maxCount = Mathf.CeilToInt(from.Population / 2f);
_game.DispatchFleet(playerId, fromPlanetId, toPlanetId, maxCount);
}
#endregion
}

220
scripts/GameState.cs Normal file
View file

@ -0,0 +1,220 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Godot;
namespace SpaceCapture;
public partial class GameState : Resource
{
public class PlayerData
{
public List<Command> Commands { get; set; } = [];
}
public class PlanetData
{
public Vector2I Position { get; set; }
public int GrowEveryTicks { get; set; }
public int PlayerId { get; set; }
public int Population { get; set; }
}
public class FleetData
{
public int Count { get; set; }
public int PlayerId { get; set; }
public int ToPlanetId { get; set; }
public int ArrivesAtTick { get; set; }
}
public enum CommandType
{
DispatchFleet,
}
public abstract class Command(CommandType type)
{
public CommandType Type => type;
public int WaitTick { get; set; }
}
public class DispatchFleetCommand() : Command(CommandType.DispatchFleet)
{
public int FromPlanetId { get; set; }
public int ToPlanetId { get; set; }
public int MaxCount { get; set; }
}
const int NeutralPlayerId = 0;
const int FleetTakeoffPlusLandingTime = 20; // ticks
const int FleetSpeed = 2; // pixels/tick
[Signal]
public delegate void GameTickedEventHandler(int tick);
[Signal]
public delegate void PlanetPopulationChangedEventHandler(int planetId, int population);
[Signal]
public delegate void PlanetPlayerChangedEventHandler(int planetId, int playerId);
[Signal]
public delegate void FleetDispatchedEventHandler(int fromPlanetId, int toPlanetId, int count, int playerId, int departedAtTick, int arrivesAtTick);
List<PlayerData> _players = [];
List<PlanetData> _planets = [];
List<FleetData> _fleets = [];
public int CurrentTick { get; private set; }
/// <summary>
/// Add a player to the game.
/// <para>
/// The first player is expected to be the game master / neutral player,
/// who does not play and for whom the population never increases.
/// </para>
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public int AddPlayer(PlayerData data = null)
{
_players.Add(data ?? new());
return _players.Count - 1;
}
/// <summary>
/// Add a planet to the game.
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public int AddPlanet(PlanetData data = null)
{
_planets.Add(data ?? new());
return _planets.Count - 1;
}
/// <summary>
/// Advance game status by one tick.
/// </summary>
/// <returns></returns>
public void Tick()
{
CurrentTick++;
// Handle planets growth.
for (int planetId = 0; planetId < _planets.Count; planetId++)
{
PlanetData planet = _planets[planetId];
if (planet.PlayerId is not NeutralPlayerId && CurrentTick % planet.GrowEveryTicks is 0)
{
planet.Population++;
EmitSignal(SignalName.PlanetPopulationChanged, planetId, planet.Population);
}
}
// Handle player commands.
for (int player_id = 0; player_id < _players.Count; player_id++)
{
PlayerData player = _players[player_id];
if (player.Commands.Count > 0 && CurrentTick >= player.Commands[0].WaitTick)
{
Command command = player.Commands[^1];
player.Commands.RemoveAt(player.Commands.Count - 1);
switch (command.Type)
{
case CommandType.DispatchFleet:
HandleDispatchFleet(player_id, (DispatchFleetCommand)command);
break;
default:
Debug.Assert(false, $"Unknown command: {command}");
break;
}
}
}
// Handle fleets arrival.
while (_fleets.Count > 0 && CurrentTick >= _fleets[0].ArrivesAtTick)
{
FleetData fleet = _fleets[^1];
_fleets.RemoveAt(_fleets.Count - 1);
_handle_fleet_arrival(fleet);
}
EmitSignal(SignalName.GameTicked, CurrentTick);
}
/// <summary>
/// Dispatch a fleet.
/// </summary>
/// <param name="playerId"></param>
/// <param name="fromPlanetId"></param>
/// <param name="toPlanetId"></param>
/// <param name="maxCount"></param>
public void DispatchFleet(int playerId, int fromPlanetId, int toPlanetId, int maxCount)
=> IssueCommand(playerId, new DispatchFleetCommand() {
FromPlanetId = fromPlanetId,
ToPlanetId = toPlanetId,
MaxCount = maxCount,
});
void IssueCommand(int player_id, Command command)
{
List<Command> commands = _players[player_id].Commands;
int index = commands.Select(x => x.WaitTick).ToList().BinarySearch(command.WaitTick);
if (index < 0)
index = ~index;
commands.Insert(index, command);
}
void HandleDispatchFleet(int playerId, DispatchFleetCommand dispatch)
{
PlanetData from = _planets[dispatch.FromPlanetId];
if (from.PlayerId == playerId)
{
PlanetData to = _planets[dispatch.ToPlanetId];
int count = Math.Min(dispatch.MaxCount, from.Population);
from.Population -= count;
EmitSignal(SignalName.PlanetPopulationChanged, dispatch.FromPlanetId, from.Population);
FleetData fleet = new() {
Count = count,
PlayerId = playerId,
ToPlanetId = dispatch.ToPlanetId,
ArrivesAtTick = CurrentTick + FleetTakeoffPlusLandingTime + Mathf.CeilToInt(from.Position.DistanceTo(to.Position) / FleetSpeed),
};
int index = _fleets.Select(x => x.ArrivesAtTick).ToList().BinarySearch(fleet.ArrivesAtTick);
if (index < 0)
index = ~index;
_fleets.Insert(index, fleet);
EmitSignal(SignalName.FleetDispatched, dispatch.FromPlanetId, fleet.ToPlanetId, fleet.Count, fleet.PlayerId, CurrentTick, fleet.ArrivesAtTick);
}
}
void _handle_fleet_arrival(FleetData fleet)
{
PlanetData to = _planets[fleet.ToPlanetId];
if (fleet.PlayerId == to.PlayerId)
{
to.Population += fleet.Count;
}
else
{
to.Population -= fleet.Count;
if (to.Population <= 0)
{
to.PlayerId = fleet.PlayerId;
to.Population = Mathf.Abs(to.Population);
EmitSignal(SignalName.PlanetPlayerChanged, fleet.ToPlanetId, to.PlayerId);
}
EmitSignal(SignalName.PlanetPopulationChanged, fleet.ToPlanetId, to.Population);
}
}
}

18
scripts/Planet.cs Normal file
View file

@ -0,0 +1,18 @@
using Godot;
namespace SpaceCapture;
[GlobalClass]
public partial class Planet : Resource
{
public enum PlanetType
{
Terrestrial,
}
[Export]
public Vector2 Location { get; set; }
[Export]
public PlanetType Type { get; set; }
}

17
scripts/Player.cs Normal file
View file

@ -0,0 +1,17 @@
using Godot;
namespace SpaceCapture;
[GlobalClass]
public partial class Player : Resource
{
[Export]
public Color Color { get; set; } = Colors.Magenta;
[Export]
public Texture2D Icon { get; set; }
public virtual void ProcessGameTick(GameLogic game)
{
}
}

11
scripts/PlayerAI.cs Normal file
View file

@ -0,0 +1,11 @@
using Godot;
namespace SpaceCapture;
[GlobalClass]
public partial class PlayerAI : Player
{
public override void ProcessGameTick(GameLogic game)
{
}
}

11
scripts/PlayerLocal.cs Normal file
View file

@ -0,0 +1,11 @@
using Godot;
namespace SpaceCapture;
[GlobalClass]
public partial class PlayerLocal : Player
{
public override void ProcessGameTick(GameLogic game)
{
}
}

31
scripts/Utils.cs Normal file
View file

@ -0,0 +1,31 @@
using System;
using System.Numerics;
using Godot;
namespace SpaceCapture;
public static class Utils
{
/// <summary>
/// Time-independent damping with exponential-decay.
/// </summary>
/// <param name="current">Current value.</param>
/// <param name="target">Target value.</param>
/// <param name="smoothing">The proportion of <paramref name="current"/> remaining after one second, the rest is <paramref name="target"/>.</param>
/// <param name="deltaTime">Seconds passed since last invocation.</param>
/// <typeparam name="T"></typeparam>
/// <returns></returns>
public static T Damp<T>(T current, T target, float smoothing, double deltaTime)
where T : IAdditionOperators<T, T, T>, ISubtractionOperators<T, T, T>, IMultiplyOperators<T, float, T>
=> Lerp(current, target, (float)(1d - Math.Pow(smoothing, deltaTime)));
/// <inheritdoc cref="Damp{T}(T, T, float, double)" />
public static Godot.Vector2 Damp(Godot.Vector2 current, Godot.Vector2 target, float smoothing, double deltaTime)
=> current.Lerp(target, (float)(1d - Math.Pow(smoothing, deltaTime)));
/// <inheritdoc cref="Mathf.Lerp(double, double, double)"/>
/// <typeparam name="T"></typeparam>
public static T Lerp<T>(T from, T to, float weight)
where T : IAdditionOperators<T, T, T>, ISubtractionOperators<T, T, T>, IMultiplyOperators<T, float, T>
=> from + (to - from) * weight;
}

View file

@ -1,107 +0,0 @@
class_name GameLogic
extends Node
const LOGIC_TICKS_PER_SECOND := 20.
const LOGIC_SECONDS_PER_TICK := 1. / LOGIC_TICKS_PER_SECOND
## Container for planets.
@export var planets_container: Node2D
## Container for trails.
@export var trails_container: Node2D
## Container for fleets.
@export var fleets_container: Node2D
## UI to interact with planets.
@export var planets_ui: PlanetsUI
## Game players.
@export var players: Array[Player]
## Game planets.
@export var planets: Array[Planet]
var _game: GameState
var _planet_controls: Array[ControlPlanet]
var _extra_time: float
func _ready() -> void:
# New game.
_game = GameState.new()
# Add players.
for i in range(players.size()):
_game.add_player()
# Add planets.
for i in range(planets.size()):
# Data.
var data := GameState.PlanetData.new()
data.position = Vector2i(planets[i].location)
data.grow_every_ticks = 10
data.player_id = (i + 1) if (i < players.size() - 1) else 0
data.population = 0
_game.add_planet(data)
# UI control.
var control: ControlPlanet = SceneTemplates.scenes.control_planet.instantiate()
control.position = planets[i].location
control.player = players[data.player_id]
control.population = data.population
_planet_controls.push_back(control)
planets_container.add_child(control)
planets_ui.register_planet(control)
# Register UI signals.
_game.planet_player_changed.connect(_set_planet_player)
_game.planet_population_changed.connect(_set_planet_population)
_game.fleet_dispatched.connect(_show_dispatched_fleet)
planets_ui.fleet_dispatched.connect(_dispatch_fleet)
func _process(delta: float) -> void:
_extra_time += delta
while _extra_time > LOGIC_SECONDS_PER_TICK:
_extra_time -= LOGIC_SECONDS_PER_TICK
_game.tick()
#region Graphics
func _set_planet_player(planet_id: int, player_id: int) -> void:
_planet_controls[planet_id].player = players[player_id]
func _set_planet_population(planet_id: int, population: int) -> void:
_planet_controls[planet_id].population = population
func _show_dispatched_fleet(from_planet_id: int, to_planet_id: int, count: int, player_id: int, departed_at_tick: int, arrives_at_tick: int) -> void:
var trail: Trail = SceneTemplates.scenes.trail.instantiate()
trail.start_position = planets[from_planet_id].location
trail.end_position = planets[to_planet_id].location
trail.color = players[player_id].color
trail.auto_free = true
trails_container.add_child(trail)
var fleet: ShipsFleet = SceneTemplates.scenes.ships_fleet.instantiate()
fleet.departed_at = departed_at_tick
fleet.arrives_at = arrives_at_tick
fleet.from = planets[from_planet_id].location
fleet.to = planets[to_planet_id].location
fleet.player = players[player_id]
fleet.count = count
fleet.trail = trail
fleets_container.add_child(fleet)
fleet.register_game(_game)
#endregion
#region Commands
func _dispatch_fleet(from: ControlPlanet, to: ControlPlanet) -> void:
var player_id := players.find(from.player)
var from_planet_id := _planet_controls.find(from)
var to_planet_id := _planet_controls.find(to)
var max_count: int = ceil(from.population / 2.)
_game.dispatch_fleet(player_id, from_planet_id, to_planet_id, max_count)
#endregion

View file

@ -1,132 +0,0 @@
class_name GameState
extends Resource
class PlayerData:
var commands: Array[Command] = []
class PlanetData:
var position: Vector2i
var grow_every_ticks: int
var player_id: int
var population: int
class FleetData:
var count: int
var player_id: int
var to_planet_id: int
var arrives_at_tick: int
enum CommandType {
DISPATCH_FLEET,
}
class Command:
var type: CommandType
var wait_tick: int
class DispatchFleedCommand extends Command:
var from_planet_id: int
var to_planet_id: int
var max_count: int
func _init():
type = CommandType.DISPATCH_FLEET
const NEUTRAL_PLAYER_ID := 0
const FLEET_TAKEOFF_PLUS_LANDING_TIME := 20 # ticks
const FLEET_SPEED := 2 # pixels/tick
signal game_ticked(tick: int)
signal planet_population_changed(planet_id: int, population: int)
signal planet_player_changed(planet_id: int, player_id: int)
signal fleet_dispatched(from_planet_id: int, to_planet_id: int, count: int, player_id: int, departed_at_tick: int, arrives_at_tick: int)
var _players: Array[PlayerData] = []
var _planets: Array[PlanetData] = []
var _fleets: Array[FleetData] = []
var current_tick: int
## Add a player to the game.
## The first player is expected to be the game master / neutral player,
## who does not play and for whom the population never increases.
func add_player(data := PlayerData.new()) -> int:
_players.push_back(data)
return _players.size() - 1
## Add a planet to the game.
func add_planet(data := PlanetData.new()) -> int:
_planets.push_back(data)
return _planets.size() - 1
## Advance game status by one tick.
func tick() -> void:
current_tick += 1
# Handle planets growth.
for planet_id in range(_planets.size()):
var planet := _planets[planet_id]
if planet.player_id != NEUTRAL_PLAYER_ID and current_tick % planet.grow_every_ticks == 0:
planet.population += 1
planet_population_changed.emit(planet_id, planet.population)
# Handle player commands.
for player_id in range(_players.size()):
var player := _players[player_id]
if player.commands.size() > 0 && current_tick >= player.commands[0].wait_tick:
var command: Command = player.commands.pop_back()
match command.type:
CommandType.DISPATCH_FLEET:
_handle_dispatch_fleet(player_id, command)
_:
assert(false, "Unknown command: %s" % [command])
# Handle fleets arrival.
while _fleets.size() > 0 && current_tick >= _fleets[0].arrives_at_tick:
var fleet: FleetData = _fleets.pop_back()
_handle_fleet_arrival(fleet)
game_ticked.emit(current_tick)
## Dispatch a fleet.
func dispatch_fleet(player_id: int, from_planet_id: int, to_planet_id: int, max_count: int) -> void:
var dispatch := DispatchFleedCommand.new()
dispatch.from_planet_id = from_planet_id
dispatch.to_planet_id = to_planet_id
dispatch.max_count = max_count
_issue_command(player_id, dispatch)
func _issue_command(player_id: int, command: Command) -> void:
var commands := _players[player_id].commands
var index := commands.map(func (x): return x.wait_tick).bsearch(command.wait_tick, false)
commands.insert(index, command)
func _handle_dispatch_fleet(player_id: int, dispatch: DispatchFleedCommand) -> void:
var from = _planets[dispatch.from_planet_id]
if from.player_id == player_id:
var to = _planets[dispatch.to_planet_id]
var count = min(dispatch.max_count, from.population)
from.population -= count
planet_population_changed.emit(dispatch.from_planet_id, from.population)
var fleet := FleetData.new()
fleet.count = count
fleet.player_id = player_id
fleet.to_planet_id = dispatch.to_planet_id
fleet.arrives_at_tick = current_tick + FLEET_TAKEOFF_PLUS_LANDING_TIME + ceili(from.position.distance_to(to.position) / FLEET_SPEED)
var index := _fleets.map(func (x): return x.arrives_at_tick).bsearch(fleet.arrives_at_tick, false)
_fleets.insert(index, fleet)
fleet_dispatched.emit(dispatch.from_planet_id, fleet.to_planet_id, fleet.count, fleet.player_id, current_tick, fleet.arrives_at_tick)
func _handle_fleet_arrival(fleet: FleetData) -> void:
var to = _planets[fleet.to_planet_id]
if fleet.player_id == to.player_id:
to.population += fleet.count
else:
to.population -= fleet.count
if to.population <= 0:
to.player_id = fleet.player_id
to.population = abs(to.population)
planet_player_changed.emit(fleet.to_planet_id, to.player_id)
planet_population_changed.emit(fleet.to_planet_id, to.population)

View file

@ -1,10 +0,0 @@
class_name Planet
extends Resource
enum PlanetType {
TERRESTRIAL,
}
@export var location: Vector2
@export var type: PlanetType

View file

@ -1,9 +0,0 @@
class_name Player
extends Resource
@export var color: Color = Color.MAGENTA
@export var icon: Texture2D
func process_game_tick(_game: GameLogic) -> void:
pass

View file

@ -1,5 +0,0 @@
class_name AIPlayer
extends Player
func process_game_tick(_game: GameLogic) -> void:
pass

View file

@ -1,5 +0,0 @@
class_name LocalPlayer
extends Player
func process_game_tick(_game: GameLogic) -> void:
pass

115
scripts/ui/PlanetsUI.cs Normal file
View file

@ -0,0 +1,115 @@
using Godot;
namespace SpaceCapture;
public partial class PlanetsUI : Control
{
Trail _trail;
/// <summary>
/// Container for trails.
/// </summary>
/// <value></value>
[Export]
public Node2D TrailsContainer { get; set; }
[Signal]
public delegate void FleetDispatchedEventHandler(ControlPlanet from, ControlPlanet to);
public void RegisterPlanet(ControlPlanet control)
{
control.Selected += OnPlanetSelected;
control.PointerEntered += OnPlanetPointerEntered;
control.PointerExited += OnPlanetPointerExited;
}
public override void _Ready()
{
_trail = SceneTemplates.Scenes.Trail.Instantiate<Trail>();
_trail.Visible = false;
TrailsContainer.AddChild(_trail);
}
public override void _Process(double delta)
{
UpdateTrail(delta, false);
}
public override void _UnhandledInput(InputEvent inputEvent)
{
if (inputEvent is InputEventMouse e)
HandleMouseInput(e);
}
#region Mouse input
Vector2 _lastCursorPosition;
void HandleMouseInput(InputEventMouse inputEvent)
{
if (inputEvent is InputEventMouseMotion e)
{
// The trail follows the cursor.
_lastCursorPosition = e.GlobalPosition;
}
if (inputEvent is InputEventMouseButton e2 && e2.ButtonIndex is MouseButton.Left && e2.IsReleased())
{
// The trail disappears when no longer dragging.
_draggingFromPlanet = false;
// Detect mouse released on a planet and spawn a fleet.
if (_selectedPlanet?.Player is PlayerLocal && _pointedPlanet is not null)
EmitSignal(SignalName.FleetDispatched, _selectedPlanet, _pointedPlanet);
}
}
#endregion
#region Planets
ControlPlanet _selectedPlanet;
ControlPlanet _pointedPlanet;
bool _draggingFromPlanet;
void OnPlanetSelected(ControlPlanet planet)
{
if (_selectedPlanet is not null)
_selectedPlanet.IsSelected = false;
_selectedPlanet = planet;
_selectedPlanet.IsSelected = true;
_draggingFromPlanet = true;
UpdateTrail(0.0, true);
}
void OnPlanetPointerEntered(ControlPlanet planet)
{
_pointedPlanet = planet;
}
void OnPlanetPointerExited(ControlPlanet planet)
{
if (_pointedPlanet == planet)
_pointedPlanet = null;
}
#endregion
#region Trail
void UpdateTrail(double delta, bool snapPosition)
{
_trail.ShowTrail = _draggingFromPlanet && _selectedPlanet is not null && _selectedPlanet.Player is PlayerLocal;
if (_trail.ShowTrail)
{
_trail.Color = _selectedPlanet.Player.Color;
var targetPosition = _pointedPlanet?.GlobalPosition ?? _lastCursorPosition;
_trail.StartPosition = _selectedPlanet.GlobalPosition;
_trail.EndPosition = snapPosition ? targetPosition : Utils.Damp(_trail.EndPosition, targetPosition, 1e-20f, delta);
}
}
#endregion
}

View file

@ -1,82 +0,0 @@
class_name PlanetsUI
extends Control
## Container for trails.
@export var trails_container: Node2D
var _trail: Trail
signal fleet_dispatched(from: ControlPlanet, to: ControlPlanet)
func register_planet(control: ControlPlanet) -> void:
control.selected.connect(_on_planet_selected)
control.pointer_entered.connect(_on_planet_pointer_entered)
control.pointer_exited.connect(_on_planet_pointer_exited)
func _ready() -> void:
_trail = SceneTemplates.scenes.trail.instantiate()
_trail.visible = false
trails_container.add_child(_trail)
func _process(delta: float) -> void:
_update_trail(delta, false)
func _unhandled_input(event: InputEvent) -> void:
if event is InputEventMouse:
_handle_mouse_input(event)
#region Mouse input
var _last_cursor_position: Vector2
func _handle_mouse_input(event: InputEventMouse) -> void:
if event is InputEventMouseMotion:
# The trail follows the cursor.
_last_cursor_position = event.global_position
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT and event.is_released():
# The trail disappears when no longer dragging.
_dragging_from_planet = false
# Detect mouse released on a planet and spawn a fleet.
if _selected_planet != null and _selected_planet.player is LocalPlayer and _pointed_planet != null:
fleet_dispatched.emit(_selected_planet, _pointed_planet)
#endregion
#region Planets
var _selected_planet: ControlPlanet
var _pointed_planet: ControlPlanet
var _dragging_from_planet: bool
func _on_planet_selected(planet: ControlPlanet) -> void:
if _selected_planet != null:
_selected_planet.is_selected = false
_selected_planet = planet
_selected_planet.is_selected = true
_dragging_from_planet = true
_update_trail(0., true)
func _on_planet_pointer_entered(planet: ControlPlanet) -> void:
_pointed_planet = planet
func _on_planet_pointer_exited(planet: ControlPlanet) -> void:
if _pointed_planet == planet:
_pointed_planet = null
#endregion
#region Trail
func _update_trail(delta: float, snap_position: bool) -> void:
_trail.show_trail = _dragging_from_planet and _selected_planet != null and _selected_planet.player is LocalPlayer
if _trail.show_trail:
_trail.color = _selected_planet.player.color
var target_position = _last_cursor_position if _pointed_planet == null else _pointed_planet.global_position
_trail.start_position = _selected_planet.global_position
_trail.end_position = target_position if snap_position else Utils.damp(_trail.end_position, target_position, 1e-20, delta)
#endregion

View file

@ -1,10 +0,0 @@
class_name Utils
## Time-independent damping with exponential-decay.
##
## [smoothing] is the proportion of [current] remaining after one second, the
## rest is [target]. [delta_time] represents the number of seconds passed since
## last invocation.
static func damp(current: Variant, target: Variant, smoothing: float, delta_time: float) -> Variant :
# https://www.rorydriscoll.com/2016/03/07/frame-rate-independent-damping-using-lerp/
return lerp(current, target, 1. - pow(smoothing, delta_time))