Compare commits

..

6 Commits

Author SHA1 Message Date
Cat Flynn beb59aebbb auto-tick, pausable clock #4 2022-12-08 19:37:33 +00:00
Cat Flynn 2b02ccfe33 add diffusion explanation to gdd 2022-12-08 18:43:21 +00:00
Cat Flynn fa1449b412 add basic heat transfer #4 2022-12-07 00:35:12 +00:00
Cat Flynn c013ab0057 generalise shader #4 2022-12-07 00:13:11 +00:00
Cat Flynn e32cd257fd add tick clock and tick counter UI #4 2022-12-07 00:13:11 +00:00
Cat Flynn 944ca8c3f2 extract struct to file 2022-12-07 00:13:11 +00:00
44 changed files with 251 additions and 1226 deletions

71
gdd.md
View File

@ -8,20 +8,25 @@
* Education * Education
* Supply chains * Supply chains
## Clock ## Critical Path
Ticks can be applied manually or they can be applied over time. ### Grid
Add a tick counter as a UI element.
## Grid
Add a 10x10 grid. Add a 10x10 grid.
Tiles are highlighted a different colour when the player hovers over them with the cursor. Tiles are highlighted a different colour when the player hovers over them with the cursor.
Tiles can be interacted with, which turns them another colour.
### Diffusion ### Diffusion
Tiles in the grid contain floating point values. Tiles in the grid contain floating point values.
These values propogate to neighbouring cells a tick happens. These values propogate to neighbouring cells a tick happens.
Ticks can be applied manually or they can be applied over time.
Add a tick counter as a UI element.
* [ ] understand different options for representing the rate of diffusion
Diffusing should use 'pull' operations; tiles should pull values from surrounding tiles.
This is an attempt to think forwards to parallelising computation of diffusiong - ideally using the GPU.
#### Diffusion Algorithm #### Diffusion Algorithm
@ -44,33 +49,7 @@ private float TransferHeat(float t0, float alpha, float nx, float ny, float px,
} }
``` ```
Diffusing uses 'pull' operations; tiles pull values from surrounding tiles. ### Tile Types
This is an attempt to think forwards to parallelising computation of diffusiong - ideally using the GPU.
## Overlays
The current overlay can be cycled with a UI button or by pressing `tab`.
Overlays display tile information by displaying tiles in different colours.
Normally, tile colours are displayed depending on their type and health.
This is the 'normal' view with no overlay.
Other overlays include:
* [Heat overlay](#heat-overlay)
## Build Mode
Build mode can be entered by pressing the `b` key.
Build the cursor will start flashing when entering build mode.
Once in build mode, a tile type must be selected to build anything.
Tiles cab be selected with the buttons along the bottom of the screen or by keyboard shortcuts.
The keyboard shortcut to select a tile type is indicated on the button.
To deselect a tile type, press `esc`.
Press `esc` again to exit build mode.
## Tile Types
Tiles start as wild tiles. Tiles start as wild tiles.
The player can use a UI element or a keyboard shortcut to enable build mode. The player can use a UI element or a keyboard shortcut to enable build mode.
@ -79,18 +58,14 @@ In build mode, the player can click on a wile tile to turn it into a developed t
* Wild tiles are green * Wild tiles are green
* Developed tiles are grey * Developed tiles are grey
## Heat ### Heat
Developed tiles produce heat while wild tiles absorb it. Developed tiles produce heat while wild tiles absorb it.
Every tile has a floating-point heat value which diffuses to its neighbours. Every tile has a floating-point heat value which diffuses to its neighbours.
Add a heat map view which can be toggled or cycled to.
### Heat overlay
A heat map view can be toggled or cycled to.
It is possible to cycle the view using a UI element. It is possible to cycle the view using a UI element.
Tile colours are displayed as a value along a gradient from cold to hot tiles.
## Tile Health ### Tile Health
Add another value to wild tiles: health. Add another value to wild tiles: health.
A wild tile can accept a certain amount of heat each tick from surrounding tiles. A wild tile can accept a certain amount of heat each tick from surrounding tiles.
@ -108,13 +83,13 @@ Its effectiveness at absorbing heat scales with its remaining health.
Diffusion still occurs to move heat away from depleted wild tiles. Diffusion still occurs to move heat away from depleted wild tiles.
## Rewilding ### Rewilding
Add a rewilding mode option to the UI. Add a rewilding mode option to the UI.
In rewilding mode, the player can click on a developed tile to turn in into a wild tile. In rewilding mode, the player can click on a developed tile to turn in into a wild tile.
The wild tile starts with 0 health. The wild tile starts with 0 health.
## Wild Recovery ### Wild Recovery
Depleted wile tiles can recover health each tick. Depleted wile tiles can recover health each tick.
The recovery rate is based on the difference between the damage threshold and the actual amount they received. The recovery rate is based on the difference between the damage threshold and the actual amount they received.
@ -123,7 +98,7 @@ The recovery rate is increased if the tile has wild neighbours.
neighbour multiplier = lerp(1, max, number of neighbours / 4) neighbour multiplier = lerp(1, max, number of neighbours / 4)
health restored = max((damage threshold - heat received), 0) * neighbour multiplier. health restored = max((damage threshold - heat received), 0) * neighbour multiplier.
## Developed tile types ### Developed tile types
Split developed tiles into 3 types: Split developed tiles into 3 types:
@ -136,19 +111,19 @@ These types generate different amounts of heat:
industrial > commercial > residential industrial > commercial > residential
## Journeys ### Journeys
Developed tiles have a chance to generate journeys between them. Developed tiles have a chance to generate journeys between them.
A journey instances a vehicle which traverses the map at a constant speed from its origin to its destination tile. A journey instances a vehicle which traverses the map at a constant speed from its origin to its destination tile.
A vehicle adds a constant amount of heat to every tile it traverses. A vehicle adds a constant amount of heat to every tile it traverses.
## Moving In ### Moving In
Residential tiles have two new integer resources, current residents and maximum residents. Residential tiles have two new integer resources, current residents and maximum residents.
Every tick there is a chance for residents to move in to a residential tile if the current number is less than the maximum number of residents. Every tick there is a chance for residents to move in to a residential tile if the current number is less than the maximum number of residents.
The initial number of maximm residents per residential tile is 4. The initial number of maximm residents per residential tile is 4.
## Jobs ### Jobs
Industrial and commercial tiles have two new integer resources, available jobs and occupied jobs. Industrial and commercial tiles have two new integer resources, available jobs and occupied jobs.
The chance for residents to move in depends on there being being fewer occupied than available jobs. The chance for residents to move in depends on there being being fewer occupied than available jobs.
@ -157,12 +132,12 @@ Their chance of generating a journey is proportional to
occupied jobs / available jobs . occupied jobs / available jobs .
## Commutes ### Commutes
A resident moves in to a residential tile and gets a job at an industrial or commercial 'job' tile. A resident moves in to a residential tile and gets a job at an industrial or commercial 'job' tile.
The residential tile regularly generates journeys to the job tile, and the job tile regularly generates journeys in the other direction. The residential tile regularly generates journeys to the job tile, and the job tile regularly generates journeys in the other direction.
## Comfortable Living Temperatures ### Comfortable Living Temperatures
If a residential tile is too hot, people will not want to live there. If a residential tile is too hot, people will not want to live there.
Residential tiles have a critical temperature at which the chance of someone moving into a tile becomes negative, becoming a chance that someone moves out. Residential tiles have a critical temperature at which the chance of someone moving into a tile becomes negative, becoming a chance that someone moves out.
@ -173,7 +148,7 @@ When someone moves out, one of the jobs associated with that residential tile is
# Not critical # Not critical
## Wealth ### Wealth
Journeys generate a new integer tile resource, wealth. Journeys generate a new integer tile resource, wealth.
When a journey is completed, the destination tile accrues 1 wealth. When a journey is completed, the destination tile accrues 1 wealth.

View File

@ -4,6 +4,6 @@
[resource] [resource]
shader = ExtResource( 1 ) shader = ExtResource( 1 )
shader_param/lowColor = Color( 0, 0, 0, 0 ) shader_param/lowColor = Color( 1, 0.960784, 0, 1 )
shader_param/highColor = Color( 0, 0.976471, 1, 1 ) shader_param/highColor = Color( 0.921569, 0, 1, 1 )
shader_param/t = null shader_param/t = null

View File

@ -0,0 +1,36 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/DebugUI.cs" type="Script" id=1]
[node name="Debug UI" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
TicksLabelPath = NodePath("Ticks")
PauseLabelPath = NodePath("Paused")
[node name="Ticks" type="Label" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_right = -10.0
margin_bottom = -30.0
grow_horizontal = 0
grow_vertical = 0
text = "ticks: 0"
align = 2
valign = 2
[node name="Paused" type="Label" parent="."]
anchor_left = 1.0
anchor_top = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
margin_right = -10.0
margin_bottom = -10.0
grow_horizontal = 0
grow_vertical = 0
text = "pause: false"
align = 2
valign = 2

View File

@ -1,68 +1,23 @@
[gd_scene load_steps=13 format=2] [gd_scene load_steps=5 format=2]
[ext_resource path="res://nodes/grid.tscn" type="PackedScene" id=1] [ext_resource path="res://nodes/grid.tscn" type="PackedScene" id=1]
[ext_resource path="res://nodes/grid_cursor.tscn" type="PackedScene" id=2] [ext_resource path="res://nodes/grid_cursor.tscn" type="PackedScene" id=2]
[ext_resource path="res://nodes/ui/debug_ui.tscn" type="PackedScene" id=3] [ext_resource path="res://nodes/debug_ui.tscn" type="PackedScene" id=3]
[ext_resource path="res://nodes/clock.tscn" type="PackedScene" id=4] [ext_resource path="res://nodes/clock.tscn" type="PackedScene" id=4]
[ext_resource path="res://nodes/interaction_modes/interaction_mode.tscn" type="PackedScene" id=5]
[ext_resource path="res://nodes/ui/build_mode_ui.tscn" type="PackedScene" id=6]
[ext_resource path="res://nodes/interaction_modes/build_mode.tscn" type="PackedScene" id=7]
[ext_resource path="res://nodes/ui/overlays UI.tscn" type="PackedScene" id=8]
[ext_resource path="res://nodes/ui/mode_selection_ui.tscn" type="PackedScene" id=9]
[ext_resource path="res://nodes/overlays.tscn" type="PackedScene" id=10]
[ext_resource path="res://resources/overlays/overlays.tres" type="Resource" id=11]
[sub_resource type="Curve" id=1]
_data = [ Vector2( 0, 0 ), 0.0, 5.0, 0, 0, Vector2( 0.5, 1 ), 0.0, 0.0, 0, 0, Vector2( 1, 0 ), -4.0, 0.0, 0, 0 ]
[node name="Root" type="Node"] [node name="Root" type="Node"]
[node name="Grid" parent="." instance=ExtResource( 1 )]
DiffusionCoefficient = 0.1
[node name="Cursor" parent="." instance=ExtResource( 2 )]
Grid = NodePath("../Grid")
[node name="Debug UI" parent="." instance=ExtResource( 3 )]
[node name="Clock" parent="." instance=ExtResource( 4 )] [node name="Clock" parent="." instance=ExtResource( 4 )]
_autoTick = true _autoTick = true
[node name="World Grid" parent="." instance=ExtResource( 1 )] [connection signal="OnPauseChanged" from="Clock" to="Debug UI" method="_on_Clock_OnPauseChanged"]
DiffusionCoefficient = 0.1 [connection signal="OnTick" from="Clock" to="Grid" method="_on_Clock_OnTick"]
[connection signal="OnTick" from="Clock" to="Debug UI" method="_on_Clock_OnTick"]
[node name="Cursor" parent="." instance=ExtResource( 2 )]
Grid = NodePath("../World Grid")
_pulseShape = SubResource( 1 )
[node name="Interaction Modes" parent="." instance=ExtResource( 5 )]
_buildModePath = NodePath("Build Mode")
[node name="Build Mode" parent="Interaction Modes" instance=ExtResource( 7 )]
_gridPath = NodePath("../../World Grid")
_cursorPath = NodePath("../../Cursor")
[node name="Overlays" parent="." instance=ExtResource( 10 )]
_overlaysResource = ExtResource( 11 )
_worldGridPath = NodePath("../World Grid")
[node name="UI" type="Control" parent="."]
anchor_right = 1.0
anchor_bottom = 1.0
mouse_filter = 2
[node name="Debug" parent="UI" instance=ExtResource( 3 )]
[node name="Build Mode" parent="UI" instance=ExtResource( 6 )]
_buildModePath = NodePath("../../Interaction Modes/Build Mode")
[node name="Mode Selection" parent="UI" instance=ExtResource( 9 )]
[node name="Overlays UI" parent="UI" instance=ExtResource( 8 )]
_overlaysPath = NodePath("../../Overlays")
[connection signal="OnPauseChanged" from="Clock" to="UI/Debug" method="_on_Clock_OnPauseChanged"]
[connection signal="OnTick" from="Clock" to="World Grid" method="_on_Clock_OnTick"]
[connection signal="OnTick" from="Clock" to="UI/Debug" method="_on_Clock_OnTick"]
[connection signal="OnInteractionModeChanged" from="Interaction Modes" to="Cursor" method="_on_Interaction_Mode_OnInteractionModeChanged"]
[connection signal="OnInteractionModeChanged" from="Interaction Modes" to="UI/Debug" method="_on_Interaction_Mode_OnInteractionModeChanged"]
[connection signal="OnModeEntered" from="Interaction Modes/Build Mode" to="Interaction Modes" method="_on_Build_Mode_OnModeEntered"]
[connection signal="OnModeEntered" from="Interaction Modes/Build Mode" to="UI/Mode Selection" method="EnableBuildMode"]
[connection signal="OnModeExited" from="Interaction Modes/Build Mode" to="Interaction Modes" method="_on_Build_Mode_OnModeExited"]
[connection signal="OnModeExited" from="Interaction Modes/Build Mode" to="UI/Mode Selection" method="Enable"]
[connection signal="SelectedTileTypeChanged" from="Interaction Modes/Build Mode" to="UI/Build Mode" method="UpdateButtonToggleState"]
[connection signal="OverlayCycled" from="Overlays" to="UI/Overlays UI" method="UpdateOverlayText"]
[connection signal="OnExit" from="UI/Build Mode" to="Interaction Modes" method="Reset"]
[connection signal="BuildModeEnabled" from="UI/Mode Selection" to="Interaction Modes/Build Mode" method="Enable"]

View File

@ -1,11 +1,10 @@
[gd_scene load_steps=4 format=2] [gd_scene load_steps=3 format=2]
[ext_resource path="res://scripts/WorldGrid.cs" type="Script" id=1] [ext_resource path="res://scripts/WorldGrid.cs" type="Script" id=1]
[ext_resource path="res://nodes/tile.tscn" type="PackedScene" id=2] [ext_resource path="res://nodes/tile.tscn" type="PackedScene" id=2]
[ext_resource path="res://resources/tiles/tiles.tres" type="Resource" id=3]
[node name="Grid" type="Node2D"] [node name="Grid" type="Node2D"]
script = ExtResource( 1 ) script = ExtResource( 1 )
TileScene = ExtResource( 2 ) TileScene = ExtResource( 2 )
Size = 10
CellSize = 64 CellSize = 64
_tileGridResource = ExtResource( 3 )

View File

@ -1,9 +0,0 @@
[gd_scene load_steps=3 format=2]
[ext_resource path="res://scripts/interaction_modes/BuildMode.cs" type="Script" id=1]
[ext_resource path="res://resources/tiles/tile_types/tile_type_collections/buildable_tiles.tres" type="Resource" id=2]
[node name="Build Mode" type="Node"]
script = ExtResource( 1 )
_key = 66
_buildableTilesResource = ExtResource( 2 )

View File

@ -1,6 +0,0 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/interaction_modes/InteractionModes.cs" type="Script" id=1]
[node name="Interaction Mode" type="Node"]
script = ExtResource( 1 )

View File

@ -1,3 +0,0 @@
[gd_scene format=2]
[node name="Selection Mode" type="Node"]

View File

@ -1,12 +0,0 @@
[gd_scene load_steps=5 format=2]
[ext_resource path="res://scripts/overlays/Overlays.cs" type="Script" id=1]
[ext_resource path="res://resources/overlays/heat_overlay.tres" type="Resource" id=2]
[ext_resource path="res://resources/overlays/no_overlay.tres" type="Resource" id=3]
[ext_resource path="res://resources/tiles/tiles.tres" type="Resource" id=4]
[node name="Overlays" type="Node"]
script = ExtResource( 1 )
_cycleOverlayKey = 16777218
_overlayResources = [ ExtResource( 3 ), ExtResource( 2 ) ]
_tileGridResource = ExtResource( 4 )

View File

@ -1,18 +0,0 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/ui/BuildModeUI.cs" type="Script" id=1]
[node name="Build Mode" type="Control"]
anchor_right = 1.0
anchor_bottom = 1.0
script = ExtResource( 1 )
[node name="Exit" type="Button" parent="."]
anchor_top = 1.0
anchor_bottom = 1.0
margin_top = -40.0
margin_right = 40.0
grow_vertical = 0
text = "Exit"
[connection signal="pressed" from="Exit" to="." method="Exit"]

View File

@ -1,48 +0,0 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/ui/DebugUI.cs" type="Script" id=1]
[node name="Debug UI" type="Control"]
anchor_left = 1.0
anchor_right = 1.0
mouse_filter = 2
script = ExtResource( 1 )
TicksLabelPath = NodePath("Ticks")
PauseLabelPath = NodePath("Paused")
InteractionModeLabelPath = NodePath("Interaction Mode")
[node name="Interaction Mode" type="Label" parent="."]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -102.0
margin_top = 15.0
margin_right = -14.0
margin_bottom = 29.0
grow_horizontal = 0
grow_vertical = 0
text = "mode: Selection"
align = 2
[node name="Ticks" type="Label" parent="."]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -60.0
margin_top = 35.0
margin_right = -14.0
margin_bottom = 49.0
grow_horizontal = 0
grow_vertical = 0
text = "ticks: 0"
align = 2
[node name="Paused" type="Label" parent="."]
anchor_left = 1.0
anchor_right = 1.0
margin_left = -90.0
margin_top = 55.0
margin_right = -14.0
margin_bottom = 69.0
grow_horizontal = 0
grow_vertical = 0
text = "pause: false"
align = 2

View File

@ -1,19 +0,0 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/ui/ModeSelectionUI.cs" type="Script" id=1]
[node name="Mode Selection UI" type="Control"]
anchor_top = 1.0
anchor_bottom = 1.0
margin_top = -600.0
margin_bottom = -600.0
script = ExtResource( 1 )
[node name="Build Mode" type="Button" parent="."]
margin_left = 2.0
margin_top = 562.0
margin_right = 38.0
margin_bottom = 598.0
text = "[B] Build Mode"
[connection signal="pressed" from="Build Mode" to="." method="EnableBuildMode"]

View File

@ -1,18 +0,0 @@
[gd_scene load_steps=2 format=2]
[ext_resource path="res://scripts/ui/OverlaysUI.cs" type="Script" id=1]
[node name="Overlays UI" type="Node"]
script = ExtResource( 1 )
_cycleOverlayButtonPath = NodePath("Button")
[node name="Button" type="Button" parent="."]
anchor_top = 1.0
anchor_bottom = 1.0
margin_top = -90.0
margin_right = 42.0
margin_bottom = -52.0
grow_vertical = 0
text = "[Tab] Cycle Overlay"
[connection signal="pressed" from="Button" to="." method="CycleOverlay"]

View File

@ -1,8 +0,0 @@
[gd_resource type="Resource" load_steps=2 format=2]
[ext_resource path="res://scripts/overlays/HeatOverlay.cs" type="Script" id=1]
[resource]
script = ExtResource( 1 )
_coldColor = Color( 0, 0, 0, 1 )
_hotColor = Color( 1, 0.654902, 0, 1 )

View File

@ -1,8 +0,0 @@
[gd_resource type="Resource" load_steps=2 format=2]
[ext_resource path="res://scripts/overlays/NoOverlay.cs" type="Script" id=1]
[resource]
script = ExtResource( 1 )
_healthy = Color( 0.0431373, 0.541176, 0.0235294, 1 )
_depleted = Color( 0.670588, 0.572549, 0.27451, 1 )

View File

@ -1,9 +0,0 @@
[gd_resource type="Resource" load_steps=4 format=2]
[ext_resource path="res://scripts/overlays/OverlayCollection.cs" type="Script" id=1]
[ext_resource path="res://resources/overlays/no_overlay.tres" type="Resource" id=2]
[ext_resource path="res://resources/overlays/heat_overlay.tres" type="Resource" id=3]
[resource]
script = ExtResource( 1 )
_resources = [ ExtResource( 2 ), ExtResource( 3 ) ]

View File

@ -1,10 +0,0 @@
[gd_resource type="Resource" load_steps=2 format=2]
[ext_resource path="res://scripts/tile/TileType.cs" type="Script" id=1]
[resource]
script = ExtResource( 1 )
Name = "Developed"
Key = 68
Color = Color( 0.372549, 0.372549, 0.372549, 1 )
HeatGeneration = 0.15

View File

@ -1,8 +0,0 @@
[gd_resource type="Resource" load_steps=3 format=2]
[ext_resource path="res://scripts/tile/TileTypeCollection.cs" type="Script" id=1]
[ext_resource path="res://resources/tiles/tile_types/developed.tres" type="Resource" id=2]
[resource]
script = ExtResource( 1 )
_resources = [ ExtResource( 2 ) ]

View File

@ -1,11 +0,0 @@
[gd_resource type="Resource" load_steps=2 format=2]
[ext_resource path="res://scripts/tile/Wild.cs" type="Script" id=1]
[resource]
script = ExtResource( 1 )
Name = "Wild"
Key = 87
Color = Color( 0.14902, 1, 0, 0 )
HeatGeneration = -0.02
Threshold = 0.6

View File

@ -1,9 +0,0 @@
[gd_resource type="Resource" load_steps=3 format=2]
[ext_resource path="res://scripts/tile/TileGrid.cs" type="Script" id=1]
[ext_resource path="res://resources/tiles/tile_types/wild.tres" type="Resource" id=2]
[resource]
script = ExtResource( 1 )
Size = 10
_startTileTypeResource = ExtResource( 2 )

View File

@ -0,0 +1,37 @@
using Godot;
using System;
public class DebugUI : Control
{
[Export]
public NodePath TicksLabelPath { private get; set; }
private Label _ticksLabel;
[Export]
public NodePath PauseLabelPath { private get; set; }
private Label _pauseLabel;
public override void _Ready()
{
_ticksLabel = GetNode<Label>(TicksLabelPath);
_pauseLabel = GetNode<Label>(PauseLabelPath);
SetTicksText(0);
SetPauseText(false);
}
#region Signals
public void _on_Clock_OnPauseChanged(bool paused) => SetPauseText(paused);
public void _on_Clock_OnTick(int ticks) => SetTicksText(ticks);
#endregion
private void SetTicksText(int ticks)
{
_ticksLabel.Text = $"ticks: {ticks}";
}
private void SetPauseText(bool paused)
{
_pauseLabel.Text = $"pause: {paused}";
}
}

View File

@ -8,22 +8,8 @@ public class GridCursor : Sprite
private WorldGrid _grid; private WorldGrid _grid;
private ShaderMaterial _material; private ShaderMaterial _material;
#region Animation
[Export]
private Curve _pulseShape;
[Export]
private float _pulseRate = 1.0f;
private float PulsePeriod => 1.0f / _pulseRate;
#endregion
private float _elapsed = 0.0f;
private bool _pulsing = false;
private const string T = "t"; private const string T = "t";
public int X { get; private set; }
public int Y { get; private set; }
public override void _Ready() public override void _Ready()
{ {
_grid = GetNode<WorldGrid>(Grid); _grid = GetNode<WorldGrid>(Grid);
@ -38,32 +24,28 @@ public class GridCursor : Sprite
{ {
var pos = mouseMoveEvent.Position; var pos = mouseMoveEvent.Position;
_grid.GetGridPos(pos, out var x, out var y); _grid.GetGridPos(pos, out var x, out var y);
X = x; this.Visible = _grid.IsInBounds(x, y);
Y = y; var position = new Vector2(x + .5f, y + .5f) * _grid.CellSize;
this.Visible = _grid.IsInBounds(X, Y);
var position = new Vector2(X + .5f, Y + .5f) * _grid.CellSize;
this.Position = position; this.Position = position;
return;
} }
} else if (@event is InputEventMouseButton mouseButtonEvent)
public override void _Process(float delta)
{ {
_elapsed += delta * _pulseRate; switch ((ButtonList)mouseButtonEvent.ButtonIndex)
if (!_pulsing)
{ {
case ButtonList.Left:
if (mouseButtonEvent.Pressed)
{
var pos = mouseButtonEvent.Position;
_grid.GetGridPos(pos, out var x, out var y);
_grid.SetTileValue(x, y, 1.0f);
_material.SetShaderParam(T, 1f); _material.SetShaderParam(T, 1f);
return;
} }
else
var a = _pulseShape.Interpolate(_elapsed % 1.0f);
_material.SetShaderParam(T, a);
}
public void _on_Interaction_Mode_OnInteractionModeChanged(Mode oldMode, Mode newMode)
{ {
_pulsing = newMode is BuildMode; _material.SetShaderParam(T, 0f);
}
break;
}
}
} }
} }

View File

@ -1,100 +0,0 @@
using Godot;
public class HeatSystem
{
private readonly TileGrid _tiles;
private readonly float _diffusionCoefficient;
private float[] _nextTemperatures = null;
public HeatSystem(TileGrid tiles, float diffusionCoefficient)
{
_tiles = tiles;
_diffusionCoefficient = diffusionCoefficient;
_nextTemperatures = new float[tiles.Count];
}
public void Process()
{
GenerateHeat();
Diffuse();
}
private float TransferHeat(float t0, float alpha, float nx, float ny, float px, float py)
{
float d2tdx2 = nx - 2 * t0 + px;
float d2tdy2 = ny - 2 * t0 + py;
return t0 + alpha * (d2tdx2 + d2tdy2);
}
private void GetNeighbourTemperatures(int x, int y, out float nx, out float ny, out float px, out float py)
{
// default value
var t = _tiles[x, y].temperature;
nx = x > 0 ? _tiles[x - 1, y].temperature : t;
px = x < _tiles.Size - 1 ? _tiles[x + 1, y].temperature : t;
ny = y > 0 ? _tiles[x, y - 1].temperature : t;
py = y < _tiles.Size - 1 ? _tiles[x, y + 1].temperature : t;
}
private void GenerateHeat()
{
for (int i = 0; i < _tiles.Count; i++)
{
var tile = _tiles[i];
var type = tile.type;
tile.temperature += type.HeatGeneration;
ApplyHeatDamage(ref tile);
_tiles[i] = tile;
}
}
private void ApplyHeatDamage(ref Tile tile)
{
if (!(tile.type is IDamageable damageable))
return;
var surplus = tile.temperature - damageable.Threshold;
if (surplus < 0)
return;
// TODO: parameterised balancing
tile.currentHealth -= surplus;
tile.currentHealth = Mathf.Clamp(tile.currentHealth, 0, 1);
}
private void Diffuse()
{
var D = _diffusionCoefficient;
for (int i = 0; i < _tiles.Count; i++)
{
float t = _tiles[i].temperature;
_tiles.MapIndex(i, out var x, out var y);
GetNeighbourTemperatures(
x, y,
out var nx,
out var ny,
out var px,
out var py);
var temperature = TransferHeat(t, D, nx, ny, px, py);
// TODO: what if it's really really cold out?
temperature = Mathf.Max(0, temperature);
_nextTemperatures[i] = temperature;
}
for (int i = 0; i < _tiles.Count; i++)
{
var tile = _tiles[i];
tile.temperature = _nextTemperatures[i];
_tiles[i] = tile;
}
}
}

View File

@ -1,33 +0,0 @@
using Godot;
using System.Collections;
using System.Collections.Generic;
public abstract class ReadOnlyResourceList<T> : Resource, IReadOnlyList<T> where T : Resource
{
[Export]
private List<Resource> _resources = new List<Resource>();
private List<T> Collection
{
get
{
if (_collection != null)
return _collection;
_collection = new List<T>();
foreach (var resource in _resources)
{
_collection.Add((T)resource);
}
return _collection;
}
}
private List<T> _collection = null;
public T this[int index] => Collection[index];
public int Count => Collection.Count;
public IEnumerator<T> GetEnumerator() => Collection.GetEnumerator();
IEnumerator IEnumerable.GetEnumerator() => Collection.GetEnumerator();
}

View File

@ -0,0 +1,8 @@
using Godot;
public struct Tile
{
public bool isHighlighted;
public float value;
public ShaderMaterial material;
}

View File

@ -7,8 +7,7 @@ public class WorldGrid : Node2D
private PackedScene TileScene { get; set; } private PackedScene TileScene { get; set; }
[Export] [Export]
private Resource _tileGridResource; public int Size { get; set; }
private TileGrid _tileGrid;
[Export] [Export]
public int CellSize { get; set; } public int CellSize { get; set; }
@ -16,49 +15,58 @@ public class WorldGrid : Node2D
[Export] [Export]
public float DiffusionCoefficient { get; set; } = 0.01f; public float DiffusionCoefficient { get; set; } = 0.01f;
public struct TileView private Tile[,] _tiles;
{ private float[,] _nextValues = null;
public ShaderMaterial material;
}
private TileView[] _tileViews;
private HeatSystem _heat;
// Called when the node enters the scene tree for the first time. // Called when the node enters the scene tree for the first time.
public override void _Ready() public override void _Ready()
{ {
_tileGrid = (TileGrid)_tileGridResource; GenerateGrid(Size);
}
GenerateCanvasItems(_tileGrid);
public override void _Process(float delta)
_heat = new HeatSystem(_tileGrid, DiffusionCoefficient); {
base._Process(delta);
for (int x = 0; x < Size; x++)
{
for (int y = 0; y < Size; y++)
{
var tile = _tiles[x, y];
var material = tile.material;
material.SetShaderParam("t", tile.value);
}
}
}
public bool IsInBounds(int x, int y)
{
return x >= 0 && x < Size && y >= 0 && y < Size;
}
public void ToggleTileHighlight(int x, int y)
{
if (!IsInBounds(x, y))
return;
_tiles[x, y].isHighlighted ^= true;
}
public void SetTileValue(int x, int y, float value)
{
_tiles[x, y].value = value;
} }
#region Positioning
public void GetGridPos(Vector2 position, out int x, out int y) public void GetGridPos(Vector2 position, out int x, out int y)
{ {
x = Mathf.FloorToInt(position.x / CellSize); x = Mathf.FloorToInt(position.x / CellSize);
y = Mathf.FloorToInt(position.y / CellSize); y = Mathf.FloorToInt(position.y / CellSize);
} }
public bool IsInBounds(int x, int y) => _tileGrid.IsInBounds(x, y);
#endregion
#region Interaction
public void SetTileType(int x, int y, TileType tileType)
{
if (!_tileGrid.IsInBounds(x, y))
return;
_tileGrid.MapPosition(x, y, out var idx);
var tile = _tileGrid[idx];
tile.type = tileType;
_tileGrid[x, y] = tile;
}
#endregion
private void Clear() private void Clear()
{ {
_tiles = null;
int children = this.GetChildCount(); int children = this.GetChildCount();
GD.Print(children); GD.Print(children);
for (int i = 0; i < children; i++) for (int i = 0; i < children; i++)
@ -66,19 +74,22 @@ public class WorldGrid : Node2D
var child = this.GetChild(i); var child = this.GetChild(i);
child.QueueFree(); child.QueueFree();
} }
_tileViews = null;
} }
private void GenerateCanvasItems(TileGrid grid) private void GenerateGrid(int size)
{ {
this.Clear(); this.Clear();
_tileViews = new TileView[grid.Count]; _tiles = new Tile[size, size];
_nextValues = new float[size, size];
for (int i = 0; i < grid.Count; i++) for (int x = 0; x < size; x++)
{ {
_tileGrid.MapIndex(i, out var x, out var y); for (int y = 0; y < size; y++)
{
var tile = new Tile();
tile.isHighlighted = false;
tile.value = 0.0f;
var node = TileScene.Instance<Node2D>(); var node = TileScene.Instance<Node2D>();
this.AddChild(node); this.AddChild(node);
@ -89,16 +100,64 @@ public class WorldGrid : Node2D
}; };
node.Position = position; node.Position = position;
var canvasItem = (CanvasItem)node; var canvasItem = (CanvasItem)node;
tile.material = (ShaderMaterial)canvasItem.Material;
var tile = _tileGrid[i]; _tiles[x, y] = tile;
}
TileView view = default;
view.material = (ShaderMaterial)canvasItem.Material;
_tileViews[i] = view;
} }
} }
public ShaderMaterial GetTileMaterial(int idx) => _tileViews[idx].material; public void _on_Clock_OnTick(int ticks)
{
Diffuse();
}
public void _on_Clock_OnTick(int ticks) => _heat.Process(); private float TransferHeat(float t0, float alpha, float nx, float ny, float px, float py)
{
float d2tdx2 = nx - 2 * t0 + px;
float d2tdy2 = ny - 2 * t0 + py;
return t0 + alpha * (d2tdx2 + d2tdy2);
}
private void GetNeighbourTemperatures(int x, int y, out float nx, out float ny, out float px, out float py)
{
// default value
var t = _tiles[x, y].value;
nx = x > 0 ? _tiles[x - 1, y].value : t;
px = x < Size - 1 ? _tiles[x + 1, y].value : t;
ny = y > 0 ? _tiles[x, y - 1].value : t;
py = y < Size - 1 ? _tiles[x, y + 1].value : t;
}
private void Diffuse()
{
for (int x = 0; x < Size; x++)
{
for (int y = 0; y < Size; y++)
{
float t = _tiles[x, y].value;
var D = DiffusionCoefficient;
GetNeighbourTemperatures(
x, y,
out var nx,
out var ny,
out var px,
out var py);
// current value
_nextValues[x, y] = TransferHeat(t, D, nx, ny, px, py);
}
}
for (int x = 0; x < Size; x++)
{
for (int y = 0; y < Size; y++)
{
_tiles[x, y].value = _nextValues[x, y];
}
}
}
} }

