diff --git a/Scenes/Node Types/Audio.tscn b/Scenes/Node Types/Audio.tscn
new file mode 100644
index 0000000..c0aea4a
--- /dev/null
+++ b/Scenes/Node Types/Audio.tscn	
@@ -0,0 +1,17 @@
+[gd_scene load_steps=3 format=3 uid="uid://lossothjt5ye"]
+
+[ext_resource type="PackedScene" uid="uid://b0arjg8r75f8y" path="res://Scenes/Nodes/Node.tscn" id="1_db1pk"]
+[ext_resource type="Script" uid="uid://bmukxrwmoyc20" path="res://Scripts/GL_Audio.gd" id="2_db1pk"]
+
+[node name="Node" type="Control"]
+layout_mode = 3
+anchors_preset = 0
+mouse_filter = 1
+
+[node name="Node" parent="." groups=["GL Node"] instance=ExtResource("1_db1pk")]
+layout_mode = 0
+tooltip_text = "Outputs the path to an audio source saved in the workspace. Does not output live audio- playback on nodes this is plugged into must be handled by a Timeline node or similar."
+script = ExtResource("2_db1pk")
+
+[connection signal="mouse_entered" from="Node" to="Node" method="mouse_enter"]
+[connection signal="mouse_exited" from="Node" to="Node" method="mouse_exit"]
diff --git a/Scenes/Node Types/Direct Output.tscn b/Scenes/Node Types/Direct Output.tscn
new file mode 100644
index 0000000..629abdc
--- /dev/null
+++ b/Scenes/Node Types/Direct Output.tscn	
@@ -0,0 +1,20 @@
+[gd_scene load_steps=3 format=3 uid="uid://b03x861ratqbj"]
+
+[ext_resource type="PackedScene" uid="uid://b0arjg8r75f8y" path="res://Scenes/Nodes/Node.tscn" id="1_uifvy"]
+[ext_resource type="Script" uid="uid://t8bsiegtsiwo" path="res://Scripts/GL_Output.gd" id="2_nkf8v"]
+
+[node name="Node" type="Control"]
+layout_mode = 3
+anchors_preset = 0
+mouse_filter = 1
+
+[node name="Node" parent="." groups=["GL Node"] instance=ExtResource("1_uifvy")]
+layout_mode = 0
+tooltip_text = "Controls Chica's movements. All eyelid movements can be set to a particular position using values of 0.0 to 1.0."
+script = ExtResource("2_nkf8v")
+identification = "DIRECT_OUTPUT"
+names = PackedStringArray("Audio", "Volume", "Current Time")
+types = PackedStringArray("audio", "float", "float")
+
+[connection signal="mouse_entered" from="Node" to="Node" method="mouse_enter"]
+[connection signal="mouse_exited" from="Node" to="Node" method="mouse_exit"]
diff --git a/Scenes/Node Types/Mix Floats.tscn b/Scenes/Node Types/Mix Floats.tscn
new file mode 100644
index 0000000..e90b6e0
--- /dev/null
+++ b/Scenes/Node Types/Mix Floats.tscn	
@@ -0,0 +1,17 @@
+[gd_scene load_steps=3 format=3 uid="uid://b83i85vl6gd01"]
+
+[ext_resource type="PackedScene" uid="uid://b0arjg8r75f8y" path="res://Scenes/Nodes/Node.tscn" id="1_njggm"]
+[ext_resource type="Script" uid="uid://beit3xudynjdl" path="res://Scripts/GL_Mix_Floats.gd" id="2_njggm"]
+
+[node name="Node" type="Control"]
+layout_mode = 3
+anchors_preset = 0
+mouse_filter = 1
+
+[node name="Node" parent="." groups=["GL Node"] instance=ExtResource("1_njggm")]
+layout_mode = 0
+tooltip_text = "Mixes two floats (numbers) together using the 'Factor', with 0.0 being fully Float A, and 1.0 being fully Float B."
+script = ExtResource("2_njggm")
+
+[connection signal="mouse_entered" from="Node" to="Node" method="mouse_enter"]
+[connection signal="mouse_exited" from="Node" to="Node" method="mouse_exit"]
diff --git a/Scenes/Node Types/Mouse Wheel.tscn b/Scenes/Node Types/Mouse Wheel.tscn
index b909118..1a921fd 100644
--- a/Scenes/Node Types/Mouse Wheel.tscn	
+++ b/Scenes/Node Types/Mouse Wheel.tscn	
@@ -1,4 +1,4 @@
-[gd_scene load_steps=3 format=3 uid="uid://bhkp4bfwm1agf"]
+[gd_scene load_steps=3 format=3 uid="uid://d2da0nd32yqo7"]
 
 [ext_resource type="PackedScene" uid="uid://b0arjg8r75f8y" path="res://Scenes/Nodes/Node.tscn" id="1_o85ib"]
 [ext_resource type="Script" uid="uid://y8j8wap2o4oe" path="res://Scripts/GL_Mouse_Wheel.gd" id="2_o85ib"]
diff --git a/Scenes/Nodes/Node Add.tscn b/Scenes/Nodes/Node Add.tscn
index e421276..923de08 100644
--- a/Scenes/Nodes/Node Add.tscn	
+++ b/Scenes/Nodes/Node Add.tscn	
@@ -7,13 +7,15 @@ offset_right = 68.0
 offset_bottom = 31.0
 selected = 0
 allow_reselect = true
-item_count = 3
+item_count = 4
 popup/item_0/text = "+Add Float"
 popup/item_0/id = 0
 popup/item_1/text = "+Add Bool"
 popup/item_1/id = 1
 popup/item_2/text = "+Add Color"
 popup/item_2/id = 2
+popup/item_3/text = "+Add Audio"
+popup/item_3/id = 3
 script = ExtResource("1_vw1dw")
 
 [node name="Panel" type="PanelContainer" parent="."]
diff --git a/Scenes/Nodes/Node Row.tscn b/Scenes/Nodes/Node Row.tscn
index 88e9a20..aa5e458 100644
--- a/Scenes/Nodes/Node Row.tscn	
+++ b/Scenes/Nodes/Node Row.tscn	
@@ -1,9 +1,10 @@
-[gd_scene load_steps=5 format=3 uid="uid://bdcxusbd86oox"]
+[gd_scene load_steps=6 format=3 uid="uid://bdcxusbd86oox"]
 
 [ext_resource type="Script" uid="uid://dwl36vn5chqmq" path="res://Scripts/GL_Node_Point.gd" id="1_fygh4"]
 [ext_resource type="Script" uid="uid://q5qlhwvjb16w" path="res://Scripts/GL_Node_Picker_Float.gd" id="2_vlx6y"]
 [ext_resource type="Script" uid="uid://dflftb37a7ind" path="res://Scripts/GL_Node_Picker_Color.gd" id="3_tf34m"]
 [ext_resource type="Script" uid="uid://b7ysqwtxh8pf" path="res://Scripts/GL_Node_Picker_Bool.gd" id="4_yal7b"]
+[ext_resource type="Script" uid="uid://rmlqvxot3kys" path="res://Scripts/GL_Node_Picker_Audio.gd" id="5_yal7b"]
 
 [node name="Node Row" type="HBoxContainer"]
 
