diff --git a/entities/Bee.tscn b/entities/Bee.tscn index 1d01258..ad76a7c 100644 --- a/entities/Bee.tscn +++ b/entities/Bee.tscn @@ -1,3 +1,48 @@ -[gd_scene format=3 uid="uid://deek6uv574xas"] +[gd_scene load_steps=8 format=3 uid="uid://deek6uv574xas"] -[node name="Bee" type="Node2D"] +[ext_resource type="Script" path="res://entities/scripts/bee.gd" id="1_pnu7x"] +[ext_resource type="Script" path="res://entities/scripts/finite_state_machine.gd" id="1_t3s5d"] +[ext_resource type="Script" path="res://entities/bee/states/bee_idle.gd" id="3_vasc5"] +[ext_resource type="Script" path="res://entities/bee/states/bee_traveling.gd" id="4_rr0w6"] +[ext_resource type="Script" path="res://entities/bee/states/bee_gather.gd" id="5_4vs4l"] +[ext_resource type="Script" path="res://entities/scripts/bee_hit_box.gd" id="5_agq38"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_86nxf"] +radius = 22.0907 + +[node name="Bee" type="CharacterBody2D"] +collision_mask = 0 +script = ExtResource("1_pnu7x") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="."] +light_mask = 0 +shape = SubResource("CircleShape2D_86nxf") + +[node name="HitBox" type="Area2D" parent="."] +collision_layer = 6 +collision_mask = 7 +script = ExtResource("5_agq38") + +[node name="CollisionShape2D" type="CollisionShape2D" parent="HitBox"] +light_mask = 0 +shape = SubResource("CircleShape2D_86nxf") + +[node name="StateMachine" type="Node2D" parent="." node_paths=PackedStringArray("initial_state")] +script = ExtResource("1_t3s5d") +initial_state = NodePath("Idle") + +[node name="Idle" type="Node" parent="StateMachine"] +script = ExtResource("3_vasc5") + +[node name="Traveling" type="Node" parent="StateMachine"] +script = ExtResource("4_rr0w6") + +[node name="Gathering" type="Node" parent="StateMachine"] +script = ExtResource("5_4vs4l") + +[node name="Polygon2D" type="Polygon2D" parent="."] +color = Color(1, 1, 0.0745098, 1) +polygon = PackedVector2Array(-18, -11, -6, -21, 17, -19, 23, 1, 3, 12, -18, 7) + +[connection signal="area_entered" from="HitBox" to="HitBox" method="_on_area_entered"] +[connection signal="area_exited" from="HitBox" to="HitBox" method="_on_area_exited"] diff --git a/entities/DirectorDrone.tscn b/entities/DirectorDrone.tscn index adf5cf7..9b80578 100644 --- a/entities/DirectorDrone.tscn +++ b/entities/DirectorDrone.tscn @@ -1,8 +1,19 @@ -[gd_scene format=3 uid="uid://nxq2fd04ehcu"] +[gd_scene load_steps=2 format=3 uid="uid://nxq2fd04ehcu"] + +[ext_resource type="Script" path="res://entities/scripts/director_drone.gd" id="1_3v6jp"] [node name="DirectorDrone" type="Node2D"] +script = ExtResource("1_3v6jp") [node name="Polygon2D" type="Polygon2D" parent="."] position = Vector2(1, -1) color = Color(0.703926, 0.656042, 0.441124, 1) polygon = PackedVector2Array(-28, -25, 25, -28, 26, 33, -32, 19) + +[node name="Label" type="Label" parent="."] +offset_left = -16.0 +offset_top = -12.0 +offset_right = 24.0 +offset_bottom = 11.0 +theme_override_colors/font_color = Color(0, 0, 0, 1) +text = "999" diff --git a/entities/bee/states/bee_gather.gd b/entities/bee/states/bee_gather.gd new file mode 100644 index 0000000..3711147 --- /dev/null +++ b/entities/bee/states/bee_gather.gd @@ -0,0 +1,29 @@ +extends State +class_name BeeGathering + +@export var animator : AnimationPlayer +@onready var bee = get_parent().get_parent() as Bee # I think this is bad but I dont care it works + +var time_at_patch : float = 0.0 + +func enter(_msg := {}): + Log.pr("I am going to attempt to gather some stuff!") + #animator.play("Idle") + + #if !bee.in_range_of_flowers: + # ## Go home bee, you're drunk + # state_transition.emit(self, "Idle") + + + +func update(_delta : float): + + if bee.in_range_of_flowers: + #animator.play("Gathering") + time_at_patch += _delta + if time_at_patch > 5.0: + bee.nectar += 1 + state_transition.emit(self, "Idle") + else: + state_transition.emit(self, "Idle") + diff --git a/entities/bee/states/bee_idle.gd b/entities/bee/states/bee_idle.gd new file mode 100644 index 0000000..c1f62e3 --- /dev/null +++ b/entities/bee/states/bee_idle.gd @@ -0,0 +1,37 @@ +extends State +class_name BeeIdle + +@export var animator : AnimationPlayer + +@onready var bee = get_parent().get_parent() # I think this is bad but I dont care it works + +var idle_time : float = 0.0 + +func _ready(): + Log.pr(bee, bee.nectar) + +func enter(_msg := {}): + Log.pr("I sit by idl-bee") + #animator.play("Idle") + pass + +func update(delta : float): + + idle_time += delta + + if idle_time > 2.0: + Log.pr("Been idle for 2 seconds, check for something to do...") + find_something_to_do() + idle_time = 0.0 + pass + +func find_something_to_do(): + if bee.nectar > 0: + ## Bee has pollen - head home + Log.pr(bee, "I have nectar, I should return to the hive") + state_transition.emit(self, "Traveling") + else: + ## Bee has no pollen - they should move to a flower + Log.pr(bee, "I have no nectar, I should find a flower") + state_transition.emit(self, "Traveling") + pass diff --git a/entities/bee/states/bee_traveling.gd b/entities/bee/states/bee_traveling.gd new file mode 100644 index 0000000..b29abf3 --- /dev/null +++ b/entities/bee/states/bee_traveling.gd @@ -0,0 +1,37 @@ +extends State +class_name BeeTraveling + +@export var animator : AnimationPlayer + +@export var target : Drone = null + +@onready var bee = get_parent().get_parent() as Bee # I think this is bad but I dont care it works + +var t = 0 + +func enter(_msg := {}): + Log.pr("I am on the move!") + ## Get the next target location from the bee + target = bee.get_next_target() + + #animator.play("Idle") + + pass + +func update(delta : float): + + if target: + if bee.position.distance_to(target.position) > 3: + + bee.velocity = (target.get_global_position() - bee.position).normalized() * bee.speed * delta + bee.move_and_collide(bee.velocity) + bee.look_at(target.position) + + else: + # Bee has arrived at location, if its the hive or a collector drone do the things + if(target.name == "CollectorDrone"): + state_transition.emit(self, "Gathering") + else: + state_transition.emit(self, "Idle") + + pass diff --git a/entities/scripts/bee.gd b/entities/scripts/bee.gd new file mode 100644 index 0000000..0eb7e55 --- /dev/null +++ b/entities/scripts/bee.gd @@ -0,0 +1,55 @@ +extends Node2D +class_name Bee + +@onready var fsm = $StateMachine as FiniteStateMachine +@onready var drone_manager = get_tree().get_first_node_in_group("dronemanager") as DroneManager + +@export var nectar : int = 0 +@export var speed : int = 30 + +var latest_target_director : int = 0 + +# This is updated when the bee enters or exits a flower patch +var in_range_of_flowers : bool = false + +func _ready(): + Log.pr("I have never bee-n so ready!") + + Log.pr(fsm.current_state.name) + +## Get the next target to move to +## If we have no nectar, we need to go up the director list +## If we have nectar, we need to go down the director list +## If we are at the lower director, we need to go the hive +## If we are at the highest director, we need to go to a flower +func get_next_target(): + if nectar == 0: + Log.pr("I have no nectar") + + ## If there is a next directory drone, lets go to it + var next_drone = drone_manager.get_next_director(latest_target_director) + if next_drone: + Log.pr("Next drone target", next_drone) + latest_target_director = next_drone.visit_order + return next_drone + + ## If there is no next drone, check for a collector drone + var collector_drone = drone_manager.get_collector() + if collector_drone: + Log.pr("Collector drone target", collector_drone) + return collector_drone + else: + Log.pr("I have nectar") + + ## Let's go home, we need the previous director drones location + var previous_drone = drone_manager.get_previous_director(latest_target_director) + if previous_drone: + Log.pr("Previous drone target", previous_drone) + latest_target_director = previous_drone.visit_order + return previous_drone + + pass + + + + diff --git a/entities/scripts/bee_hit_box.gd b/entities/scripts/bee_hit_box.gd new file mode 100644 index 0000000..51b9cec --- /dev/null +++ b/entities/scripts/bee_hit_box.gd @@ -0,0 +1,19 @@ +extends Area2D + +@onready var bee = get_parent() as Bee + +func _ready(): + pass + + +func _on_area_entered(area:Area2D): + ## Check if the area entered is a flower patch + if area.is_in_group("flowers"): + bee.in_range_of_flowers = true + Log.pr("I found some flowers!") + +func _on_area_exited(area:Area2D): + ## Check if the area exited is a flower patch + if area.is_in_group("flowers"): + bee.in_range_of_flowers = false + Log.pr("I left the flowers!") \ No newline at end of file diff --git a/entities/scripts/director_drone.gd b/entities/scripts/director_drone.gd index a6454ba..c3f448b 100644 --- a/entities/scripts/director_drone.gd +++ b/entities/scripts/director_drone.gd @@ -1,11 +1,17 @@ class_name DirectorDrone extends Drone +@export var visit_order : int = 0 : + get: + return visit_order + set(value): + visit_order = value + update_label_value() -# Called when the node enters the scene tree for the first time. func _ready(): - pass # Replace with function body. + pass - -# Called every frame. 'delta' is the elapsed time since the previous frame. func _process(_delta): pass + +func update_label_value(): + $Label.text = str(visit_order) \ No newline at end of file diff --git a/entities/scripts/finite_state_machine.gd b/entities/scripts/finite_state_machine.gd new file mode 100644 index 0000000..06c0d02 --- /dev/null +++ b/entities/scripts/finite_state_machine.gd @@ -0,0 +1,69 @@ +@icon("res://resources/icons/fsm.png") +extends Node +class_name FiniteStateMachine + +var states : Dictionary = {} +var current_state : State +@export var initial_state : State + +#NOTE This is a generic finite_state_machine, it handles all states, changes to this code will affect + # everything that uses a state machine! + +func _ready(): + Log.pr("FSM Ready") + for child in get_children(): + if child is State: + states[child.name.to_lower()] = child + child.state_transition.connect(change_state) + + Log.pr(states) + + if initial_state: + initial_state.enter() + current_state = initial_state + +# Call the current states update function +func _process(delta): + if current_state: + current_state.update(delta) + +# Use force_change_state cautiously, it immediately switches to a state regardless of any transitions. +# This is used to force us into a 'death state' when killed +func force_change_state(new_state : String): + var newState = states.get(new_state.to_lower()) + + if !newState: + print(new_state + " does not exist in the dictionary of states") + return + + if current_state == newState: + print("State is same, aborting") + return + + # NOTE Calling exit like so: (current_state.Exit()) may cause warnings when flushing queries, like when the enemy is being removed after death. + # call_deferred is safe and prevents this from occuring. We get the Exit function from the state as a callable and then call it in a thread-safe manner + if current_state: + var exit_callable = Callable(current_state, "exit") + exit_callable.call_deferred() + + newState.enter() + + current_state = newState + +func change_state(source_state : State, new_state_name : String): + if source_state != current_state: + #print("Invalid change_state trying from: " + source_state.name + " but currently in: " + current_state.name) + #This typically only happens when trying to switch from death state following a force_change + return + + var new_state = states.get(new_state_name.to_lower()) + if !new_state: + print("New state is empty") + return + + if current_state: + current_state.exit() + + new_state.enter() + + current_state = new_state diff --git a/entities/scripts/state.gd b/entities/scripts/state.gd new file mode 100644 index 0000000..f92365e --- /dev/null +++ b/entities/scripts/state.gd @@ -0,0 +1,15 @@ +@icon("res://resources/icons/fsm.png") +extends Node +class_name State + +signal state_transition + +func enter(_msg := {}): + pass + +func exit(): + pass + +func update(_delta:float): + pass + diff --git a/project.godot b/project.godot index 9d814a3..7b0fc16 100644 --- a/project.godot +++ b/project.godot @@ -24,3 +24,9 @@ window/vsync/vsync_mode=0 [editor_plugins] enabled=PackedStringArray("res://addons/log/plugin.cfg") + +[layer_names] + +2d_physics/layer_1="bees" +2d_physics/layer_2="hazards" +2d_physics/layer_3="flowers" diff --git a/resources/icons/fsm.png b/resources/icons/fsm.png new file mode 100644 index 0000000..262b2e3 Binary files /dev/null and b/resources/icons/fsm.png differ diff --git a/resources/icons/fsm.png.import b/resources/icons/fsm.png.import new file mode 100644 index 0000000..717ffdd --- /dev/null +++ b/resources/icons/fsm.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://ct5paeky0cexp" +path="res://.godot/imported/fsm.png-8987802cee881c94358eee3cb13f0556.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://resources/icons/fsm.png" +dest_files=["res://.godot/imported/fsm.png-8987802cee881c94358eee3cb13f0556.ctex"] + +[params] + +compress/mode=0 +compress/high_quality=false +compress/lossy_quality=0.7 +compress/hdr_compression=1 +compress/normal_map=0 +compress/channel_pack=0 +mipmaps/generate=false +mipmaps/limit=-1 +roughness/mode=0 +roughness/src_normal="" +process/fix_alpha_border=true +process/premult_alpha=false +process/normal_map_invert_y=false +process/hdr_as_srgb=false +process/hdr_clamp_exposure=false +process/size_limit=0 +detect_3d/compress_to=1 diff --git a/scenes/scripts/bee_spawner.gd b/scenes/scripts/bee_spawner.gd new file mode 100644 index 0000000..d247eba --- /dev/null +++ b/scenes/scripts/bee_spawner.gd @@ -0,0 +1,29 @@ +extends Node2D +class_name BeeSpawner + +var bee = preload("res://entities/Bee.tscn") + +@onready var beehive = get_node("../Beehive") + +var bee_count = 0 +var max_bees = 10 +var spawn_interval = 3 +var spawn_timer = 0.0 + +func _ready(): + Log.pr("Bee Spawner ready") + +func spawn_bee(): + var bee_instance = bee.instantiate() + add_child(bee_instance) + bee_instance.position = beehive.position + # bee_instance.connect("bee_died", self, "bee_died") + +func _process(delta): + spawn_timer += delta + + if spawn_timer > spawn_interval and bee_count < max_bees: + spawn_bee() + spawn_timer = 0.0 + bee_count += 1 + Log.pr("Bee count: " + str(bee_count)) diff --git a/scenes/scripts/drone_manager.gd b/scenes/scripts/drone_manager.gd index d3a7760..19f1245 100644 --- a/scenes/scripts/drone_manager.gd +++ b/scenes/scripts/drone_manager.gd @@ -1,8 +1,11 @@ extends Node2D +class_name DroneManager var spawning_drone : bool = false var spawning_type : String = "" +var director_drones : Array = [] # List of all director drones in the world + @onready var drone_controls = %DroneControls @onready var spawned_drones_container = get_node("SpawnedDrones") @onready var cursor = preload("res://resources/cursors/launch_drone.png") @@ -33,6 +36,7 @@ func spawn_drone(drone_type : String) -> void: # Create a new instance of the drone if drone_type == "director": new_drone = director_drone.instantiate() + # new_drone.visit_order = spawned_drones_container.get_child_count() elif drone_type == "dancer": new_drone = dancer_drone.instantiate() elif drone_type == "distractor": @@ -45,6 +49,12 @@ func spawn_drone(drone_type : String) -> void: spawned_drones_container.add_child(new_drone) new_drone.position = get_viewport().get_mouse_position() + if drone_type == "director": + # Set this drones visit order to the next in line + new_drone.visit_order = director_drones.size() + 1 + # Update the director drone list + update_director_drone_list() + func place_drone(drone_type : String) -> void: if !spawning_drone: Input.set_custom_mouse_cursor(cursor, Input.CURSOR_ARROW, Vector2(32, 32)) @@ -53,8 +63,6 @@ func place_drone(drone_type : String) -> void: spawning_drone = true spawning_type = drone_type - - func cancel_spawning() -> void: Input.set_custom_mouse_cursor(null) drone_controls.reset_button_focus() @@ -73,3 +81,31 @@ func _on_spawn_distractor_pressed() -> void: func _on_spawn_dancer_pressed() -> void: place_drone("dancer") + +func update_director_drone_list(): + director_drones.clear() + for drone in spawned_drones_container.get_children(): + if drone is DirectorDrone: + director_drones.append(drone) + + Log.pr(director_drones.size()) + +func get_next_director(current_director_number : int) -> DirectorDrone: + for drone in director_drones: + if drone.visit_order == current_director_number + 1: + return drone + return null + +func get_previous_director(current_director_number : int) -> DirectorDrone: + for drone in director_drones: + if drone.visit_order == current_director_number - 1: + return drone + return null + +## For now this just returns the first collector drone it finds +## This will need to be updated to return the closest collector drone potentially? +func get_collector(): + for drone in spawned_drones_container.get_children(): + if drone is CollectorDrone: + return drone + return null \ No newline at end of file diff --git a/scenes/test_level.tscn b/scenes/test_level.tscn index 680aa1f..e1cea4a 100644 --- a/scenes/test_level.tscn +++ b/scenes/test_level.tscn @@ -1,8 +1,13 @@ -[gd_scene load_steps=4 format=3 uid="uid://mk5n0hrwk4yi"] +[gd_scene load_steps=7 format=3 uid="uid://mk5n0hrwk4yi"] [ext_resource type="Script" path="res://scenes/scripts/test_level.gd" id="1_lgt1m"] [ext_resource type="Script" path="res://scenes/scripts/drone_manager.gd" id="2_474nc"] +[ext_resource type="Script" path="res://scenes/scripts/bee_spawner.gd" id="2_qqqq4"] [ext_resource type="Script" path="res://scenes/scripts/drone_controls.gd" id="3_rqkyv"] +[ext_resource type="PackedScene" uid="uid://deek6uv574xas" path="res://entities/Bee.tscn" id="4_336fp"] + +[sub_resource type="CircleShape2D" id="CircleShape2D_usqp5"] +radius = 142.316 [node name="TestLevel" type="Node2D"] script = ExtResource("1_lgt1m") @@ -18,11 +23,25 @@ color = Color(0.588235, 0.423529, 0.423529, 1) polygon = PackedVector2Array(95, 146, 203, 134, 186, 274, 78, 287) [node name="Flower" type="Polygon2D" parent="."] -position = Vector2(648, 225) -scale = Vector2(0.435897, 0.276596) +position = Vector2(278, -97) color = Color(0.301961, 0.607843, 0.901961, 1) polygon = PackedVector2Array(752, 145, 875, 200, 893, 272, 866, 359, 781, 427, 715, 362, 659, 226) +[node name="Area2D" type="Area2D" parent="Flower" groups=["flowers"]] +position = Vector2(777.707, 282) +collision_layer = 7 +collision_mask = 7 + +[node name="CollisionShape2D" type="CollisionShape2D" parent="Flower/Area2D"] +shape = SubResource("CircleShape2D_usqp5") + +[node name="Label" type="Label" parent="Flower"] +offset_left = 721.0 +offset_top = 243.0 +offset_right = 852.0 +offset_bottom = 266.0 +text = "Flower patch bro" + [node name="Pesticide" type="Polygon2D" parent="."] position = Vector2(74.7948, -103.963) scale = Vector2(0.851611, 0.815599) @@ -34,7 +53,11 @@ position = Vector2(-354, 53) color = Color(0.803922, 0.407843, 0.239216, 1) polygon = PackedVector2Array(819, 301, 866, 278, 888, 364, 842, 319, 832, 373, 806, 423, 775, 424, 754, 377, 762, 319, 728, 360, 739, 263, 765, 284) -[node name="DroneManager" type="Node2D" parent="."] +[node name="BeeSpawner" type="Node2D" parent="."] +script = ExtResource("2_qqqq4") + +[node name="DroneManager" type="Node2D" parent="." groups=["dronemanager"]] +unique_name_in_owner = true script = ExtResource("2_474nc") [node name="SpawnedDrones" type="Node2D" parent="DroneManager"] @@ -89,6 +112,9 @@ layout_mode = 2 tooltip_text = "Spawn a dancing drone that will encourage bees to leave the hive. Best to put this near to the hive. " text = "Dancer" +[node name="Bee" parent="." instance=ExtResource("4_336fp")] +position = Vector2(704, 196) + [connection signal="pressed" from="DroneManager/Control/MarginContainer/DroneControls/SpawnDirector" to="DroneManager" method="_on_spawn_director_pressed"] [connection signal="pressed" from="DroneManager/Control/MarginContainer/DroneControls/SpawnCollector" to="DroneManager" method="_on_spawn_collector_pressed"] [connection signal="pressed" from="DroneManager/Control/MarginContainer/DroneControls/SpawnDistractor" to="DroneManager" method="_on_spawn_distractor_pressed"]