View File

@ -1,132 +0,0 @@
using Godot;
using System;
public class BuildMode : Mode
{
[Signal]
delegate void SelectedTileTypeChanged();
[Export]
private NodePath _gridPath;
private WorldGrid _grid;
[Export]
private NodePath _cursorPath;
private GridCursor _cursor;
[Export]
private Resource _buildableTilesResource;
public TileTypeCollection BuildableTiles { get; private set; }
public TileType SelectedTileType
{
get => _selectedTileType;
set
{
if (_selectedTileType == value)
return;
if (value != null)
{
GD.Print($"{GetType()}: selected {value} tile type");
}
else if (_selectedTileType != null)
{
GD.Print($"{GetType()}: cleared tile selection");
}
_selectedTileType = value;
EmitSignal(nameof(SelectedTileTypeChanged), _selectedTileType);
}
}
private TileType _selectedTileType = null;
public override void _Ready()
{
base._Ready();
BuildableTiles = (TileTypeCollection)_buildableTilesResource;
foreach (var tt in BuildableTiles)
{
GD.Print(tt.Name);
}
_grid = GetNode<WorldGrid>(_gridPath);
_cursor = GetNode<GridCursor>(_cursorPath);
}
private void HandleKeyInput(InputEventKey keyEvent)
{
if (!(keyEvent.Pressed))
return;
var key = (KeyList)keyEvent.Scancode;
if (key == KeyList.Escape)
{
if (SelectedTileType != null)
{
SelectedTileType = null;
return;
}
Disable();
}
foreach (var tt in BuildableTiles)
{
if (key != tt.Key)
continue;
SelectedTileType = tt;
}
}
private void HandleMouseInput(InputEventMouse mouseEvent)
{
if (!(mouseEvent is InputEventMouseButton buttonEvent))
return;
if (!buttonEvent.Pressed)
return;
var button = (ButtonList)buttonEvent.ButtonIndex;
if (button != ButtonList.Left)
return;
if (SelectedTileType == null)
return;
_grid.SetTileType(_cursor.X, _cursor.Y, SelectedTileType);
}
public override void _Input(InputEvent @event)
{
base._Input(@event);
if (!Active)
return;
if (@event is InputEventKey keyEvent)
{
HandleKeyInput(keyEvent);
return;
}
if (@event is InputEventMouse mouseEvent)
{
HandleMouseInput(mouseEvent);
return;
}
}
protected override void OnEnabled()
{
}
protected override void OnDisabled()
{
SelectedTileType = null;
}
}