@@ -41,6 +42,30 @@ custom_minimum_size = Vector2(50, 0)
 layout_mode = 2
 script = ExtResource("4_yal7b")
 
+[node name="Pick Audio" type="PanelContainer" parent="."]
+visible = false
+layout_mode = 2
+script = ExtResource("5_yal7b")
+
+[node name="HBox" type="HBoxContainer" parent="Pick Audio"]
+layout_mode = 2
+
+[node name="OptionButton" type="OptionButton" parent="Pick Audio/HBox"]
+layout_mode = 2
+allow_reselect = true
+
+[node name="Button" type="Button" parent="Pick Audio/HBox"]
+layout_mode = 2
+text = "🗀"
+
+[node name="FileDialog" type="FileDialog" parent="Pick Audio"]
+title = "Open a File"
+initial_position = 1
+size = Vector2i(960, 480)
+ok_button_text = "Open"
+file_mode = 0
+access = 2
+
 [node name="Output" type="Button" parent="." groups=["Outputs"]]
 layout_mode = 2
 mouse_default_cursor_shape = 2
@@ -53,4 +78,6 @@ script = ExtResource("1_fygh4")
 [connection signal="value_changed" from="Pick Float" to="Pick Float" method="value_changed"]
 [connection signal="color_changed" from="Pick Color" to="Pick Color" method="color_changed"]
 [connection signal="toggled" from="Pick Bool" to="Pick Bool" method="toggled"]
+[connection signal="item_selected" from="Pick Audio/HBox/OptionButton" to="Pick Audio" method="_on_audio_option_selected"]
+[connection signal="pressed" from="Pick Audio/HBox/Button" to="Pick Audio" method="_on_audio_button_pressed"]
 [connection signal="button_down" from="Output" to="Output" method="_start_drag"]
diff --git a/Scenes/Nodes/Node.tscn b/Scenes/Nodes/Node.tscn
index a6a437f..5637091 100644
--- a/Scenes/Nodes/Node.tscn
+++ b/Scenes/Nodes/Node.tscn
@@ -1,7 +1,9 @@
-[gd_scene load_steps=2 format=3 uid="uid://b0arjg8r75f8y"]
+[gd_scene load_steps=3 format=3 uid="uid://b0arjg8r75f8y"]
 
 [ext_resource type="Theme" uid="uid://b3wjoiiv6sq22" path="res://UI/Themes/Default.tres" id="1_arhwt"]
 
