diff --git a/Prefabs/DSL/ForNode.tscn b/Prefabs/DSL/ForNode.tscn index 56582fa..b41d5f0 100644 --- a/Prefabs/DSL/ForNode.tscn +++ b/Prefabs/DSL/ForNode.tscn @@ -1,6 +1,6 @@ [gd_scene format=3 uid="uid://co7op7et6is8p"] -[ext_resource type="Script" path="res://Scripts/UI/DSL/NodeDisplays/ForNodeDisplay.cs" id="1_xshi5"] +[ext_resource type="Script" uid="uid://gptqyjv5swwc" path="res://Scripts/UI/DSL/NodeDisplays/ForNodeDisplay.cs" id="1_xshi5"] [node name="For" type="GraphNode" unique_id=1235671673] offset_right = 229.0 @@ -16,7 +16,7 @@ slot/0/right_type = 0 slot/0/right_color = Color(1, 1, 1, 1) slot/0/right_icon = null slot/0/draw_stylebox = true -slot/1/left_enabled = true +slot/1/left_enabled = false slot/1/left_type = 0 slot/1/left_color = Color(1, 1, 1, 1) slot/1/left_icon = null @@ -61,7 +61,7 @@ vertical_alignment = 1 [node name="RichTextLabel" type="RichTextLabel" parent="." unique_id=1677882951] layout_mode = 2 -text = "Until" +text = "Afterwards" fit_content = true autowrap_mode = 0 horizontal_alignment = 1 diff --git a/Prefabs/DSL/IfNode.tscn b/Prefabs/DSL/IfNode.tscn index 3a8415a..6278a98 100644 --- a/Prefabs/DSL/IfNode.tscn +++ b/Prefabs/DSL/IfNode.tscn @@ -1,6 +1,6 @@ [gd_scene format=3 uid="uid://ctmad6foidkvp"] -[ext_resource type="Script" path="res://Scripts/UI/DSL/NodeDisplays/IfNodeDisplay.cs" id="1_ygi5c"] +[ext_resource type="Script" uid="uid://cngxwfcrim746" path="res://Scripts/UI/DSL/NodeDisplays/IfNodeDisplay.cs" id="1_ygi5c"] [node name="If" type="GraphNode" unique_id=821127877] offset_right = 468.0 @@ -16,7 +16,7 @@ slot/0/right_type = 0 slot/0/right_color = Color(1, 1, 1, 1) slot/0/right_icon = null slot/0/draw_stylebox = true -slot/1/left_enabled = true +slot/1/left_enabled = false slot/1/left_type = 0 slot/1/left_color = Color(1, 1, 1, 1) slot/1/left_icon = null diff --git a/Prefabs/DSL/StartNode.tscn b/Prefabs/DSL/StartNode.tscn new file mode 100644 index 0000000..ca03c4a --- /dev/null +++ b/Prefabs/DSL/StartNode.tscn @@ -0,0 +1,21 @@ +[gd_scene format=3 uid="uid://b1y4r3s7ukmfw"] + +[ext_resource type="Script" path="res://Scripts/UI/DSL/NodeDisplays/StartNodeDisplay.cs" id="1_start"] + +[node name="Start" type="GraphNode" unique_id=2100000001] +offset_right = 65.0 +offset_bottom = 55.0 +title = "Start" +slot/0/left_enabled = false +slot/0/left_type = 0 +slot/0/left_color = Color(1, 1, 1, 1) +slot/0/left_icon = null +slot/0/right_enabled = true +slot/0/right_type = 0 +slot/0/right_color = Color(1, 1, 1, 1) +slot/0/right_icon = null +slot/0/draw_stylebox = true +script = ExtResource("1_start") + +[node name="Control" type="Control" parent="." unique_id=2100000002] +layout_mode = 2 diff --git a/Prefabs/DSL/WhileNode.tscn b/Prefabs/DSL/WhileNode.tscn index 7eaf651..2423ea8 100644 --- a/Prefabs/DSL/WhileNode.tscn +++ b/Prefabs/DSL/WhileNode.tscn @@ -1,6 +1,6 @@ [gd_scene format=3 uid="uid://bwiaqvl0d4x8v"] -[ext_resource type="Script" path="res://Scripts/UI/DSL/NodeDisplays/WhileNodeDisplay.cs" id="1_q0rc7"] +[ext_resource type="Script" uid="uid://d3npiur46icru" path="res://Scripts/UI/DSL/NodeDisplays/WhileNodeDisplay.cs" id="1_q0rc7"] [node name="While" type="GraphNode" unique_id=2040226700] offset_right = 499.0 @@ -16,7 +16,7 @@ slot/0/right_type = 0 slot/0/right_color = Color(1, 1, 1, 1) slot/0/right_icon = null slot/0/draw_stylebox = true -slot/1/left_enabled = true +slot/1/left_enabled = false slot/1/left_type = 0 slot/1/left_color = Color(1, 1, 1, 1) slot/1/left_icon = null @@ -82,7 +82,7 @@ vertical_alignment = 1 [node name="RichTextLabel" type="RichTextLabel" parent="." unique_id=1571031910] layout_mode = 2 -text = "Until" +text = "Afterwards" fit_content = true autowrap_mode = 0 horizontal_alignment = 1 diff --git a/Scripts/Core/ResourceLoader.cs b/Scripts/Core/ResourceLoader.cs index 8aa2544..0b5734d 100644 --- a/Scripts/Core/ResourceLoader.cs +++ b/Scripts/Core/ResourceLoader.cs @@ -66,6 +66,7 @@ public partial class ResourceLoader { Dictionary nodes = new Dictionary() { + { new StartNode(), GD.Load("res://Prefabs/DSL/StartNode.tscn") }, { new MoveNode(), GD.Load("res://Prefabs/DSL/MoveNode.tscn") }, { new HarvestNode(), GD.Load("res://Prefabs/DSL/HarvestNode.tscn") }, { new CraftNode(), GD.Load("res://Prefabs/DSL/CraftNode.tscn") }, diff --git a/Scripts/DSL/Nodes/ForNode.cs b/Scripts/DSL/Nodes/ForNode.cs index b82a948..10ffccb 100644 --- a/Scripts/DSL/Nodes/ForNode.cs +++ b/Scripts/DSL/Nodes/ForNode.cs @@ -1,4 +1,5 @@ using Godot; +using System.Collections.Generic; public class ForNode : ProgramNode { @@ -34,4 +35,27 @@ public class ForNode : ProgramNode { return $"Name: {DisplayText}, AmountExecuted: {amountExecuted}, Amount: {amount}"; } + + public override void SetNextNode( + List connections, + Dictionary availableNodes + ) + { + nextNode = null; + NegativeNode = null; + + foreach (Godot.Collections.Dictionary connection in connections) + { + int port = (int)connection["from_port"]; + ProgramNode connectedNode = GetConnectedNode(connection, availableNodes); + if (port == 0) + { + nextNode = connectedNode; + } + else + { + NegativeNode = connectedNode; + } + } + } } diff --git a/Scripts/DSL/Nodes/IfNode.cs b/Scripts/DSL/Nodes/IfNode.cs index 8ab209e..017edb2 100644 --- a/Scripts/DSL/Nodes/IfNode.cs +++ b/Scripts/DSL/Nodes/IfNode.cs @@ -1,4 +1,5 @@ using Godot; +using System.Collections.Generic; public class IfNode : ProgramNode { @@ -56,4 +57,27 @@ public class IfNode : ProgramNode { return $"Name: {DisplayText}, Item: {(selectedItem == null ? "Empty" : selectedItem.data.Id)}, Comparator: {comparator}, Amount: {amount}"; } + + public override void SetNextNode( + List connections, + Dictionary availableNodes + ) + { + nextNode = null; + NegativeNode = null; + + foreach (Godot.Collections.Dictionary connection in connections) + { + int port = (int)connection["from_port"]; + ProgramNode connectedNode = GetConnectedNode(connection, availableNodes); + if (port == 0) + { + nextNode = connectedNode; + } + else + { + NegativeNode = connectedNode; + } + } + } } diff --git a/Scripts/DSL/Nodes/MoveNode.cs b/Scripts/DSL/Nodes/MoveNode.cs index c85451f..5f0815e 100644 --- a/Scripts/DSL/Nodes/MoveNode.cs +++ b/Scripts/DSL/Nodes/MoveNode.cs @@ -45,6 +45,7 @@ public class MoveNode : ProgramNode pathPoints.Remove(pathPoints[0]); if (pathPoints.Count <= 0) { + pathPoints = null; lastExecutionMessage = ""; return NodeResult.SUCCESS; } diff --git a/Scripts/DSL/Nodes/ProgramNode.cs b/Scripts/DSL/Nodes/ProgramNode.cs index c5afb1b..f3bdbe2 100644 --- a/Scripts/DSL/Nodes/ProgramNode.cs +++ b/Scripts/DSL/Nodes/ProgramNode.cs @@ -1,13 +1,37 @@ using Godot; +using System.Collections.Generic; public abstract class ProgramNode { public ProgramNode nextNode; - public ProgramNode previousNode; + public ProgramNode NegativeNode; public string DisplayText; public string lastExecutionMessage; public abstract NodeResult Execute(Robot robot, double delta); public abstract ProgramNode Duplicate(); public abstract string Save(); + + public virtual void SetNextNode( + List connections, + Dictionary availableNodes + ) + { + nextNode = null; + + if (connections.Count <= 0) return; + + nextNode = GetConnectedNode(connections[0], availableNodes); + } + + protected ProgramNode GetConnectedNode( + Godot.Collections.Dictionary connection, + Dictionary availableNodes + ) + { + StringName nodeName = connection["to_node"].AsStringName(); + if (!availableNodes.ContainsKey(nodeName)) return null; + + return availableNodes[nodeName]; + } } diff --git a/Scripts/DSL/Nodes/StartNode.cs b/Scripts/DSL/Nodes/StartNode.cs new file mode 100644 index 0000000..c0e5c1a --- /dev/null +++ b/Scripts/DSL/Nodes/StartNode.cs @@ -0,0 +1,23 @@ +public class StartNode : ProgramNode +{ + public StartNode() + { + DisplayText = "Start"; + } + + public override NodeResult Execute(Robot robot, double delta) + { + return NodeResult.SUCCESS; + } + + public override ProgramNode Duplicate() + { + StartNode duplicate = new StartNode(); + return duplicate; + } + + public override string Save() + { + return $"Name: {DisplayText}"; + } +} diff --git a/Scripts/DSL/Nodes/StartNode.cs.uid b/Scripts/DSL/Nodes/StartNode.cs.uid new file mode 100644 index 0000000..a94b41a --- /dev/null +++ b/Scripts/DSL/Nodes/StartNode.cs.uid @@ -0,0 +1 @@ +uid://d3kyu2j3nxjsh diff --git a/Scripts/DSL/Nodes/WhileNode.cs b/Scripts/DSL/Nodes/WhileNode.cs index 21862cd..1c2b8f8 100644 --- a/Scripts/DSL/Nodes/WhileNode.cs +++ b/Scripts/DSL/Nodes/WhileNode.cs @@ -1,4 +1,5 @@ using Godot; +using System.Collections.Generic; public class WhileNode : ProgramNode { @@ -57,4 +58,27 @@ public class WhileNode : ProgramNode { return $"Name: {DisplayText}, Item: {(selectedItem == null ? "Empty" : selectedItem.data.Id)}, Comparator: {comparator}, Amount: {amount}"; } + + public override void SetNextNode( + List connections, + Dictionary availableNodes + ) + { + nextNode = null; + NegativeNode = null; + + foreach (Godot.Collections.Dictionary connection in connections) + { + int port = (int)connection["from_port"]; + ProgramNode connectedNode = GetConnectedNode(connection, availableNodes); + if (port == 0) + { + nextNode = connectedNode; + } + else + { + NegativeNode = connectedNode; + } + } + } } diff --git a/Scripts/Gameplay/Robots/Robot.cs b/Scripts/Gameplay/Robots/Robot.cs index d2b32b6..4429a11 100644 --- a/Scripts/Gameplay/Robots/Robot.cs +++ b/Scripts/Gameplay/Robots/Robot.cs @@ -35,26 +35,11 @@ public partial class Robot : Node3D { if (CanExecute(delta)) { + GD.Print(currentNode.DisplayText); switch (currentNode.Execute(this, delta)) { case NodeResult.SUCCESS: - if (currentNode.DisplayText == "Until") - { - while (true) - { - currentNode = currentNode.previousNode; - if (currentNode == null) - { - isExecuting = false; - break; - } - if (currentNode.DisplayText == "For" || currentNode.DisplayText == "While") break; - } - } - else - { - currentNode = currentNode.nextNode; - } + currentNode = currentNode.nextNode; if (currentNode == null) { isExecuting = false; @@ -70,33 +55,10 @@ public partial class Robot : Node3D currentMessage = ""; break; case NodeResult.CONDITIONFALSE: - string sourceNode = currentNode.DisplayText; - while (true) + currentNode = currentNode.NegativeNode; + if (currentNode == null) { - currentNode = currentNode.nextNode; - if (currentNode == null) - { - isExecuting = false; - break; - } - if (sourceNode == "If") - { - if (currentNode.DisplayText == "If" || currentNode.DisplayText == "Else") break; - } - else - { - if (currentNode.DisplayText == "While" || currentNode.DisplayText == "For" || currentNode.DisplayText == "Until") - { - if (currentNode.nextNode == null) - { - isExecuting = false; - break; - } - currentNode = currentNode.nextNode; - break; - } - } - + isExecuting = false; } break; } @@ -133,6 +95,13 @@ public partial class Robot : Node3D currentMessage = ""; } + public void StopExecution(string message) + { + isExecuting = false; + currentNode = null; + currentMessage = message; + } + public float GetMovementSpeed() { return GameData.robotStats.GetMovementSpeed(GameData.robotSpeed) diff --git a/Scripts/Tests/TestRunner.cs b/Scripts/Tests/TestRunner.cs index fe5d7e9..8f7330c 100644 --- a/Scripts/Tests/TestRunner.cs +++ b/Scripts/Tests/TestRunner.cs @@ -35,6 +35,7 @@ public partial class TestRunner : Node Run("If node evaluates inventory comparisons", TestIfNodeEvaluatesInventoryComparisons); Run("While node reports false conditions", TestWhileNodeReportsFalseConditions); Run("For node stops after configured amount", TestForNodeStopsAfterConfiguredAmount); + Run("Start node succeeds immediately", TestStartNodeSucceedsImmediately); Run("Paused world does not drain survival", TestPausedWorldDoesNotDrainSurvival); Run("Open gate hides gate content", TestOpenGateHidesGateContent); Run("Layer generation succeeds above threshold", TestLayerGenerationSuccessRate); @@ -476,6 +477,14 @@ public partial class TestRunner : Node AssertEqual(NodeResult.CONDITIONFALSE, node.Execute(null, 0), "loop finished"); } + private void TestStartNodeSucceedsImmediately() + { + StartNode node = new StartNode(); + + AssertEqual(NodeResult.SUCCESS, node.Execute(null, 0), "start should succeed"); + AssertEqual("Name: Start", node.Save(), "start save"); + } + private void TestPausedWorldDoesNotDrainSurvival() { GameData.isPaused = true; @@ -549,6 +558,18 @@ public partial class TestRunner : Node AssertTrue(GameData.availableItems.ContainsKey("water"), "water item loaded"); AssertTrue(GameData.availableResearch.ContainsKey("basics"), "basics research loaded"); AssertTrue(GameData.availableResearch.ContainsKey("iron_robotics"), "iron robotics research loaded"); + + Dictionary dslNodes = ResourceLoader.LoadDSLNodes(); + bool hasStartNode = false; + foreach (ProgramNode node in dslNodes.Keys) + { + if (node is StartNode) + { + hasStartNode = true; + break; + } + } + AssertTrue(hasStartNode, "start node prefab loaded"); } private Layer CreateTestLayer(int level, string collapsedMesh) diff --git a/Scripts/UI/DSL/CodingWindow.cs b/Scripts/UI/DSL/CodingWindow.cs index 8db1d02..b322a6f 100644 --- a/Scripts/UI/DSL/CodingWindow.cs +++ b/Scripts/UI/DSL/CodingWindow.cs @@ -13,6 +13,7 @@ public partial class CodingWindow : PanelContainer [Export] LineEdit nameInput; public System.Collections.Generic.Dictionary DSLNodes; + private System.Collections.Generic.Dictionary availableNodes; public override void _Ready() { @@ -83,10 +84,19 @@ public partial class CodingWindow : PanelContainer private void AddEditorNode(ProgramNode node) { NodeDisplay editorDisplay = DSLNodes[node].Instantiate(); + editorDisplay.PositionOffset = (editorWindow.ScrollOffset + editorWindow.Size / 2) / editorWindow.Zoom - editorDisplay.Size / 2; editorWindow.AddChild(editorDisplay); RegisterEditorNode(editorDisplay); } + private void MoveNodeToVisibleGraphCenter(NodeDisplay nodeDisplay) + { + Vector2 visibleCenter = editorWindow.ScrollOffset + + editorWindow.Size / (2f * editorWindow.Zoom); + + nodeDisplay.PositionOffset = visibleCenter - nodeDisplay.Size / (2f * editorWindow.Zoom); + } + private void RegisterEditorNode(NodeDisplay editorDisplay) { editorDisplay.OnDeleteNode += () => @@ -121,7 +131,25 @@ public partial class CodingWindow : PanelContainer public void CompileProgram() { - List nodes = new List(); + if (robot == null) return; + + NodeDisplay startNode = FindStartNode(); + if (startNode == null) + { + robot.StopExecution("(FAILED) Script needs exactly one Start node"); + return; + } + + availableNodes = BuildAvailableNodeLookup(); + List nodes = BuildScriptOrder(startNode, new List()); + if (nodes.Count > 0) robot.SetupExecution(nodes); + robot.currentProgram = scriptName.Text.Length <= 0 ? $"Script{availableScripts.ItemCount}" : scriptName.Text; + } + + private System.Collections.Generic.Dictionary BuildAvailableNodeLookup() + { + System.Collections.Generic.Dictionary availableNodes = + new System.Collections.Generic.Dictionary(); for (int i = 0; i < editorWindow.GetChildCount(); i++) { @@ -129,22 +157,56 @@ public partial class CodingWindow : PanelContainer if (nodeDisplay == null) continue; nodeDisplay.ReadParameters(); - ProgramNode executableNode = nodeDisplay.node.Duplicate(); - nodes.Add(executableNode); - - if (nodes.Count > 1) - { - nodes[nodes.Count - 2].nextNode = executableNode; - } - if (nodes.Count > 1) - { - executableNode.previousNode = nodes[nodes.Count - 2]; - } + availableNodes.Add(nodeDisplay.Name, nodeDisplay.node); } - if (robot == null) return; - if (nodes.Count > 0) robot.SetupExecution(nodes); - robot.currentProgram = scriptName.Text.Length <= 0 ? $"Script{availableScripts.ItemCount}" : scriptName.Text; + return availableNodes; + } + + private List BuildScriptOrder(NodeDisplay node, List program) + { + program.Add(node.node); + if (editorWindow.GetConnectionListFromNode(node.Name).Count <= 0) return program; + List nextConnections = CheckNodeConnections(node); + if (nextConnections.Count <= 0) return program; + node.node.SetNextNode(nextConnections, availableNodes); + foreach (Dictionary connection in nextConnections) + { + program = BuildScriptOrder( + editorWindow.GetNode(new NodePath(connection["to_node"].AsStringName())), + program + ); + } + return program; + } + + private List CheckNodeConnections(NodeDisplay node) + { + List result = new List(); + Array connections = editorWindow.GetConnectionListFromNode(node.Name); + for (int i = 0; i < connections.Count; i++) + { + if (connections[i]["from_node"].AsStringName() == node.Name) + { + result.Add(connections[i]); + } + } + return result; + } + + private NodeDisplay FindStartNode() + { + NodeDisplay startNode = null; + for (int i = 0; i < editorWindow.GetChildCount(); i++) + { + NodeDisplay nodeDisplay = editorWindow.GetChild(i) as NodeDisplay; + if (nodeDisplay == null) continue; + if (nodeDisplay.node is not StartNode) continue; + if (startNode != null) return null; + + startNode = nodeDisplay; + } + return startNode; } public void SetRobot(Robot robot) @@ -177,14 +239,8 @@ public partial class CodingWindow : PanelContainer if (robot == null) return; if (robot.currentNode == null) return; - ProgramNode firstNode = robot.currentNode; - while (firstNode.previousNode != null) - { - firstNode = firstNode.previousNode; - } - HashSet loadedNodes = new HashSet(); - ProgramNode nodeToLoad = firstNode; + ProgramNode nodeToLoad = robot.currentNode; while (nodeToLoad != null && !loadedNodes.Contains(nodeToLoad)) { loadedNodes.Add(nodeToLoad); @@ -238,12 +294,10 @@ public partial class CodingWindow : PanelContainer public void OnNodeConnect(StringName from, int fromPort, StringName to, int toPort) { - GD.Print($"From {fromPort} to {toPort}"); if (to == from) return; foreach (Dictionary connection in editorWindow.GetConnectionList()) { - if (connection["to_node"].AsStringName() == to && (int)connection["to_port"] == toPort) return; if (connection["from_node"].AsStringName() == from && (int)connection["from_port"] == fromPort) return; } diff --git a/Scripts/UI/DSL/NodeDisplays/StartNodeDisplay.cs b/Scripts/UI/DSL/NodeDisplays/StartNodeDisplay.cs new file mode 100644 index 0000000..230cab2 --- /dev/null +++ b/Scripts/UI/DSL/NodeDisplays/StartNodeDisplay.cs @@ -0,0 +1,7 @@ +public partial class StartNodeDisplay : NodeDisplay +{ + protected override ProgramNode CreateProgramNode() + { + return new StartNode(); + } +} diff --git a/Scripts/UI/DSL/NodeDisplays/StartNodeDisplay.cs.uid b/Scripts/UI/DSL/NodeDisplays/StartNodeDisplay.cs.uid new file mode 100644 index 0000000..70db69f --- /dev/null +++ b/Scripts/UI/DSL/NodeDisplays/StartNodeDisplay.cs.uid @@ -0,0 +1 @@ +uid://c7w5380k530wb diff --git a/Scripts/World/Pathfinding.cs b/Scripts/World/Pathfinding.cs index 471807a..a6b09bb 100644 --- a/Scripts/World/Pathfinding.cs +++ b/Scripts/World/Pathfinding.cs @@ -81,7 +81,7 @@ public class Pathfinding if (!aStar.ArePointsConnected(fromId, toId)) { - aStar.ConnectPoints(fromId, toId); + aStar.ConnectPoints(fromId, toId, true); } } } diff --git a/project.godot b/project.godot index da92f82..6b28207 100644 --- a/project.godot +++ b/project.godot @@ -15,7 +15,7 @@ compatibility/default_parent_skeleton_in_mesh_instance_3d=true [application] config/name="RuinAdventurer" -run/main_scene="uid://dlommaelbbw2b" +run/main_scene="res://Scenes/MainMenu.tscn" config/features=PackedStringArray("4.6", "C#", "Forward Plus") config/icon="res://icon.svg"