View File

@ -1,57 +0,0 @@
using Godot;
using System;
public class InteractionModes : Node
{
[Export]
private NodePath _buildModePath;
private BuildMode _buildMode;
[Signal]
delegate void OnInteractionModeChanged(Mode oldMode, Mode newMode);
private Mode Mode
{
get => _current;
set
{
if (_current == value)
return;
if (value != null)
{
GD.Print($"set interaction mode: {value}");
}
else if (_current != null)
{
GD.Print($"clear interaction mode");
}
var old = _current;
_current = value;
EmitSignal(nameof(OnInteractionModeChanged), old, _current);
}
}
private Mode _current = null;
public override void _Ready()
{
base._Ready();
_buildMode = GetNode<BuildMode>(_buildModePath);
}
public void _on_Build_Mode_OnModeEntered()
{
Mode = _buildMode;
}
public void _on_Build_Mode_OnModeExited()
{
Mode = null;
}
public void Reset()
{
_buildMode.Disable();
}
}

View File

@ -1,68 +0,0 @@
using Godot;
using System;
public abstract class Mode : Node
{
[Signal]
delegate void OnModeEntered();
[Signal]
delegate void OnModeExited();
[Export]
private KeyList _key;
public bool Active
{
get => _active;
private set
{
if (value == _active)
return;
_active = value;
var signal = _active
? nameof(OnModeEntered)
: nameof(OnModeExited);
EmitSignal(signal);
}
}
private bool _active;
public override void _Input(InputEvent @event)
{
base._Input(@event);
if (!(@event is InputEventKey keyEvent))
return;
if (!keyEvent.Pressed)
return;
var key = (KeyList)keyEvent.Scancode;
if (key == _key)
{
Active = true;
}
}
public void Enable()
{
Active = true;
OnEnabled();
}
protected abstract void OnEnabled();
public void Disable()
{
Active = false;
OnDisabled();
}
protected abstract void OnDisabled();
public override string ToString()
{
return GetType().ToString();
}
}