+[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_arhwt"]
+
 [node name="Node" type="PanelContainer"]
 clip_contents = true
 custom_minimum_size = Vector2(200, 100)
@@ -21,12 +23,10 @@ layout_mode = 2
 [node name="Title" type="HBoxContainer" parent="Margins/Holder"]
 layout_mode = 2
 
-[node name="Title Label" type="Label" parent="Margins/Holder/Title"]
+[node name="Title Label" type="LineEdit" parent="Margins/Holder/Title"]
 layout_mode = 2
 size_flags_horizontal = 3
-text = "Test"
-horizontal_alignment = 1
-clip_text = true
+theme_override_styles/normal = SubResource("StyleBoxEmpty_arhwt")
 
 [node name="Exit Button" type="Button" parent="Margins/Holder/Title"]
 layout_mode = 2
diff --git a/Scenes/UI/Node Map.tscn b/Scenes/UI/Node Map.tscn
index c7b94b7..83849f3 100644
--- a/Scenes/UI/Node Map.tscn	
+++ b/Scenes/UI/Node Map.tscn	
@@ -1,16 +1,16 @@
-[gd_scene load_steps=5 format=3 uid="uid://c57u187iciexi"]
+[gd_scene load_steps=6 format=3 uid="uid://c57u187iciexi"]
 
 [ext_resource type="Script" uid="uid://i4p62x8fnqpn" path="res://Scripts/GL_Node_Map.gd" id="1_jyqbx"]
 [ext_resource type="PackedScene" uid="uid://mowdu1i1rldt" path="res://Scenes/UI/Search.tscn" id="1_xwfut"]
+[ext_resource type="Theme" uid="uid://b3wjoiiv6sq22" path="res://UI/Themes/Default.tres" id="2_2eix6"]
 
-[sub_resource type="Gradient" id="Gradient_jyqbx"]
-offsets = PackedFloat32Array(0)
-colors = PackedColorArray(0.121569, 0.121569, 0.121569, 0.501961)
+[sub_resource type="Gradient" id="Gradient_xwfut"]
+colors = PackedColorArray(0.448074, 0.0582233, 0.099986, 1, 0.330802, 0.066494, 0.0423852, 1)
 
-[sub_resource type="GradientTexture1D" id="GradientTexture1D_2eix6"]
-gradient = SubResource("Gradient_jyqbx")
+[sub_resource type="GradientTexture1D" id="GradientTexture1D_jyqbx"]
+gradient = SubResource("Gradient_xwfut")
 
-[node name="NodeMap" type="Control"]
+[node name="NodeMap" type="Control" groups=["Node Map"]]
 layout_mode = 3
 anchors_preset = 15
 anchor_right = 1.0
@@ -26,22 +26,72 @@ anchor_right = 1.0
 anchor_bottom = 1.0
 grow_horizontal = 2
 grow_vertical = 2
-texture = SubResource("GradientTexture1D_2eix6")
-stretch_mode = 1
-
-[node name="Label" type="Label" parent="."]
-layout_mode = 0
-offset_left = 4.0
-offset_top = 4.0
-offset_right = 249.0
-offset_bottom = 27.0
-text = "Editing nodes, press ESC to exit."
-
-[node name="Search" parent="." instance=ExtResource("1_xwfut")]
-visible = false
-layout_mode = 1
+texture = SubResource("GradientTexture1D_jyqbx")
 
 [node name="Holder" type="Control" parent="."]
 anchors_preset = 0
 offset_right = 40.0
 offset_bottom = 40.0
+
+[node name="Label" type="Label" parent="."]
+z_index = 1000
+layout_mode = 1
+anchors_preset = 3
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -1143.0
+offset_top = -97.0
+offset_right = -11.0
+offset_bottom = -48.0
+grow_horizontal = 0
+grow_vertical = 0
+theme = ExtResource("2_2eix6")
+theme_override_font_sizes/font_size = 8
+text = "Give LIFE: TEST_C_ETCHINGS
+Press Esc for Nodes, Right Click to search node.
+Tab toggles background.Middle Click hold to pan. 
+Scroll to Zoom. Hover things for tooltips."
+horizontal_alignment = 2
+
+[node name="MarginContainer" type="MarginContainer" parent="."]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+mouse_filter = 2
+theme_override_constants/margin_left = 10
+theme_override_constants/margin_top = 10
+theme_override_constants/margin_right = 10
+theme_override_constants/margin_bottom = 10
+
+[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer"]
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 8
+
+[node name="OptionButton" type="OptionButton" parent="MarginContainer/HBoxContainer"]
+layout_mode = 2
+size_flags_horizontal = 8
+size_flags_vertical = 8
+selected = 0
+item_count = 1
+popup/item_0/text = "Test"
+popup/item_0/id = 0
+
+[node name="Button" type="Button" parent="MarginContainer/HBoxContainer"]
+visible = false
+layout_mode = 2
+text = "Edit"
+
+[node name="Button2" type="Button" parent="MarginContainer/HBoxContainer"]
+layout_mode = 2
+text = "Save"
+
+[node name="Search" parent="." instance=ExtResource("1_xwfut")]
+layout_mode = 1
+
+[connection signal="pressed" from="MarginContainer/HBoxContainer/Button2" to="." method="save_everything"]
diff --git a/Scripts/GL_Audio.gd b/Scripts/GL_Audio.gd
new file mode 100644
index 0000000..2342b98
--- /dev/null
+++ b/Scripts/GL_Audio.gd
@@ -0,0 +1,14 @@
+extends GL_Node
+
+func _ready():
+	super._ready()
+	_set_title("Audio")
+	_create_row("Output",null,GL_AudioType.new(),true,GL_AudioType.new(),0)
+	_update_visuals()
+
+func _process(delta):
+	super._process(delta)
+	apply_pick_values()
+	for key in rows:
+		rows[key]["output"] = rows[key]["input"]
+	_send_input("Output")
diff --git a/Scripts/GL_Audio.gd.uid b/Scripts/GL_Audio.gd.uid
new file mode 100644
index 0000000..6af1349
--- /dev/null
+++ b/Scripts/GL_Audio.gd.uid
@@ -0,0 +1 @@
+uid://bmukxrwmoyc20
diff --git a/Scripts/GL_AudioType.gd b/Scripts/GL_AudioType.gd
new file mode 100644
index 0000000..6c2fa18
--- /dev/null
+++ b/Scripts/GL_AudioType.gd
@@ -0,0 +1,3 @@
+extends Node
+class_name GL_AudioType
+var value:String = ""
diff --git a/Scripts/GL_AudioType.gd.uid b/Scripts/GL_AudioType.gd.uid
new file mode 100644
index 0000000..a45c19a
--- /dev/null
+++ b/Scripts/GL_AudioType.gd.uid
@@ -0,0 +1 @@
+uid://dfxs3vmqxy1eu
diff --git a/Scripts/GL_Keystrokes.gd b/Scripts/GL_Keystrokes.gd
new file mode 100644
index 0000000..2b916ce
--- /dev/null
+++ b/Scripts/GL_Keystrokes.gd
@@ -0,0 +1,37 @@
+extends GL_Node
+
+func _ready():
+	super._ready()
+	_set_title("Keystrokes")
+	_create_row("KEY #1",null,false,false,0.0,1)
+	_create_row("KEY #2",null,false,false,0.0,1)
+	_create_row("KEY #3",null,false,false,0.0,1)
+	_create_row("KEY #4",null,false,false,0.0,1)
+	_create_row("KEY #5",null,false,false,0.0,1)
+	_create_row("KEY #6",null,false,false,0.0,1)
+	_create_row("KEY #7",null,false,false,0.0,1)
+	_create_row("KEY #8",null,false,false,0.0,1)
+	_create_row("KEY #9",null,false,false,0.0,1)
+	_create_row("KEY #0",null,false,false,0.0,1)
+	_update_visuals()
+	
+func _process(delta):
+	super._process(delta)
+
+	var key_map = {
+		"KEY #1": KEY_1,
+		"KEY #2": KEY_2,
+		"KEY #3": KEY_3,
+		"KEY #4": KEY_4,
+		"KEY #5": KEY_5,
+		"KEY #6": KEY_6,
+		"KEY #7": KEY_7,
+		"KEY #8": KEY_8,
+		"KEY #9": KEY_9,
+		"KEY #0": KEY_0,
+	}
+
+	for key_name in key_map.keys():
+		var is_pressed = Input.is_key_pressed(key_map[key_name]) or Input.is_key_pressed(key_map[key_name] + (KEY_KP_0 - KEY_0))
+		rows[key_name]["output"] = is_pressed
+		_send_input(key_name)
diff --git a/Scripts/GL_Keystrokes.gd.uid b/Scripts/GL_Keystrokes.gd.uid
new file mode 100644
index 0000000..adb7631
--- /dev/null
+++ b/Scripts/GL_Keystrokes.gd.uid
@@ -0,0 +1 @@
+uid://e6v6exlrhtaq
diff --git a/Scripts/GL_Mix_Floats.gd b/Scripts/GL_Mix_Floats.gd
new file mode 100644
index 0000000..cb18139
--- /dev/null
+++ b/Scripts/GL_Mix_Floats.gd
@@ -0,0 +1,16 @@
+extends GL_Node
+
+func _ready():
+	super._ready()
+	_set_title("Mix Floats")
+	_create_row("Factor",0.0,0.0,false,null,0)
+	_create_row("Float A",0.0,null,true,0.0,1.0)
+	_create_row("Float B",0.0,null,true,0.0,1.0)
+	_update_visuals()
+
+func _process(delta):
+	super._process(delta)
+	apply_pick_values()
+			
+	rows["Factor"]["output"] = lerp(float(rows["Float A"]["input"]),float(rows["Float B"]["input"]),rows["Factor"]["input"])
+	_send_input("Factor")
diff --git a/Scripts/GL_Mix_Floats.gd.uid b/Scripts/GL_Mix_Floats.gd.uid
new file mode 100644
index 0000000..473ba1d
--- /dev/null
+++ b/Scripts/GL_Mix_Floats.gd.uid
@@ -0,0 +1 @@
+uid://beit3xudynjdl
diff --git a/Scripts/GL_Mouse_Wheel.gd b/Scripts/GL_Mouse_Wheel.gd
index 3dec311..7872464 100644
--- a/Scripts/GL_Mouse_Wheel.gd
+++ b/Scripts/GL_Mouse_Wheel.gd
@@ -13,7 +13,7 @@ func _process(delta):
 	
 	_send_input("Output")
 
-func _input(event):
+func _unhandled_input(event):
 	# Check if the mouse wheel up or down button is pressed
 	if event is InputEventMouseButton:
 		if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
diff --git a/Scripts/GL_Node.gd b/Scripts/GL_Node.gd
index 81f068c..d57d874 100644
--- a/Scripts/GL_Node.gd
+++ b/Scripts/GL_Node.gd
@@ -1,7 +1,8 @@
 extends PanelContainer
 class_name GL_Node
 var rows : Dictionary
-var uuid : int #REMEMBER TO SET THIS ON CREATION
+var uuid : String
+var nodePath:String
 var dragging : bool
 var canDrag : bool
 var dragOffset : Vector2
@@ -18,6 +19,15 @@ func _ready():
 func _process(delta):
 	if dragging:
 		position = get_viewport().get_mouse_position() + dragOffset
+	for key in rows:
+		for connection in rows[key].get("connections",[]):
+			if typeof(connection.target) == TYPE_STRING:
+				for node in get_tree().get_nodes_in_group("GL Node"):
+					if node is GL_Node:
+						if node.uuid == connection.target:
+							connection.target = node
+							break
+			
 		
 func _input(event): 
 	if event is InputEventMouseButton:
@@ -30,7 +40,7 @@ func _input(event):
 func _create_uuid():
 	var rand = RandomNumberGenerator.new()
 	rand.seed = Time.get_unix_time_from_system()
-	uuid = rand.randi()
+	uuid = str(rand.randi())
 
 func _update_visuals():
 	var holder = get_node("Margins").get_node("Holder")
@@ -65,6 +75,10 @@ func _update_visuals():
 				TYPE_BOOL:
 					assignPick(nodeRow.get_node("Pick Bool"),str(key))
 					(nodeRow.get_node("Pick Bool") as CheckButton).button_pressed = rows[key]["pickValue"]
+			if rows[key]["pickValue"] is GL_AudioType:
+				assignPick(nodeRow.get_node("Pick Audio"),str(key))
+				if rows[key]["pickValue"] == null:
+					rows[key]["pickValue"] = GL_AudioType.new()
 		else:
 			(nodeRow.get_node("Label") as Label).size_flags_horizontal = Control.SIZE_EXPAND_FILL
 				
@@ -104,11 +118,18 @@ func _set_inout_type(label:Button, value):
 		TYPE_COLOR:
 			label.text = "▲"
 			label.add_theme_color_override("font_color", Color.WHITE_SMOKE)
-		_:
-			label.visible = false
+	if value is GL_AudioType:
+		label.text = "🔈"
+		label.add_theme_color_override("font_color", Color.BLUE_VIOLET)
+	if value == null:
+		label.visible = false
 
 func _set_title(name:String):
-	(get_node("Margins").get_node("Holder").get_node("Title").get_node("Title Label") as Label).text = name
+	(get_node("Margins").get_node("Holder").get_node("Title").get_node("Title Label") as LineEdit).text = name
+
+func _get_title() -> String:
+	return (get_node("Margins").get_node("Holder").get_node("Title").get_node("Title Label") as LineEdit).text
+
 
 func _create_row(name:String,input,output,picker:bool,pickDefault,pickFloatMaximum:float):
 	if rows.has(name):
@@ -126,12 +147,12 @@ func _send_input(output_name: String):
 	if not rows.has(output_name):
 		return
 
-	var connections = rows[output_name].get("connections", [])
-	for conn in connections:
+	for conn in rows[output_name].get("connections", []):
 		var target = conn.get("target", null)
 		var input_name = conn.get("input_name", null)
 		if target and input_name:
-			target._recieve_input(input_name, rows[output_name]["output"])
+			if typeof(target) != TYPE_INT:
+				target._recieve_input(input_name, rows[output_name]["output"])
 
 func _confirm_backConnection(input_name:String):
 	if !rows.has(input_name):
@@ -145,9 +166,10 @@ func _create_connection(target:GL_Node,input_name:String,output_name:String):
 	var item = target.rows.get(input_name, null)
 	if item == null:
 		return
-		
-	if typeof(rows[output_name].get("output", null)) != typeof(target.rows[input_name].get("input",null)):
-		if !(typeof(rows[output_name].get("output", null)) == TYPE_BOOL && typeof(target.rows[input_name].get("input",null)) == TYPE_FLOAT):
+	
+	var typeA = typeof(rows[output_name].get("output", null))
+	var typeB = typeof(target.rows[input_name].get("input",null))
+	if (typeA != typeB) && !(typeA == TYPE_BOOL && typeB == TYPE_FLOAT) && !(typeA == TYPE_INT && typeB == TYPE_FLOAT)&& !(typeA == TYPE_FLOAT && typeB == TYPE_INT):
 			print("Type mismatch: cannot connect " + output_name + " to " + target.name)
 			return
 	
@@ -156,7 +178,7 @@ func _create_connection(target:GL_Node,input_name:String,output_name:String):
 		"input_name": input_name
 	}
 	
-	var connections = rows[output_name].get("connections",[])
+	var connections = 	rows[output_name].get("connections", [])
 	
 	for connection in connections:
 		if connection.target == thenew.target and connection.input_name == thenew.input_name:
@@ -201,4 +223,4 @@ func delete_whole_node():
 			if node is GL_Node_Point:
 				for key in rows:
 					node.mainNode.destroy_connection(self,key)
-	queue_free()
+	get_parent().queue_free()
diff --git a/Scripts/GL_Node_Add.gd b/Scripts/GL_Node_Add.gd
index 4073c27..6350ecb 100644
--- a/Scripts/GL_Node_Add.gd
+++ b/Scripts/GL_Node_Add.gd
@@ -21,6 +21,8 @@ func _named(name:String):
 			mainNode._create_row(name,false,false,true,false,0)
 		2:
 			mainNode._create_row(name,Color.WHITE,Color.WHITE,true,Color.WHITE,0)
+		3:
+			mainNode._create_row(name,GL_AudioType.new(),GL_AudioType.new(),true,GL_AudioType.new(),0)
 	mainNode._update_visuals()
 func _cancelled():
 	disabled = false
diff --git a/Scripts/GL_Node_Map.gd b/Scripts/GL_Node_Map.gd
index ba8a701..d34a807 100644
--- a/Scripts/GL_Node_Map.gd
+++ b/Scripts/GL_Node_Map.gd
@@ -1,35 +1,63 @@
 extends Control
+class_name GL_Node_Map
 
+var background: TextureRect
 var holder: Control
 var is_panning: bool = false
 var last_mouse_pos: Vector2
 var is_hovered: bool = false
 
+#Workspace shenanigans
+var optionsVar:OptionButton
+
+#Workspaces
+var _workspace_ID:String
+var save_name: String = "My Save"
+var author_name: String = "Unnamed Author"
+var version: String = ProjectSettings.get_setting("application/config/version")
+var game_title: String = ProjectSettings.get_setting("application/config/name")
+var time_created: String = ""
+var last_updated: String = ""
+
+
+func _notification(what):
+	if what == NOTIFICATION_EXIT_TREE:
+		save_everything()
+
 func _ready():
 	visible = false
+	background = get_node("Background")
 	holder = get_node("Holder")
+	optionsVar = get_node("MarginContainer/HBoxContainer/OptionButton")
+	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
 
 	connect("mouse_entered", _on_mouse_entered)
 	connect("mouse_exited", _on_mouse_exited)
 
+	_workspace_ID = generate_new_workspace_id()
+	populate_workspace_options()
+	optionsVar.connect("item_selected", Callable(self, "_on_workspace_selected"))
+
+
 func _on_mouse_entered():
 	is_hovered = true
 
 func _on_mouse_exited():
 	is_hovered = false
 
+
 func _input(event: InputEvent) -> void:
 	if event is InputEventKey and event.pressed:
 		match event.keycode:
 			KEY_ESCAPE:
 				visible = not visible
-
+			KEY_TAB:
+				background.self_modulate.a = abs(background.self_modulate.a - 1)
 	if not visible:
 		Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
-		return
 	else:
 		Input.set_mouse_mode(Input.MOUSE_MODE_VISIBLE)
-
+		
 	if not is_hovered:
 		return
 
@@ -50,14 +78,11 @@ func _input(event: InputEvent) -> void:
 			elif event.button_index == MOUSE_BUTTON_WHEEL_DOWN:
 				zoom_factor = 0.9
 
-			# Apply scale
 			holder.scale *= zoom_factor
 
-			# Recalculate the new local position of the mouse after scaling
 			var new_global_xform = holder.get_global_transform()
 			var new_local_mouse_pos = new_global_xform.affine_inverse().basis_xform(mouse_pos)
 
-			# Calculate offset to shift so the mouse stays “anchored”
 			var delta = (new_local_mouse_pos - local_mouse_pos)
 			holder.position += delta * holder.scale
 
@@ -65,3 +90,169 @@ func _input(event: InputEvent) -> void:
 		var delta = event.position - last_mouse_pos
 		holder.position += delta
 		last_mouse_pos = event.position
+
+func save_everything():
+	var saveDict := {}
+	var rng = RandomNumberGenerator.new()
+	rng.seed = Time.get_ticks_msec()
+	
+	if holder.get_child_count() == 0:
+		return
+
+	for child in holder.get_children():
+		child = child.get_child(0)
+		if child is not GL_Node:
+			print(child.name)
+			continue
+
+		var id = "SAVE_" + str(rng.randi())
+		var node_data = {
+			"path": child.nodePath,
+			"name": child._get_title(),
+			"uuid": child.uuid,
+			"rows": child.rows.duplicate(true),
+			"position": child.position
+		}
+
+		# Save recording if it's a GL_Record and has enough data
+		if child is GL_Record and child.recording != null:
+			if child.recording.size() >= 3:
+				var recording_file_path = "user://My Precious Save Files/" + str(_workspace_ID) + "/" + child.uuid + "_recording.tres"
+				var recording_config = ConfigFile.new()
+				recording_config.set_value("recording", "data", child.recording)
+				var err = recording_config.save(recording_file_path)
+				if err != OK:
+					push_error("Failed to save recording for " + child.uuid + ": " + str(err))
+				else:
+					print("Saved recording for node ", child.uuid)
+
+		# Convert connections to uuid references
+		for key in node_data["rows"]:
+			if node_data["rows"][key].has("connections"):
+				var connections = node_data["rows"][key]["connections"]
+				for i in range(connections.size()):
+					if connections[i]["target"] is GL_Node:
+						connections[i]["target"] = connections[i]["target"].uuid
+
+		saveDict[id] = node_data
+
+	var save_dir = "user://My Precious Save Files/" + str(_workspace_ID)
+	DirAccess.make_dir_recursive_absolute(save_dir)
+	var file_path = save_dir + "/node_workspace.tres"
+
+	var resource = ConfigFile.new()
+
+	# Metadata section
+	if time_created == "":
+		time_created = Time.get_datetime_string_from_system(true)
+	last_updated = Time.get_datetime_string_from_system(true)
+
+	resource.set_value("meta", "save_name", save_name)
+	resource.set_value("meta", "author", author_name)
+	resource.set_value("meta", "version", ProjectSettings.get_setting("application/config/version"))
+	resource.set_value("meta", "game_title", ProjectSettings.get_setting("application/config/name"))
+	resource.set_value("meta", "time_created", time_created)
+	resource.set_value("meta", "last_updated", last_updated)
+
+	# Main save data
+	resource.set_value("workspace", "data", saveDict)
+
+	var err = resource.save(file_path)
+	if err != OK:
+		push_error("Failed to save workspace: " + str(err))
+	else:
+		print("Saved workspace to: ", file_path)
+
+	populate_workspace_options()
+
+
+		
+func load_everything():
+	var file_path = "user://My Precious Save Files/" + str(_workspace_ID) + "/node_workspace.tres"
+	var resource = ConfigFile.new()
+	var err = resource.load(file_path)
+	if err != OK:
+		push_error("Failed to load workspace: " + str(err))
+		return {}
+
+	# Load metadata
+	save_name = resource.get_value("meta", "save_name", "Unnamed Save")
+	author_name = resource.get_value("meta", "author", "Unknown Author")
+	version = resource.get_value("meta", "version", "0.0")
+	game_title = resource.get_value("meta", "game_title", "Untitled Game")
+	time_created = resource.get_value("meta", "time_created", "")
+	last_updated = resource.get_value("meta", "last_updated", "")
+
+	print("Loaded workspace metadata:")
+	print("Save Name: ", save_name)
+	print("Author: ", author_name)
+	print("Version: ", version)
+	print("Game Title: ", game_title)
+	print("Time Created: ", time_created)
+	print("Last Updated: ", last_updated)
+
+	# Load nodes
+	var data = resource.get_value("workspace", "data", {})
+	for key in data:
+		var packed_scene = load(data[key]["path"])
+		if packed_scene == null:
+			printerr("Could not load resource at: " + data[key]["path"])
+			continue
+		var node = packed_scene.instantiate() as Control
+		holder.add_child(node)
+		node = node.get_child(0) as GL_Node
+		node.position = data[key].get("position",Vector2.ZERO)
+		node.nodePath = data[key].get("path","ERR")
+		node.uuid = data[key].get("uuid","ERR_" + key + str(Time.get_ticks_msec()))
+		node._set_title(data[key].get("name","???"))
+		node.rows = data[key].get("rows",{})
+		node._update_visuals()
+		if node is GL_Record:
+			var recording_file = "user://My Precious Save Files/" + str(_workspace_ID) + "/" + node.uuid + "_recording.tres"
+			var config = ConfigFile.new()
+			if config.load(recording_file) == OK:
+				node.recording = config.get_value("recording", "data", {})
+
+
+func generate_new_workspace_id() -> String:
+	var rng = RandomNumberGenerator.new()
+	rng.seed = Time.get_ticks_msec()
+	return str(rng.randi())
+
+func clear_holder():
+	for node in holder.get_children():
+		node.queue_free()
+	await get_tree().process_frame  # ensure all nodes are freed
+	
+func populate_workspace_options():
+	optionsVar.clear()
+	optionsVar.add_item("New Workspace")
+
+	var dir := DirAccess.open("user://My Precious Save Files")
+	if dir:
+		dir.list_dir_begin()
+		var name = dir.get_next()
+		while name != "":
+			if dir.current_is_dir() and name != "." and name != "..":
+				optionsVar.add_item(name)
+			name = dir.get_next()
+		dir.list_dir_end()
+
+func _on_workspace_selected(index: int):
+	save_everything()
+
+	if index == 0:  # New Workspace
+		clear_holder()
+		_workspace_ID = generate_new_workspace_id()
+		save_name = "My Save"
+		author_name = "Unnamed Author"
+		version = ProjectSettings.get_setting("application/config/version")
+		game_title = ProjectSettings.get_setting("application/config/name")
+		time_created = ""
+		last_updated = ""
+		print("Created new workspace: ", _workspace_ID)
+	else:
+		var selected_name = optionsVar.get_item_text(index)
+		_workspace_ID = selected_name
+		clear_holder()
+		load_everything()
diff --git a/Scripts/GL_Node_Picker_Audio.gd b/Scripts/GL_Node_Picker_Audio.gd
new file mode 100644
index 0000000..6c3c2e4
--- /dev/null
+++ b/Scripts/GL_Node_Picker_Audio.gd
@@ -0,0 +1,81 @@
+extends GL_Node_Picker
+
+var audio_selector:OptionButton
+var file_dialog:FileDialog
+
+func _ready():
+	file_dialog = get_node("FileDialog")
+	audio_selector = get_node("HBox").get_node("OptionButton")
+	DirAccess.make_dir_recursive_absolute(find_audio_path())
+	file_dialog.file_selected.connect(_on_audio_file_selected)
+	_update_audio_options()
+	
+func _on_audio_button_pressed():
+	file_dialog.clear_filters()
+	file_dialog.current_path = OS.get_system_dir(OS.SYSTEM_DIR_DOWNLOADS)
+	file_dialog.add_filter("*.wav ; WAV Audio")
+	file_dialog.add_filter("*.mp3 ; MP3 Audio")
+	file_dialog.add_filter("*.ogg ; OGG Audio")
+	file_dialog.popup_centered()
+	
+func _on_audio_file_selected(path: String):
+	var filename = path.get_file()
+	var dest_path = find_audio_path() + "/" + filename
+
+	var file = FileAccess.open(path, FileAccess.READ)
+	if file:
+		var data = file.get_buffer(file.get_length())
+		file.close()
+
+		var save_file = FileAccess.open(dest_path, FileAccess.WRITE)
+		if save_file:
+			save_file.store_buffer(data)
+			save_file.close()
+			print("Saved audio to: ", dest_path)
+			_update_audio_options(filename)  # repopulate and select this one
+		else:
+			push_error("Failed to write audio file.")
+	else:
+		push_error("Failed to read selected audio.")
+		
+func _update_audio_options(select_filename := ""):
+	audio_selector.clear()
+	var audio_files := []
+
+	var dir := DirAccess.open(find_audio_path())
+	if dir:
+		dir.list_dir_begin()
+		var file = dir.get_next()
+		while file != "":
+			if not dir.current_is_dir():
+				audio_files.append(file)
+			file = dir.get_next()
+		dir.list_dir_end()
+
+	audio_files.sort()
+	for i in range(audio_files.size()):
+		audio_selector.add_item(audio_files[i])
+		if audio_files[i] == select_filename:
+			audio_selector.select(i)
+			_set_audio_path(audio_files[i])
+			
+func _on_audio_option_selected(index: int):
+	var file = audio_selector.get_item_text(index)
+	_set_audio_path(file)
+	
+func _set_audio_path(file: String):
+	var path = find_audio_path() + "/" + file
+	if mainNode and mainNode.rows.has(valueName):
+		var audio = GL_AudioType.new()
+		audio.value = path
+		mainNode.rows[valueName]["pickValue"] = audio
+		print("Audio set: ", path)
+	else:
+		push_error("mainNode or rows[valueName] not found.")
+
+func find_audio_path() -> String:
+	for node in get_tree().get_nodes_in_group("Node Map"):
+		if node is GL_Node_Map:
+			return "user://My Precious Save Files/" + node._workspace_ID + "/Audio"
+	printerr("Uhhhhh")
+	return ""
diff --git a/Scripts/GL_Node_Picker_Audio.gd.uid b/Scripts/GL_Node_Picker_Audio.gd.uid
new file mode 100644
index 0000000..1849fd0
--- /dev/null
+++ b/Scripts/GL_Node_Picker_Audio.gd.uid
@@ -0,0 +1 @@
+uid://rmlqvxot3kys
diff --git a/Scripts/GL_Node_Point.gd b/Scripts/GL_Node_Point.gd
index 4847ebf..ad1c497 100644
--- a/Scripts/GL_Node_Point.gd
+++ b/Scripts/GL_Node_Point.gd
@@ -26,6 +26,8 @@ func _process(delta):
 					previewLine.default_color = Color.BLACK
 			TYPE_COLOR:
 				previewLine.default_color = output
+		if output is GL_AudioType:
+			previewLine.default_color = Color.BLUE_VIOLET
 		
 	var connections = mainNode.rows[valueName].get("connections",[])
 	if connections != []:
@@ -43,8 +45,14 @@ func _process(delta):
 						child.default_color = Color.BLACK
 				TYPE_COLOR:
 					child.default_color = output
+			if output is GL_AudioType:
+				if output.value == "":
+					child.default_color = Color.BLACK
+				else:
+					child.default_color = Color.BLUE_VIOLET
 			child.points[0] = global_position + Vector2(size.x / 2, size.y / 2)
-			child.points[1] = (connections[iter]["target"] as GL_Node).give_input_point_pos(connections[iter]["input_name"])# - child.global_position
+			if typeof(connections[iter]["target"]) != TYPE_INT:
+				child.points[1] = (connections[iter]["target"] as GL_Node).give_input_point_pos(connections[iter]["input_name"])# - child.global_position
 			iter += 1
 
 func _create_line() -> Line2D:
diff --git a/Scripts/GL_Output.gd b/Scripts/GL_Output.gd
index d7a5b52..30b8639 100644
--- a/Scripts/GL_Output.gd
+++ b/Scripts/GL_Output.gd
@@ -6,6 +6,7 @@ extends GL_Node
 
 func _ready():
 	super._ready()
+	_set_title(identification)
 	for i in names.size():
 		match(types[i].to_lower()):
 			"float":
@@ -14,6 +15,8 @@ func _ready():
 				_create_row(str(names[i]),Color.WHITE,null,true,Color.WHITE,0)
 			"bool":
 				_create_row(str(names[i]),false,null,true,false,0)
+			"audio":
+				_create_row(str(names[i]),GL_AudioType.new(),null,true,GL_AudioType.new(),0)
 	_update_visuals()
 
 func _process(delta):
diff --git a/Scripts/GL_Record.gd b/Scripts/GL_Record.gd
index 5d63f7f..71c40c4 100644
--- a/Scripts/GL_Record.gd
+++ b/Scripts/GL_Record.gd
@@ -1,4 +1,5 @@
 extends GL_Node
+class_name GL_Record
 var timer:float
 const sampleRate = 0.05
 var recording:Dictionary
@@ -59,7 +60,14 @@ func _traverse():
 				recording[key]["lastUsed"] = current
 				recording[key]["current"] = newCurrent 
 			if recording[key]["lastUsed"] != null && recording[key]["current"] != recording[key]["end"]:
-				rows[key]["output"] = lerp(recording[key]["list"][recording[key]["lastUsed"]]["value"],recording[key]["list"][recording[key]["current"]]["value"],remap_time(time,recording[key]["list"][recording[key]["lastUsed"]]["time"],recording[key]["list"][recording[key]["current"]]["time"]))
+				if(rows[key]["output"] is float):
+					rows[key]["output"] = lerp(float(recording[key]["list"][recording[key]["lastUsed"]]["value"]),float(recording[key]["list"][recording[key]["current"]]["value"]),remap_time(time,recording[key]["list"][recording[key]["lastUsed"]]["time"],recording[key]["list"][recording[key]["current"]]["time"]))
+				elif(rows[key]["output"] is bool || rows[key]["output"] is GL_AudioType):
+					rows[key]["output"] = recording[key]["current"]
+				elif(rows[key]["output"] is Color):
+					rows[key]["output"] = lerp(recording[key]["list"][recording[key]["lastUsed"]]["value"],recording[key]["list"][recording[key]["current"]]["value"],remap_time(time,recording[key]["list"][recording[key]["lastUsed"]]["time"],recording[key]["list"][recording[key]["current"]]["time"]))
+
+
 func remap_time(value: float, start: float, end: float) -> float:
 	if start == end:
 		return 0.0 
@@ -80,10 +88,6 @@ func _record():
 	for key in recording:
 		if key == "Recording" || key == "Current Time":
 			continue
-		if defaultValues[key] == rows[key]["input"]:
-			continue
-		elif defaultValues[key] != null:
-			defaultValues[key] == null #is this gonna bite me back if I allow null values to pass
 		var currentSave = recording[key]["current"]
 		if currentSave == null:
 			var id = "ID_" + str(rng.randi())
@@ -98,7 +102,11 @@ func _record():
 			recording[key]["end"] = id
 			rows[key]["output"] = recording[key]["list"][id]["value"]
 			continue
-		else:
+		if defaultValues[key] == rows[key]["input"]:
+			continue
+		elif defaultValues[key] != null:
+			defaultValues[key] = null #is this gonna bite me back if I allow null values to pass
+		if currentSave != null:
 			if time < oldTime: #rewind
 				continue #fix pls
 			else: #forward
diff --git a/Scripts/GL_Search.gd b/Scripts/GL_Search.gd
index 19f566f..30f22ce 100644
--- a/Scripts/GL_Search.gd
+++ b/Scripts/GL_Search.gd
@@ -7,14 +7,17 @@ var rows : Dictionary = {
 	"ChuckSpot":1,
 	"HelenSpot":1,
 	"MunchSpot":1,
+	"Audio":1,
 	"Bool":1,
 	"Color":1,
+	"Direct Output":1,
 	"Float":1,
 	"Invert":1,
 	"MiscKeys":1,
 	"NumberKeys":1,
 	"Keystroke Ramp":1,
 	"Lerp":1,
+	"Mix Floats":1,
 	"Mix Colors":1,
 	"Mouse Wheel":1,
 	"Random":1,
diff --git a/Scripts/GL_Speaker.gd b/Scripts/GL_Speaker.gd
new file mode 100644
index 0000000..f9af39a
--- /dev/null
+++ b/Scripts/GL_Speaker.gd
@@ -0,0 +1,45 @@
+extends GL_Animatable
+
+var speaker:AudioStreamPlayer
+var oldPath:String
+var oldTime:float
+
+func _ready():
+	speaker = get_child(0)
+	
+func _sent_signals(anim_name: String, value):
+	if speaker == null:
+		printerr("Can't find Animatable Speaker, needs to be the first child of node")
+		return
+		
+	match(anim_name):
+		"Audio":
+			print(value.value)
+			if value is not GL_AudioType:
+				return
+			var path = (value as GL_AudioType).value
+			if path != "" && path != oldPath:
+				var stream
+				match(path.get_extension().to_lower()):
+					"mp3":
+						stream = AudioStreamMP3.load_from_file(path)
+					"wav":
+						stream = AudioStreamWAV.load_from_file(path)
+					"ogg":
+						stream = AudioStreamOggVorbis.load_from_file(path)
+				if stream and stream is AudioStream:
+					speaker.stream = stream
+					oldPath = path
+				else:
+					printerr("Invalid audio stream at path: ", path)
+		"Volume":
+			speaker.volume_linear = value
+		"Current Time":
+			if speaker.stream != null:
+				if abs(speaker.get_playback_position() - value) > 0.05 && value < speaker.stream.get_length():
+					speaker.play(value)
+				if oldTime == value:
+					speaker.stop()
+				oldTime = value
+
+	
diff --git a/Scripts/GL_Speaker.gd.uid b/Scripts/GL_Speaker.gd.uid
new file mode 100644
index 0000000..273cb9c
--- /dev/null
+++ b/Scripts/GL_Speaker.gd.uid
@@ -0,0 +1 @@
+uid://c5kxam0k3beml
diff --git a/Scripts/freecam.gd b/Scripts/freecam.gd
index 7848c95..926eb24 100644
--- a/Scripts/freecam.gd
+++ b/Scripts/freecam.gd
@@ -1,146 +1,114 @@
-extends Camera3D
+class_name FreeLookCamera extends Camera3D
 
-## Camera with flying script attached to it.
-class_name Freecam3D
-
-##
-## Camera with toggleable freecam mode for prototyping when creating levels, shaders, lighting, etc.
-##
-## Usage: Run your game, press <TAB> and fly around freely. Uses Minecraft-like controls.
-##
-
-## Customize your own toggle key to avoid collisions with your current mappings.
-@export var toggle_key: Key = KEY_TAB
-## Speed up / down by scrolling the mouse whell down / up
-@export var invert_speed_controls: bool = false
-
-@export var overlay_text: bool = true
-
-## Pivot node for camera looking around
-@onready var pivot := Node3D.new()
-## Main parent for camera overlay.
-@onready var screen_overlay := VBoxContainer.new()
-## Container for the chat-like event log.
-@onready var event_log := VBoxContainer.new()
-
-const MAX_SPEED := 0.25
-const MIN_SPEED := 0.01
-const ACCELERATION := 0.1
-const MOUSE_SENSITIVITY := 0.002
-
-## Whether or not the camera can move.
-var movement_active := false:
-	set(val):
-		movement_active = val
-		display_message("[Movement ON]" if movement_active else "[Movement OFF]")
-
-## The current maximum speed. Lower or higher it by scrolling the mouse wheel.
-var target_speed := MIN_SPEED
-## Movement velocity.
-var velocity := Vector3.ZERO
+# Modifier keys' speed multiplier
+const SHIFT_MULTIPLIER = 2.5
+const ALT_MULTIPLIER = 1.0 / SHIFT_MULTIPLIER
 
 
-## Sets up pivot and UI overlay elements.
-func _setup_nodes() -> void:
-	self.add_sibling(pivot)
-	pivot.position = position
-	pivot.rotation = rotation
-	pivot.name = "FreecamPivot"
-	self.reparent(pivot)
-	self.position = Vector3.ZERO
-	self.rotation = Vector3.ZERO
-	# UI stuff
-	screen_overlay.add_theme_constant_override("Separation", 8)
-	self.add_child(screen_overlay)
-	screen_overlay.add_child(_make_label("Debug Camera"))
-	screen_overlay.add_spacer(false)
+@export_range(0.0, 1.0) var sensitivity: float = 0.25
+
+# Mouse state
+var _mouse_position = Vector2(0.0, 0.0)
+var _total_pitch = 0.0
+
+# Movement state
+var _direction = Vector3(0.0, 0.0, 0.0)
+var _velocity = Vector3(0.0, 0.0, 0.0)
+var _acceleration = 30
+var _deceleration = -10
+var _vel_multiplier = 4
+
+# Keyboard state
+var _w = false
+var _s = false
+var _a = false
+var _d = false
+var _q = false
+var _e = false
+var _shift = false
+var _alt = false
+
+func _input(event):
+	# Receives mouse motion
+	if event is InputEventMouseMotion:
+		_mouse_position = event.relative
 	
-	screen_overlay.add_child(event_log)
-	screen_overlay.visible = overlay_text
+	# Receives mouse button input
+	if event is InputEventMouseButton:
+		match event.button_index:
+			MOUSE_BUTTON_WHEEL_UP: # Increases max velocity
+				_vel_multiplier = clamp(_vel_multiplier * 1.1, 0.2, 20)
+			MOUSE_BUTTON_WHEEL_DOWN: # Decereases max velocity
+				_vel_multiplier = clamp(_vel_multiplier / 1.1, 0.2, 20)
 
+	# Receives key input
+	if event is InputEventKey:
+		match event.keycode:
+			KEY_W:
+				_w = event.pressed
+			KEY_S:
+				_s = event.pressed
+			KEY_A:
+				_a = event.pressed
+			KEY_D:
+				_d = event.pressed
+			KEY_Q:
+				_q = event.pressed
+			KEY_E:
+				_e = event.pressed
+			KEY_SHIFT:
+				_shift = event.pressed
+			KEY_ALT:
+				_alt = event.pressed
 
-func _ready() -> void:
-	_setup_nodes.call_deferred()
-	_add_keybindings()
-	movement_active = true
-	Input.set_mouse_mode(Input.MOUSE_MODE_CAPTURED)
+# Updates mouselook and movement every frame
+func _process(delta):
+	_update_mouselook()
+	_update_movement(delta)
 
-
-
-func _process(delta: float) -> void:
+# Updates camera movement
+func _update_movement(delta):
+	# Computes desired direction from key states
+	_direction = Vector3(
+		(_d as float) - (_a as float), 
+		(_e as float) - (_q as float),
+		(_s as float) - (_w as float)
+	)
 	
-	if Input.is_action_just_released("__debug_camera_toggle"):
-		movement_active = not movement_active
+	# Computes the change in velocity due to desired direction and "drag"
+	# The "drag" is a constant acceleration on the camera to bring it's velocity to 0
+	var offset = _direction.normalized() * _acceleration * _vel_multiplier * delta \
+		+ _velocity.normalized() * _deceleration * _vel_multiplier * delta
 	
-	if movement_active:
-		var dir = Vector3.ZERO
-		if Input.is_action_pressed("__debug_camera_forward"): 	dir.z -= 1
-		if Input.is_action_pressed("__debug_camera_back"): 		dir.z += 1
-		if Input.is_action_pressed("__debug_camera_left"): 		dir.x -= 1
-		if Input.is_action_pressed("__debug_camera_right"): 	dir.x += 1
-		if Input.is_action_pressed("__debug_camera_up"): 		dir.y += 1
-		if Input.is_action_pressed("__debug_camera_down"): 		dir.y -= 1
+	# Compute modifiers' speed multiplier
+	var speed_multi = 1
+	if _shift: speed_multi *= SHIFT_MULTIPLIER
+	if _alt: speed_multi *= ALT_MULTIPLIER
+	
+	# Checks if we should bother translating the camera
+	if _direction == Vector3.ZERO and offset.length_squared() > _velocity.length_squared():
+		# Sets the velocity to 0 to prevent jittering due to imperfect deceleration
+		_velocity = Vector3.ZERO
+	else:
+		# Clamps speed to stay within maximum value (_vel_multiplier)
+		_velocity.x = clamp(_velocity.x + offset.x, -_vel_multiplier, _vel_multiplier)
+		_velocity.y = clamp(_velocity.y + offset.y, -_vel_multiplier, _vel_multiplier)
+		_velocity.z = clamp(_velocity.z + offset.z, -_vel_multiplier, _vel_multiplier)
+	
+		translate(_velocity * delta * speed_multi)
+
+# Updates mouse look 
+func _update_mouselook():
+	# Only rotates mouse if the mouse is captured
+	if Input.get_mouse_mode() == Input.MOUSE_MODE_CAPTURED:
+		_mouse_position *= sensitivity
+		var yaw = _mouse_position.x
+		var pitch = _mouse_position.y
+		_mouse_position = Vector2(0, 0)
 		
-		dir = dir.normalized()
-		dir = dir.rotated(Vector3.UP, pivot.rotation.y)
-		
-		velocity = lerp(velocity, dir * target_speed, ACCELERATION)
-		pivot.position += velocity
-
-
-func _input(event: InputEvent) -> void:
-	if movement_active:
-		# Turn around
-		if event is InputEventMouseMotion:
-			pivot.rotate_y(-event.relative.x * MOUSE_SENSITIVITY)
-			rotate_x(-event.relative.y * MOUSE_SENSITIVITY)
-			rotation.x = clamp(rotation.x, -PI/2, PI/2)
-		
-		var speed_up = func():
-			target_speed = clamp(target_speed + 0.015, MIN_SPEED, MAX_SPEED)
-			display_message("[Speed up] " + str(target_speed))
-			
-		var slow_down = func():
-			target_speed = clamp(target_speed - 0.015, MIN_SPEED, MAX_SPEED)
-			display_message("[Slow down] " + str(target_speed))
-		
-		# Speed up and down with the mouse wheel
-		if event is InputEventMouseButton:
-			if event.button_index == MOUSE_BUTTON_WHEEL_UP and event.pressed:
-				slow_down.call() if invert_speed_controls else speed_up.call()
-				
-			if event.button_index == MOUSE_BUTTON_WHEEL_DOWN and event.pressed:
-				speed_up.call() if invert_speed_controls else slow_down.call()
-
-
-## Pushes new message label into "chat" and removes the old ones if necessary
-func display_message(text: String) -> void:
-	while event_log.get_child_count() >= 3:
-		event_log.remove_child(event_log.get_child(0))
+		# Prevents looking up/down too far
+		pitch = clamp(pitch, -90 - _total_pitch, 90 - _total_pitch)
+		_total_pitch += pitch
 	
-	event_log.add_child(_make_label(text))
-
-
-func _make_label(text: String) -> Label:
-	var l = Label.new()
-	l.text = text
-	return l
-
-
-func _add_keybindings() -> void:
-	var actions = InputMap.get_actions()
-	if "__debug_camera_forward" not in actions: _add_key_input_action("__debug_camera_forward", KEY_W)
-	if "__debug_camera_back" 	not in actions: _add_key_input_action("__debug_camera_back", KEY_S)
-	if "__debug_camera_left" 	not in actions: _add_key_input_action("__debug_camera_left", KEY_A)
-	if "__debug_camera_right" 	not in actions: _add_key_input_action("__debug_camera_right", KEY_D)
-	if "__debug_camera_up" 		not in actions: _add_key_input_action("__debug_camera_up", KEY_E)
-	if "__debug_camera_down" 	not in actions: _add_key_input_action("__debug_camera_down", KEY_Q)
-	if "__debug_camera_toggle" 	not in actions: _add_key_input_action("__debug_camera_toggle", toggle_key)
-
-
-func _add_key_input_action(name: String, key: Key) -> void:
-	var ev = InputEventKey.new()
-	ev.physical_keycode = key
-	
-	InputMap.add_action(name)
-	InputMap.action_add_event(name, ev)
+		rotate_y(deg_to_rad(-yaw))
+		rotate_object_local(Vector3(1,0,0), deg_to_rad(-pitch))