View File

@ -1,18 +0,0 @@
using Godot;
using System;
public class HeatOverlay : Overlay
{
[Export]
private Color _coldColor;
[Export]
private Color _hotColor;
public override void Apply(Tile tile, ShaderMaterial material)
{
material.SetShaderParam("lowColor", _coldColor);
material.SetShaderParam("highColor", _hotColor);
material.SetShaderParam("t", tile.temperature);
}
}

View File

@ -1,27 +0,0 @@
using Godot;
public class NoOverlay : Overlay
{
[Export]
private Color _healthy;
[Export]
private Color _depleted;
// in this view we want to draw tiles with their normal colour
public override void Apply(Tile tile, ShaderMaterial material)
{
var type = tile.type;
if (type is Wild wild)
{
material.SetShaderParam("lowColor", _depleted);
material.SetShaderParam("highColor", _healthy);
material.SetShaderParam("t", tile.currentHealth);
return;
}
material.SetShaderParam("lowColor", type.Color);
material.SetShaderParam("t", 0);
}
}

View File

@ -1,7 +0,0 @@
using Godot;
public abstract class Overlay : Resource
{
public string Name => GetType().ToString();
public abstract void Apply(Tile tile, ShaderMaterial material);
}

View File

@ -1,3 +0,0 @@
public class OverlayCollection : ReadOnlyResourceList<Overlay>
{
}

View File

@ -1,67 +0,0 @@
using Godot;
using System;
public class Overlays : Node
{
[Signal]
delegate void OverlayCycled(Overlay overlay);
[Export]
private KeyList _cycleOverlayKey;
[Export] private Resource _overlaysResource;
public Overlay Current => _overlays[_overlayIdx];
private OverlayCollection _overlays;
private int _overlayIdx = 0;
[Export]
private Resource _tileGridResource;
private TileGrid _tileGrid;
[Export]
private NodePath _worldGridPath;
private WorldGrid _worldGrid;
public override void _Ready()
{
_overlays = (OverlayCollection)_overlaysResource;
_tileGrid = (TileGrid)_tileGridResource;
_worldGrid = GetNode<WorldGrid>(_worldGridPath);
}
public override void _Input(InputEvent @event)
{
base._Input(@event);
if (!(@event is InputEventKey keyEvent))
return;
if (!(keyEvent.Pressed))
return;
if ((KeyList)keyEvent.Scancode != _cycleOverlayKey)
return;
CycleOverlay();
}
public override void _Process(float delta)
{
base._Process(delta);
for (int i = 0; i < _tileGrid.Count; i++)
{
var tile = _tileGrid[i];
var material = _worldGrid.GetTileMaterial(i);
Current.Apply(tile, material);
}
}
public void CycleOverlay()
{
_overlayIdx++;
_overlayIdx %= _overlays.Count;
EmitSignal(nameof(OverlayCycled), Current);
}
}

View File

@ -1,4 +0,0 @@
public interface IDamageable
{
float Threshold { get; }
}

View File

@ -1,9 +0,0 @@
using Godot;
public struct Tile
{
public bool isHighlighted;
public float temperature;
public TileType type;
public float currentHealth;
}

View File

@ -1,93 +0,0 @@
using Godot;
using System;
using System.Collections;
using System.Collections.Generic;
public class TileGrid : Resource, IReadOnlyList<Tile>
{
[Export]
public int Size { get; private set; } = 10;
[Export]
private Resource _startTileTypeResource;
private TileType StartTileType => (TileType)_startTileTypeResource;
public int Count => Size * Size;
public Tile this[int index]
{
get => Tiles[index];
set => Tiles[index] = value;
}
public Tile this[int x, int y]
{
get
{
MapPosition(x, y, out int idx);
return Tiles[idx];
}
set
{
MapPosition(x, y, out int idx);
Tiles[idx] = value;
}
}
private List<Tile> Tiles
{
get
{
if (_tiles == null)
{
_tiles = new List<Tile>();
Generate(_tiles);
}
return _tiles;
}
}
private List<Tile> _tiles = null;
private void Generate(List<Tile> tiles)
{
tiles.Clear();
for (int i = 0; i < Count; i++)
{
var tile = new Tile();
tile.isHighlighted = false;
tile.temperature = 0.0f;
tile.type = StartTileType;
tile.currentHealth = 1.0f;
tiles.Add(tile);
}
}
public IEnumerator<Tile> GetEnumerator()
{
return Tiles.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return Tiles.GetEnumerator();
}
public void MapIndex(int idx, out int x, out int y)
{
y = idx / Size;
x = idx % Size;
}
public void MapPosition(int x, int y, out int idx)
{
idx = y * Size + x;
}
public bool IsInBounds(int x, int y)
{
return x >= 0 && x < Size && y >= 0 && y < Size;
}
}

View File

@ -1,20 +0,0 @@
using Godot;
public class TileType : Resource
{
[Export]
public string Name { get; private set; }
public string BuildLabel => $"[{Key}] {Name}";
[Export]
public KeyList Key { get; private set; }
[Export]
public Color Color { get; private set; }
[Export]
public float HeatGeneration { get; private set; }
public override string ToString() => Name;
}

View File

@ -1,3 +0,0 @@
public class TileTypeCollection : ReadOnlyResourceList<TileType>
{
}

View File

@ -1,7 +0,0 @@
using Godot;
public class Wild : TileType, IDamageable
{
[Export]
public float Threshold { get; private set; }
}

View File

@ -1,81 +0,0 @@
using Godot;
using System;
using System.Collections.Generic;
public class BuildModeUI : Control
{
[Signal]
delegate void OnExit();
[Export]
private NodePath _buildModePath;
private BuildMode _buildMode;
private readonly Dictionary<TileType, Button> _typeButtons
= new Dictionary<TileType, Button>();
// Called when the node enters the scene tree for the first time.
public override void _Ready()
{
_buildMode = GetNode<BuildMode>(_buildModePath);
var buttonHeight = 40;
var tileType = _buildMode.BuildableTiles[0];
int x = 50;
SpawnButton(tileType, buttonHeight, ref x);
}
private void UpdateButtonToggleState(TileType tileType)
{
foreach (var kvp in _typeButtons)
{
var button = _typeButtons[kvp.Key];
button.SetPressedNoSignal(kvp.Key == tileType);
}
}
private void SpawnButton(TileType tileType, int buttonHeight, ref int x)
{
const int MARGIN = 5;
var button = new Button();
this.AddChild(button);
button.Text = tileType.BuildLabel;
var parameters = new Godot.Collections.Array(button, tileType);
button.Connect("pressed", this, nameof(SelectTileType), parameters);
button.SetAnchorsPreset(LayoutPreset.BottomLeft);
button.MarginBottom = 0;
var size = button.RectSize;
button.RectSize = new Vector2(size.x, buttonHeight);
button.ToggleMode = true;
button.SetPosition(new Vector2(x, -buttonHeight));
x += Mathf.RoundToInt(size.x) + MARGIN;
_typeButtons[tileType] = button;
}
private void SelectTileType(Button button, TileType tileType)
{
_buildMode.SelectedTileType = button.Pressed
? tileType
: null;
}
public void Exit()
{
EmitSignal(nameof(OnExit));
}
public override void _Process(float delta)
{
base._Process(delta);
Visible = _buildMode.Active;
}
}

View File

@ -1,41 +0,0 @@
using Godot;
using System;
public class DebugUI : Control
{
[Export]
public NodePath TicksLabelPath { private get; set; }
private Label _ticksLabel;
[Export]
public NodePath PauseLabelPath { private get; set; }
private Label _pauseLabel;
[Export]
public NodePath InteractionModeLabelPath { private get; set; }
private Label _interactionModeLabel;
public override void _Ready()
{
_ticksLabel = GetNode<Label>(TicksLabelPath);
_pauseLabel = GetNode<Label>(PauseLabelPath);
_interactionModeLabel = GetNode<Label>(InteractionModeLabelPath);
}
#region Signals
public void _on_Clock_OnPauseChanged(bool paused) =>
SetLabelText(_pauseLabel, "pause", paused);
public void _on_Clock_OnTick(int ticks) =>
SetLabelText(_ticksLabel, "ticks", ticks);
public void _on_Interaction_Mode_OnInteractionModeChanged(Mode oldMode, Mode newMode)
{
SetLabelText(_interactionModeLabel, "mode", newMode);
}
#endregion
private void SetLabelText(Label label, string key, object value)
{
label.Text = $"{key}: {value}";
}
}

View File

@ -1,24 +0,0 @@
using Godot;
using System;
public class ModeSelectionUI : Control
{
[Signal]
delegate void BuildModeEnabled();
public override void _Process(float delta)
{
base._Process(delta);
}
public void EnableBuildMode()
{
EmitSignal(nameof(BuildModeEnabled));
Visible = false;
}
public void Enable()
{
Visible = true;
}
}

View File

@ -1,36 +0,0 @@
using Godot;
using System;
using System.Collections.Generic;
public class OverlaysUI : Node
{
[Export]
private NodePath _cycleOverlayButtonPath;
private Button _cycleOverlayButton;
[Export]
private NodePath _overlaysPath;
private Overlays _overlays;
private string _mainText;
public override void _Ready()
{
base._Ready();
_cycleOverlayButton = GetNode<Button>(_cycleOverlayButtonPath);
_mainText = _cycleOverlayButton.Text;
_overlays = GetNode<Overlays>(_overlaysPath);
}
public void CycleOverlay()
{
_overlays.CycleOverlay();
}
public void UpdateOverlayText(Overlay overlay)
{
_cycleOverlayButton.Text = $"{_mainText} ({_overlays.Current.Name})";
}
}