diff --git a/Fonts/Qubi.ttf b/Fonts/Qubi.ttf
new file mode 100644
index 0000000..ce2e10a
Binary files /dev/null and b/Fonts/Qubi.ttf differ
diff --git a/Fonts/Qubi.ttf.import b/Fonts/Qubi.ttf.import
new file mode 100644
index 0000000..e704151
--- /dev/null
+++ b/Fonts/Qubi.ttf.import
@@ -0,0 +1,40 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://byubry1acvlug"
+path="res://.godot/imported/Qubi.ttf-ef2072390941155281c267608c0b0094.fontdata"
+
+[deps]
+
+source_file="res://Fonts/Qubi.ttf"
+dest_files=["res://.godot/imported/Qubi.ttf-ef2072390941155281c267608c0b0094.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+disable_embedded_bitmaps=true
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=0
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[{
+"chars": [],
+"glyphs": [],
+"name": "New Configuration",
+"size": Vector2i(16, 0),
+"variation_embolden": 0.0
+}]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/Fonts/Qubio.ttf b/Fonts/Qubio.ttf
new file mode 100644
index 0000000..fb02072
Binary files /dev/null and b/Fonts/Qubio.ttf differ
diff --git a/Fonts/Qubio.ttf.import b/Fonts/Qubio.ttf.import
new file mode 100644
index 0000000..09a20b5
--- /dev/null
+++ b/Fonts/Qubio.ttf.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="font_data_dynamic"
+type="FontFile"
+uid="uid://chmfli1xm8oe1"
+path="res://.godot/imported/Qubio.ttf-32f8479b52e99fe78535ddaada4aa1d7.fontdata"
+
+[deps]
+
+source_file="res://Fonts/Qubio.ttf"
+dest_files=["res://.godot/imported/Qubio.ttf-32f8479b52e99fe78535ddaada4aa1d7.fontdata"]
+
+[params]
+
+Rendering=null
+antialiasing=1
+generate_mipmaps=false
+disable_embedded_bitmaps=true
+multichannel_signed_distance_field=false
+msdf_pixel_range=8
+msdf_size=48
+allow_system_fallback=true
+force_autohinter=false
+hinting=0
+subpixel_positioning=1
+oversampling=0.0
+Fallbacks=null
+fallbacks=[]
+Compress=null
+compress=true
+preload=[]
+language_support={}
+script_support={}
+opentype_features={}
diff --git a/Fonts/Theme.tres b/Fonts/Theme.tres
new file mode 100644
index 0000000..267a889
--- /dev/null
+++ b/Fonts/Theme.tres
@@ -0,0 +1,7 @@
+[gd_resource type="Theme" load_steps=2 format=3 uid="uid://ck7603ob4gflc"]
+
+[ext_resource type="FontFile" uid="uid://byubry1acvlug" path="res://Fonts/Qubi.ttf" id="1_y7uny"]
+
+[resource]
+default_font = ExtResource("1_y7uny")
+default_font_size = 8
diff --git a/Main.tscn b/Main.tscn
new file mode 100644
index 0000000..bf09a1d
--- /dev/null
+++ b/Main.tscn
@@ -0,0 +1,3 @@
+[gd_scene format=3 uid="uid://bs6ojoud4mvb8"]
+
+[node name="Main" type="Node2D"]
diff --git a/addons/laia_highlighter/plugin.cfg b/addons/laia_highlighter/plugin.cfg
new file mode 100644
index 0000000..fc0bffb
--- /dev/null
+++ b/addons/laia_highlighter/plugin.cfg
@@ -0,0 +1,7 @@
+[plugin]
+
+name="Laia Highlighter"
+description="A highlighter to highting laia scripting files"
+author="Ategon"
+version="v1.0.0"
+script="plugin.gd"
diff --git a/addons/laia_highlighter/plugin.gd b/addons/laia_highlighter/plugin.gd
new file mode 100644
index 0000000..03061d2
--- /dev/null
+++ b/addons/laia_highlighter/plugin.gd
@@ -0,0 +1,81 @@
+@tool
+extends EditorPlugin
+
+
+var dialogue_highlighter:DialogueHighlighter
+
+
+func _enter_tree() -> void:
+ dialogue_highlighter = DialogueHighlighter.new()
+ var script_editor = EditorInterface.get_script_editor()
+ print(script_editor)
+ print(dialogue_highlighter)
+ script_editor.register_syntax_highlighter(dialogue_highlighter)
+ print(script_editor.get_current_script())
+ print(script_editor.get_open_scripts())
+ script_editor.goto_line(20)
+
+
+func _exit_tree() -> void:
+ if is_instance_valid(dialogue_highlighter):
+ var script_editor = EditorInterface.get_script_editor()
+ script_editor.unregister_syntax_highlighter(dialogue_highlighter)
+ dialogue_highlighter = null
+
+
+class DialogueHighlighter extends EditorSyntaxHighlighter:
+ func _get_name() -> String:
+ return "Laia"
+
+ func _get_supported_languages() -> PackedStringArray:
+ return ["TextFile"]
+
+ func _get_line_syntax_highlighting(line: int) -> Dictionary:
+ var color_map = {}
+ var text_editor = get_text_edit()
+ var str = text_editor.get_line(line)
+
+ # Comment
+ if str.strip_edges().begins_with("#"):
+ color_map[0] = { "color": Color.WEB_GRAY }
+ return color_map
+
+ # Key
+ var regex3 = RegEx.new()
+ regex3.compile("([a-zA-Z0-9_]+:).*")
+ var result3 = regex3.search(str)
+
+ if result3:
+ color_map[result3.get_start(1)] = { "color": Color.SEA_GREEN }
+ color_map[result3.get_end(1)] = { "color": Color.WHITE }
+
+ # Array
+ var regex = RegEx.new()
+ regex.compile("([a-zA-Z0-9_\\-])+\\[\\]")
+ var result = regex.search(str)
+
+ if result:
+ color_map[result.get_start()] = { "color": Color.CYAN }
+ color_map[result.get_end(1)] = { "color": Color.LIGHT_CYAN }
+
+ # Enum
+ var regex4 = RegEx.new()
+ regex4.compile("[a-zA-Z0-9_]+(\\.[a-zA-Z0-9_]+)")
+ var result4 = regex4.search(str)
+
+ if result4:
+ color_map[result4.get_start()] = { "color": Color.ORANGE }
+ color_map[result4.get_start(1)] = { "color": Color.ORANGE_RED }
+ color_map[result4.get_end()] = { "color": Color.WHITE }
+
+ # Color
+ var regex2 = RegEx.new()
+ regex2.compile("#[a-zA-Z0-9]+")
+ var result2 = regex2.search(str)
+
+ if result2:
+ color_map[result2.get_start()] = { "color": Color(result2.get_string()) }
+
+
+
+ return color_map
diff --git a/components/Achievements/Achievements.tscn b/components/Achievements/Achievements.tscn
new file mode 100644
index 0000000..c36b88f
--- /dev/null
+++ b/components/Achievements/Achievements.tscn
@@ -0,0 +1,164 @@
+[gd_scene load_steps=11 format=3 uid="uid://c8nsgn4idu8ay"]
+
+[ext_resource type="Script" path="res://components/Achievements/achievements.gd" id="1_fj2tg"]
+[ext_resource type="Texture2D" uid="uid://dat440dg86mjl" path="res://components/Achievements/back.png" id="2_1q85v"]
+[ext_resource type="Theme" uid="uid://ck7603ob4gflc" path="res://Fonts/Theme.tres" id="2_p5tbu"]
+[ext_resource type="PackedScene" uid="uid://dykc1mgg5uopw" path="res://components/Cursor/MouseHandler.tscn" id="2_vij8n"]
+[ext_resource type="Texture2D" uid="uid://bypm21lqwg7g0" path="res://components/Achievements/Village 6.png" id="4_1mson"]
+[ext_resource type="Texture2D" uid="uid://ur6oiwg6ksns" path="res://components/Achievements/award.svg" id="4_7grt2"]
+[ext_resource type="Texture2D" uid="uid://dcj0xniur7dk1" path="res://components/Achievements/thumbnail-mask.png" id="6_dwjjq"]
+[ext_resource type="AudioStream" uid="uid://dxac8e02mctgp" path="res://components/Achievements/achievement.wav" id="8_74gug"]
+[ext_resource type="AudioStream" uid="uid://4m4dwlrv78l" path="res://components/Achievements/achievement-back.ogg" id="9_c3xir"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_yajw4"]
+size = Vector2(180, 32)
+
+[node name="Achievements" type="Node"]
+script = ExtResource("1_fj2tg")
+
+[node name="CanvasLayer" type="CanvasLayer" parent="."]
+
+[node name="Popup" type="Control" parent="CanvasLayer"]
+layout_mode = 3
+anchors_preset = 3
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -140.0
+offset_top = -42.0
+offset_right = 40.0
+offset_bottom = -12.0
+grow_horizontal = 0
+grow_vertical = 0
+
+[node name="MouseHandler" parent="CanvasLayer/Popup" instance=ExtResource("2_vij8n")]
+position = Vector2(62, 15)
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="CanvasLayer/Popup/MouseHandler"]
+position = Vector2(29, 0)
+shape = SubResource("RectangleShape2D_yajw4")
+
+[node name="Shadow" type="TextureRect" parent="CanvasLayer/Popup"]
+modulate = Color(0, 0, 0, 1)
+show_behind_parent = true
+layout_mode = 1
+anchors_preset = 3
+anchor_left = 1.0
+anchor_top = 1.0
+anchor_right = 1.0
+anchor_bottom = 1.0
+offset_left = -178.0
+offset_top = -29.0
+offset_right = 2.0
+offset_bottom = 2.0
+grow_horizontal = 0
+grow_vertical = 0
+size_flags_horizontal = 8
+size_flags_vertical = 8
+theme = ExtResource("2_p5tbu")
+texture = ExtResource("2_1q85v")
+expand_mode = 1
+
+[node name="Popup" type="TextureRect" parent="CanvasLayer/Popup"]
+clip_children = 2
+layout_mode = 1
+anchors_preset = 6
+anchor_left = 1.0
+anchor_top = 0.5
+anchor_right = 1.0
+anchor_bottom = 0.5
+offset_left = -180.0
+offset_top = -15.0
+offset_bottom = 15.0
+grow_horizontal = 0
+grow_vertical = 2
+size_flags_horizontal = 8
+size_flags_vertical = 8
+texture = ExtResource("2_1q85v")
+expand_mode = 1
+
+[node name="Title" type="RichTextLabel" parent="CanvasLayer/Popup/Popup"]
+modulate = Color(0.752941, 0.478431, 0.129412, 1)
+layout_mode = 1
+anchors_preset = 4
+anchor_top = 0.5
+anchor_bottom = 0.5
+offset_left = 46.0
+offset_top = -11.0
+offset_right = 217.0
+offset_bottom = 7.0
+grow_vertical = 2
+theme_override_colors/font_shadow_color = Color(0, 0, 0, 1)
+theme_override_font_sizes/normal_font_size = 7
+text = "Achievement Text"
+
+[node name="Body" type="RichTextLabel" parent="CanvasLayer/Popup/Popup"]
+layout_mode = 1
+anchors_preset = 4
+anchor_top = 0.5
+anchor_bottom = 0.5
+offset_left = 37.0
+offset_top = 2.0
+offset_right = 223.0
+offset_bottom = 18.0
+grow_vertical = 2
+theme_override_colors/default_color = Color(0.607843, 0.607843, 0.607843, 1)
+theme_override_colors/font_shadow_color = Color(0, 0, 0, 1)
+theme_override_font_sizes/normal_font_size = 7
+text = "Achievement Body"
+
+[node name="AwardShadow" type="TextureRect" parent="CanvasLayer/Popup/Popup"]
+modulate = Color(0, 0, 0, 1)
+layout_mode = 0
+offset_left = 36.0
+offset_top = 6.0
+offset_right = 45.0
+offset_bottom = 15.0
+texture = ExtResource("4_7grt2")
+expand_mode = 2
+
+[node name="Award" type="TextureRect" parent="CanvasLayer/Popup/Popup"]
+modulate = Color(0.752941, 0.478431, 0.129412, 1)
+layout_mode = 0
+offset_left = 34.5
+offset_top = 4.5
+offset_right = 43.5
+offset_bottom = 13.5
+texture = ExtResource("4_7grt2")
+expand_mode = 2
+
+[node name="ThumbnailMask" type="TextureRect" parent="CanvasLayer/Popup"]
+clip_children = 1
+layout_mode = 1
+anchors_preset = 4
+anchor_top = 0.5
+anchor_bottom = 0.5
+offset_left = 3.0
+offset_top = -12.0
+offset_right = 32.0
+offset_bottom = 12.0
+grow_vertical = 2
+texture = ExtResource("6_dwjjq")
+expand_mode = 1
+
+[node name="Thumbnail" type="TextureRect" parent="CanvasLayer/Popup/ThumbnailMask"]
+layout_mode = 0
+offset_right = 29.0
+offset_bottom = 24.0
+texture = ExtResource("4_1mson")
+expand_mode = 1
+
+[node name="AudioStreamPlayer" type="AudioStreamPlayer" parent="."]
+stream = ExtResource("8_74gug")
+volume_db = -9.043
+
+[node name="AudioStreamPlayer2" type="AudioStreamPlayer" parent="."]
+stream = ExtResource("9_c3xir")
+volume_db = -17.441
+
+[connection signal="clicked" from="CanvasLayer/Popup/MouseHandler" to="." method="_on_mouse_handler_clicked"]
+[connection signal="hovered" from="CanvasLayer/Popup/MouseHandler" to="." method="_on_mouse_handler_hovered"]
+[connection signal="unhovered" from="CanvasLayer/Popup/MouseHandler" to="." method="_on_mouse_handler_unhovered"]
+
+[editable path="CanvasLayer/Popup/MouseHandler"]
diff --git a/components/Achievements/Village 6.png b/components/Achievements/Village 6.png
new file mode 100644
index 0000000..4ad409e
Binary files /dev/null and b/components/Achievements/Village 6.png differ
diff --git a/components/Achievements/Village 6.png.import b/components/Achievements/Village 6.png.import
new file mode 100644
index 0000000..b6bef0a
--- /dev/null
+++ b/components/Achievements/Village 6.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bypm21lqwg7g0"
+path="res://.godot/imported/Village 6.png-38dba7a44eb73e6643b934b25a9dbbf9.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Achievements/Village 6.png"
+dest_files=["res://.godot/imported/Village 6.png-38dba7a44eb73e6643b934b25a9dbbf9.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/components/Achievements/achievement-back.ogg b/components/Achievements/achievement-back.ogg
new file mode 100644
index 0000000..89ff71d
Binary files /dev/null and b/components/Achievements/achievement-back.ogg differ
diff --git a/components/Achievements/achievement-back.ogg.import b/components/Achievements/achievement-back.ogg.import
new file mode 100644
index 0000000..9a6ae42
--- /dev/null
+++ b/components/Achievements/achievement-back.ogg.import
@@ -0,0 +1,19 @@
+[remap]
+
+importer="oggvorbisstr"
+type="AudioStreamOggVorbis"
+uid="uid://4m4dwlrv78l"
+path="res://.godot/imported/achievement-back.ogg-e15f114c4307f4746154a747d47f126e.oggvorbisstr"
+
+[deps]
+
+source_file="res://components/Achievements/achievement-back.ogg"
+dest_files=["res://.godot/imported/achievement-back.ogg-e15f114c4307f4746154a747d47f126e.oggvorbisstr"]
+
+[params]
+
+loop=false
+loop_offset=0
+bpm=0
+beat_count=0
+bar_beats=4
diff --git a/components/Achievements/achievement.wav b/components/Achievements/achievement.wav
new file mode 100644
index 0000000..e426e74
Binary files /dev/null and b/components/Achievements/achievement.wav differ
diff --git a/components/Achievements/achievement.wav.import b/components/Achievements/achievement.wav.import
new file mode 100644
index 0000000..7dbdd07
--- /dev/null
+++ b/components/Achievements/achievement.wav.import
@@ -0,0 +1,24 @@
+[remap]
+
+importer="wav"
+type="AudioStreamWAV"
+uid="uid://dxac8e02mctgp"
+path="res://.godot/imported/achievement.wav-eb6c9e09c6c75764f5cf65335ad5f16a.sample"
+
+[deps]
+
+source_file="res://components/Achievements/achievement.wav"
+dest_files=["res://.godot/imported/achievement.wav-eb6c9e09c6c75764f5cf65335ad5f16a.sample"]
+
+[params]
+
+force/8_bit=false
+force/mono=false
+force/max_rate=false
+force/max_rate_hz=44100
+edit/trim=false
+edit/normalize=false
+edit/loop_mode=0
+edit/loop_begin=0
+edit/loop_end=-1
+compress/mode=0
diff --git a/components/Achievements/achievements.gd b/components/Achievements/achievements.gd
new file mode 100644
index 0000000..056d2f5
--- /dev/null
+++ b/components/Achievements/achievements.gd
@@ -0,0 +1,261 @@
+extends Base
+## Achievement handler to check and unlock achievements
+##
+## An achievement handler that will check the available achievements defined in
+## both the game files and the mods. When called with triggers will check to see
+## if that trigger value unlocks achievements.
+
+signal achievement_unlocked(achievement: String)
+
+const CACHE_TIME = 60000
+const POPUP_POP_TIME = 1000
+const POPUP_LINGER_TIME = 5000
+const POPUP_HIDE_TIME = 1000
+const POPUP_BREAK = 1000
+const POPUP_HOVER_TIME = 250
+
+@onready var popup = $"CanvasLayer/Popup"
+@onready var popup_title = $"CanvasLayer/Popup/Popup/Title"
+@onready var popup_body = $"CanvasLayer/Popup/Popup/Body"
+@onready var audio = $"AudioStreamPlayer"
+@onready var audio2 = $"AudioStreamPlayer2"
+
+enum AchievementState {
+ ENTERING,
+ SHOWN,
+ HOVERED,
+ EXITING,
+ HIDDEN
+}
+
+var queue = []
+var cached_percents = {}
+var last_percent_pull: int
+var showing_popup = false
+var starting_popup_pos
+var starting_popup_size
+var popup_tween
+var popup_timer = 0
+var popup_time = 6
+#var entering = false
+#var exiting = false
+#var hovered = false
+
+var achievement_state = AchievementState.HIDDEN
+var hovered = false
+
+
+
+func _ready():
+ #_log_category = "ACH"
+ #_log_icon = "res://components/Logger/scroll-text.svg"
+ #_log_color = "#a166a1"
+ super()
+ _info("Achievements is active")
+ starting_popup_pos = popup.position
+ starting_popup_size = popup.size
+ popup.position.x = starting_popup_pos.x + starting_popup_size.x
+ popup.modulate = Color.TRANSPARENT
+ popup.scale = Vector2(0.5, 0.5)
+ if _triggerer:
+ _triggerer.listen("any", _on_trigger)
+
+
+func _on_trigger(data: Dictionary) -> void:
+ _check_trigger(data.trigger, "Trigger")
+
+
+func _process(delta):
+ popup_timer += delta
+
+ if popup_timer > popup_time and achievement_state == AchievementState.SHOWN:
+ _exit_popup()
+
+
+ if not queue.is_empty() and not showing_popup:
+ _show_popup(queue.pop_front())
+
+ if popup_tween and not popup_tween.is_running():
+ if hovered and achievement_state == AchievementState.SHOWN:
+ achievement_state = AchievementState.HOVERED
+
+ popup_tween = create_tween()
+ popup_tween.set_parallel()
+ popup_tween.set_trans(Tween.TRANS_QUAD)
+ popup_tween.tween_property(popup, "scale", Vector2(1.05, 1.05), float(POPUP_HOVER_TIME) / 1000)
+ popup_tween.tween_property(popup, "position:x", starting_popup_pos.x - 20, float(POPUP_HOVER_TIME) / 1000)
+ if not hovered and achievement_state == AchievementState.HOVERED:
+ achievement_state = AchievementState.SHOWN
+
+ popup_tween = create_tween()
+ popup_tween.set_parallel()
+ popup_tween.set_trans(Tween.TRANS_QUAD)
+ popup_tween.tween_property(popup, "scale", Vector2(1, 1), float(POPUP_HOVER_TIME) / 1000)
+ popup_tween.tween_property(popup, "position:x", starting_popup_pos.x, float(POPUP_HOVER_TIME) / 1000)
+
+
+func _exit_popup():
+ achievement_state = AchievementState.EXITING
+ audio2.play()
+
+ if popup_tween: popup_tween.kill()
+ popup_tween = create_tween()
+
+ popup_tween.set_ease(Tween.EASE_IN)
+ popup_tween.set_trans(Tween.TRANS_BACK)
+ popup_tween.set_parallel()
+ popup_tween.tween_property(popup, "modulate", Color.TRANSPARENT, POPUP_POP_TIME / 1000)
+ popup_tween.tween_property(popup, "scale", Vector2(0.5, 0.5), POPUP_POP_TIME / 1000)
+ popup_tween.tween_property(popup, "position:x", starting_popup_pos.x + starting_popup_size.x, POPUP_HIDE_TIME / 1000)
+ popup_tween.tween_interval(POPUP_BREAK / 1000)
+ popup_tween.chain()
+ popup_tween.tween_callback(func(): showing_popup = false)
+
+ _info("Hiding achievement popup")
+
+
+func _on_data_persisted(key: String, value, category: PersisterEnums.Scope):
+ _check_trigger(key, "Data", value)
+
+
+## Get all of the achievement info
+func get_achievements() -> Dictionary:
+ if not _data:
+ if (_logger): _logger.warn("Could not find data autoload when getting achievements")
+ return {}
+
+ if not _data.data.has("achievements"):
+ if (_logger): _logger.warn("Could not find achievement data")
+ return {}
+
+ var local_ach_data = _data.data.achievements.duplicate()
+
+ var percents = _get_achievement_percents()
+
+ for achievement in _data.data.achievements:
+ if percents.keys().has(achievement):
+ local_ach_data.percent = percents[achievement]
+
+ return _data.data.achievements
+
+
+## Get achievement info given the achievement slug
+func get_achievement(achievement: String) -> Dictionary:
+ if not _data:
+ if _logger: _logger.warn("Could not find data autoload when getting achievement %s" % [achievement])
+ return {}
+
+ if not _data.data.has("achievements"):
+ if _logger: _logger.warn("Could not find achievement data")
+ return {}
+
+ if not _data.data.achievements.has(achievement):
+ if _logger: _logger.warn("Could not find achievement %s" % [achievement])
+ return {}
+
+ var achievement_data = _data.data.achievements[achievement].duplicate()
+
+ # TODO: Add in achievement percent
+
+ return achievement_data
+
+
+func _show_popup(achievement: String) -> void:
+ var achievement_data = get_achievement(achievement)
+ achievement_state = AchievementState.ENTERING
+ popup_timer = 0
+
+ if achievement_data.is_empty():
+ if _logger: _logger.error("Could not get achievement %s info when trying to show" % [achievement])
+ return
+
+ popup_title.text = achievement_data.name
+ popup_body.text = achievement_data.description
+
+ showing_popup = true
+ popup.position.x = starting_popup_pos.x + starting_popup_size.x
+ popup.modulate = Color.TRANSPARENT
+ popup.scale = Vector2(0.5, 0.5)
+ #popup.position.y = starting_popup_pos.y + starting_popup_size.y
+
+ if popup_tween:
+ popup_tween.kill()
+
+ audio.play()
+
+ popup_tween = create_tween()
+ popup_tween.set_parallel()
+ popup_tween.set_ease(Tween.EASE_OUT)
+ popup_tween.set_trans(Tween.TRANS_BACK)
+ popup_tween.tween_property(popup, "modulate", Color.WHITE, POPUP_POP_TIME / 1000)
+ popup_tween.tween_property(popup, "scale", Vector2(1, 1), POPUP_POP_TIME / 1000)
+ popup_tween.tween_property(popup, "position:x", starting_popup_pos.x, POPUP_POP_TIME / 1000)
+ popup_tween.chain()
+ popup_tween.tween_callback(func():
+ achievement_state = AchievementState.SHOWN
+ )
+
+ _info("Showing achievement %s popup" % [achievement])
+
+
+func _get_achievement_percents() -> Dictionary:
+ if not last_percent_pull:
+ return _get_percents_from_db()
+
+ if last_percent_pull + CACHE_TIME <= Time.get_ticks_msec():
+ return cached_percents
+
+ return _get_percents_from_db()
+
+
+func _get_percents_from_db() -> Dictionary:
+ last_percent_pull = Time.get_ticks_msec()
+ return {}
+
+
+func _send_to_db(achievement: String) -> void:
+ pass
+
+
+func _check_trigger(key: String, type: String, value = null):
+ if not _data:
+ if (_logger): _logger.warn("Could not find data autoload when checking trigger")
+ return
+
+ if not _data.data.has("achievements"):
+ return
+
+ for achievement in _data.data.achievements:
+ # If the persister does not have the achievement already
+ if not _persister.get_value(achievement):
+ var ach_data = _data.data.achievements[achievement]
+
+ var valid = false
+
+ match type:
+ "Data":
+ if ach_data.has("key") and ach_data.key == key:
+ valid = ach_data.value <= value
+ "Trigger":
+ if ach_data.has("trigger") and ach_data.trigger == key:
+ valid = true
+
+ if valid:
+ achievement_unlocked.emit(achievement)
+ _persister.persist_data(achievement, true, PersisterEnums.Scope.PERMANENT)
+ _info("Added achievement %s to queue" % [achievement])
+ queue.push_back(achievement)
+ _send_to_db(achievement)
+
+
+func _on_mouse_handler_hovered():
+ hovered = true
+
+
+func _on_mouse_handler_clicked():
+ if achievement_state != AchievementState.EXITING:
+ _exit_popup()
+
+
+func _on_mouse_handler_unhovered():
+ hovered = false
diff --git a/components/Achievements/award.svg b/components/Achievements/award.svg
new file mode 100644
index 0000000..5a4fe24
--- /dev/null
+++ b/components/Achievements/award.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Achievements/award.svg.import b/components/Achievements/award.svg.import
new file mode 100644
index 0000000..a625d70
--- /dev/null
+++ b/components/Achievements/award.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ur6oiwg6ksns"
+path="res://.godot/imported/award.svg-6bd80af369d65193b7f216d026b9ff51.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Achievements/award.svg"
+dest_files=["res://.godot/imported/award.svg-6bd80af369d65193b7f216d026b9ff51.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Achievements/back.png b/components/Achievements/back.png
new file mode 100644
index 0000000..a42a0ca
Binary files /dev/null and b/components/Achievements/back.png differ
diff --git a/components/Achievements/back.png.import b/components/Achievements/back.png.import
new file mode 100644
index 0000000..01d89eb
--- /dev/null
+++ b/components/Achievements/back.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dat440dg86mjl"
+path="res://.godot/imported/back.png-7865ce000d117e80cc752a23c0588205.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Achievements/back.png"
+dest_files=["res://.godot/imported/back.png-7865ce000d117e80cc752a23c0588205.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/components/Achievements/information.txt b/components/Achievements/information.txt
new file mode 100644
index 0000000..9287528
--- /dev/null
+++ b/components/Achievements/information.txt
@@ -0,0 +1,8 @@
+name: Achievements
+short: Achievement handler to check and unlock achievements
+description: An achievement handler that will check the available achievements
+ defined in both the game files and the mods. When called with triggers will
+ check to see if that trigger value unlocks achievements.
+accent: #a166a1
+log_category: ACH
+icon: res://components/Logger/scroll-text.svg
diff --git a/components/Achievements/particle.aseprite b/components/Achievements/particle.aseprite
new file mode 100644
index 0000000..4d9848b
Binary files /dev/null and b/components/Achievements/particle.aseprite differ
diff --git a/components/Achievements/particle.png b/components/Achievements/particle.png
new file mode 100644
index 0000000..e571fbb
Binary files /dev/null and b/components/Achievements/particle.png differ
diff --git a/components/Achievements/particle.png.import b/components/Achievements/particle.png.import
new file mode 100644
index 0000000..3d16af9
--- /dev/null
+++ b/components/Achievements/particle.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://brmlpo1rv8m47"
+path="res://.godot/imported/particle.png-659e7c73fd80f7940a2d61abac8f1984.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Achievements/particle.png"
+dest_files=["res://.godot/imported/particle.png-659e7c73fd80f7940a2d61abac8f1984.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/components/Achievements/popup-background.png b/components/Achievements/popup-background.png
new file mode 100644
index 0000000..49d8c01
Binary files /dev/null and b/components/Achievements/popup-background.png differ
diff --git a/components/Achievements/popup-background.png.import b/components/Achievements/popup-background.png.import
new file mode 100644
index 0000000..be4e21f
--- /dev/null
+++ b/components/Achievements/popup-background.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dpmrungfff7jd"
+path="res://.godot/imported/popup-background.png-1f543826c612308bac959317015a3a98.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Achievements/popup-background.png"
+dest_files=["res://.godot/imported/popup-background.png-1f543826c612308bac959317015a3a98.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/components/Achievements/thumbnail-mask.png b/components/Achievements/thumbnail-mask.png
new file mode 100644
index 0000000..bd290e6
Binary files /dev/null and b/components/Achievements/thumbnail-mask.png differ
diff --git a/components/Achievements/thumbnail-mask.png.import b/components/Achievements/thumbnail-mask.png.import
new file mode 100644
index 0000000..3a0cd27
--- /dev/null
+++ b/components/Achievements/thumbnail-mask.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://dcj0xniur7dk1"
+path="res://.godot/imported/thumbnail-mask.png-9e7311d6f1141413915356d4bb61948c.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Achievements/thumbnail-mask.png"
+dest_files=["res://.godot/imported/thumbnail-mask.png-9e7311d6f1141413915356d4bb61948c.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/components/Audio/Audio.tscn b/components/Audio/Audio.tscn
new file mode 100644
index 0000000..8bb1e41
--- /dev/null
+++ b/components/Audio/Audio.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://cjcs6jhw45bj4"]
+
+[ext_resource type="Script" path="res://components/Audio/audio.gd" id="1_c2epj"]
+
+[node name="Audio" type="Node"]
+script = ExtResource("1_c2epj")
diff --git a/components/Audio/audio.gd b/components/Audio/audio.gd
new file mode 100644
index 0000000..ed4ba46
--- /dev/null
+++ b/components/Audio/audio.gd
@@ -0,0 +1,99 @@
+@icon("res://components/Audio/music.svg")
+extends Base
+
+var music_data
+var sfx_data
+
+var current_playing_music
+
+var tweens = {}
+
+func _spawned():
+ if _triggerer:
+ _triggerer.listen("any", _on_trigger)
+
+ music_data = _data.data.music
+ sfx_data = _data.data.sfx
+
+ for music in music_data:
+ var music_value = music_data[music]
+
+ if not _data.data.audio.has(music):
+ _error("Could not find file for music %s" % [music])
+ continue
+
+ music_value.track = _data.data.audio[music]
+
+ var music_object = AudioStreamPlayer.new()
+ music_object.name = "Music - %s" % [music_value.name]
+ music_object.stream = music_value.track
+ add_child(music_object)
+ music_value.object = music_object
+
+ _info("Loaded music %s" % [music])
+
+ for sfx in sfx_data:
+ var sfx_value = sfx_data[sfx]
+
+ if not _data.data.audio.has(sfx):
+ _error("Could not find file for sfx %s" % [sfx])
+ continue
+
+ sfx_value.track = _data.data.audio[sfx]
+
+ var sfx_object = AudioStreamPlayer.new()
+ sfx_object.name = "SFX - %s" % [sfx_value.name]
+ sfx_object.stream = sfx_value.track
+ add_child(sfx_object)
+ sfx_value.object = sfx_object
+
+ _info("Loaded sfx %s" % [sfx])
+
+
+func _on_trigger(data: Dictionary) -> void:
+ var trigger = data.trigger
+
+ if not trigger.begins_with("audio_"):
+ return
+
+ var audio_name = trigger.split("_", false, 1)[1]
+
+ for music in music_data:
+ var music_value = music_data[music]
+
+ if not music == audio_name:
+ continue
+
+ if not music_value.has("track"):
+ continue
+
+ if current_playing_music:
+ if tweens.has(current_playing_music):
+ tweens[current_playing_music].kill()
+
+ tweens[current_playing_music] = create_tween()
+ tweens[current_playing_music].tween_property(music_data[current_playing_music].object, "volume_db", -30, 1)
+ tweens[current_playing_music].tween_callback(func():
+ music_data[current_playing_music].object.stop()
+ )
+
+ if tweens.has(music):
+ tweens[music].kill()
+
+ if data.has("keep_position") and data.keep_position:
+ music_value.object.play(music_data[current_playing_music].object.get_playback_position())
+ else:
+ music_value.object.play()
+
+ tweens[music] = create_tween()
+ tweens[music].tween_property(music_data[music].object, "volume_db", 0, 1)
+
+ current_playing_music = music
+
+ return
+
+ for sfx in sfx_data:
+ if not sfx == audio_name:
+ continue
+
+ pass
diff --git a/components/Audio/information.txt b/components/Audio/information.txt
new file mode 100644
index 0000000..4eb13c5
--- /dev/null
+++ b/components/Audio/information.txt
@@ -0,0 +1,6 @@
+name: Audio
+short: ???
+description: ???
+accent: #32ad61
+log_category: AUD
+icon: res://components/Audio/music.svg
diff --git a/components/Audio/music.svg b/components/Audio/music.svg
new file mode 100644
index 0000000..b16547a
--- /dev/null
+++ b/components/Audio/music.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Audio/music.svg.import b/components/Audio/music.svg.import
new file mode 100644
index 0000000..8f7fc54
--- /dev/null
+++ b/components/Audio/music.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bnrlsuxullf0v"
+path="res://.godot/imported/music.svg-641a4ffd0c5780599b93a42e5f43eb33.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Audio/music.svg"
+dest_files=["res://.godot/imported/music.svg-641a4ffd0c5780599b93a42e5f43eb33.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Base/base.gd b/components/Base/base.gd
new file mode 100644
index 0000000..39a5fea
--- /dev/null
+++ b/components/Base/base.gd
@@ -0,0 +1,90 @@
+class_name Base
+extends Node
+## A base addon
+##
+## A base addon for shared functionality between all other addons
+
+var _logger
+var _data
+var _persister
+var _triggerer
+var _information = {
+ "log_category": "???",
+ "accent": "#000000",
+ "icon": "res://components/Cursor/mouse-pointer-2.svg"
+}
+
+var CORE_NODES = {
+ "Logger": {
+ "property": "_logger"
+ },
+ "Data": {
+ "property": "_data"
+ },
+ "Triggerer": {
+ "property": "_triggerer",
+ },
+ "Persister": {
+ "property": "_persister",
+ "connections": {"data_persisted": "_on_data_persisted"}
+ }
+}
+
+
+func _ready():
+ for node in CORE_NODES:
+ # Prevent circular references
+ if name == node:
+ break
+
+ # Connect up core nodes
+ var node_value = CORE_NODES[node]
+
+ if get_tree().root.has_node(node):
+ set(node_value.property, get_tree().root.get_node(node))
+ var property = get(node_value.property)
+
+ if node_value.has("connections"):
+ var connections = node_value.connections
+
+ for connection in connections:
+ var connection_callback = connections[connection]
+
+ property.get(connection).connect(get(connection_callback))
+
+ if _data and _data.components.has(name):
+ _information = _data.components[name].information
+
+ if _information.has("triggers"):
+ for trigger in _information.triggers:
+ _triggerer.listen(trigger, Callable(self, _information.triggers[trigger]))
+
+ if has_method("_prespawned"):
+ call("_prespawned")
+
+ _info("%s is active" % [name])
+
+ if has_method("_spawned"):
+ call("_spawned")
+
+ _info("%s is ready" % [name])
+
+
+func _on_data_persisted(key: String, value, category: PersisterEnums.Scope):
+ pass
+
+
+func _debug(message: String):
+ if _logger: _logger.debug(message, { "category": _information.log_category, "image": _information.icon, "color": _information.accent })
+
+
+func _info(message: String):
+ if _logger: _logger.info(message, { "category": _information.log_category, "image": _information.icon, "color": _information.accent })
+
+
+func _warn(message: String):
+ if _logger: _logger.warn(message, { "category": _information.log_category, "image": _information.icon, "color": _information.accent })
+
+
+func _error(message: String):
+ if _logger: _logger.error(message, { "category": _information.log_category, "image": _information.icon, "color": _information.accent })
diff --git a/components/Cursor/Cursor.tscn b/components/Cursor/Cursor.tscn
new file mode 100644
index 0000000..938b920
--- /dev/null
+++ b/components/Cursor/Cursor.tscn
@@ -0,0 +1,21 @@
+[gd_scene load_steps=4 format=3 uid="uid://qecwga1b4yqn"]
+
+[ext_resource type="Script" path="res://components/Cursor/cursor.gd" id="1_nmkwm"]
+[ext_resource type="Texture2D" uid="uid://yg134s8wlxmi" path="res://icon.svg" id="2_yaf0k"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_4cq27"]
+
+[node name="Cursor" type="CanvasLayer"]
+script = ExtResource("1_nmkwm")
+
+[node name="MouseControl" type="Area2D" parent="."]
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="MouseControl"]
+shape = SubResource("RectangleShape2D_4cq27")
+
+[node name="Sprite2D" type="Sprite2D" parent="MouseControl"]
+scale = Vector2(0.15625, 0.15625)
+texture = ExtResource("2_yaf0k")
+
+[connection signal="area_entered" from="MouseControl" to="." method="_on_mouse_control_area_entered"]
+[connection signal="area_exited" from="MouseControl" to="." method="_on_mouse_control_area_exited"]
diff --git a/components/Cursor/MouseHandler.tscn b/components/Cursor/MouseHandler.tscn
new file mode 100644
index 0000000..e610a47
--- /dev/null
+++ b/components/Cursor/MouseHandler.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://dykc1mgg5uopw"]
+
+[ext_resource type="Script" path="res://components/Cursor/mouse_handler.gd" id="1_n30mb"]
+
+[node name="MouseHandler" type="Area2D"]
+script = ExtResource("1_n30mb")
diff --git a/components/Cursor/cursor.gd b/components/Cursor/cursor.gd
new file mode 100644
index 0000000..11c8dce
--- /dev/null
+++ b/components/Cursor/cursor.gd
@@ -0,0 +1,30 @@
+@icon("res://components/Cursor/mouse-pointer-2.svg")
+extends Base
+
+@onready var mouse_control = $"MouseControl"
+
+
+func _process(delta):
+ Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN)
+ mouse_control.position = mouse_control.get_global_mouse_position()
+
+ if Input.is_action_just_pressed("left_click"):
+ var overlapping_areas = mouse_control.get_overlapping_areas()
+ overlapping_areas.sort_custom(func(a, b):
+ return a.z_index > b.z_index
+ )
+
+ for area in overlapping_areas:
+ if area.has_method("_on_clicked"):
+ if not area._on_clicked():
+ break
+
+
+func _on_mouse_control_area_entered(area):
+ if area.has_method("_on_hovered"):
+ area._on_hovered()
+
+
+func _on_mouse_control_area_exited(area):
+ if area.has_method("_on_unhovered"):
+ area._on_unhovered()
diff --git a/components/Cursor/information.txt b/components/Cursor/information.txt
new file mode 100644
index 0000000..c4152e4
--- /dev/null
+++ b/components/Cursor/information.txt
@@ -0,0 +1,6 @@
+name: Cursor
+short: ???
+description: ???
+accent: #35b84b
+log_category: CUR
+icon: res://components/Cursor/mouse-pointer-2.svg
diff --git a/components/Cursor/mouse-pointer-2.svg b/components/Cursor/mouse-pointer-2.svg
new file mode 100644
index 0000000..2bad1dd
--- /dev/null
+++ b/components/Cursor/mouse-pointer-2.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Cursor/mouse-pointer-2.svg.import b/components/Cursor/mouse-pointer-2.svg.import
new file mode 100644
index 0000000..c38994b
--- /dev/null
+++ b/components/Cursor/mouse-pointer-2.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cdu6b7pclsrrg"
+path="res://.godot/imported/mouse-pointer-2.svg-528a80312e1a4aed351ba060d183df97.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Cursor/mouse-pointer-2.svg"
+dest_files=["res://.godot/imported/mouse-pointer-2.svg-528a80312e1a4aed351ba060d183df97.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Cursor/mouse-pointer-click.svg b/components/Cursor/mouse-pointer-click.svg
new file mode 100644
index 0000000..31aa43d
--- /dev/null
+++ b/components/Cursor/mouse-pointer-click.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Cursor/mouse-pointer-click.svg.import b/components/Cursor/mouse-pointer-click.svg.import
new file mode 100644
index 0000000..973b20a
--- /dev/null
+++ b/components/Cursor/mouse-pointer-click.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://ddkp18the486t"
+path="res://.godot/imported/mouse-pointer-click.svg-f0b06ba161b377fd039b4f40e477ff4d.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Cursor/mouse-pointer-click.svg"
+dest_files=["res://.godot/imported/mouse-pointer-click.svg-f0b06ba161b377fd039b4f40e477ff4d.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Cursor/mouse_handler.gd b/components/Cursor/mouse_handler.gd
new file mode 100644
index 0000000..5895c0b
--- /dev/null
+++ b/components/Cursor/mouse_handler.gd
@@ -0,0 +1,39 @@
+@icon("res://components/Cursor/mouse-pointer-click.svg")
+extends Area2D
+
+signal clicked
+signal hovered
+signal unhovered
+
+@export var passthrough = false
+
+var _logger
+
+func _ready():
+ if get_tree().root.has_node("Logger"):
+ _logger = get_tree().root.get_node("Logger")
+
+func _on_clicked():
+ clicked.emit()
+ #_logger.info("Clicked mouse handler for object →%s←" % [get_parent().name], {
+ #"color": "#919191",
+ #"image": "res://components/Cursor/mouse-pointer-click.svg",
+ #"category": "MOU"
+ #})
+ return passthrough
+
+func _on_hovered():
+ hovered.emit()
+ #_logger.info("Hovered over mouse handler for object →%s←" % [get_parent().name], {
+ #"color": "#919191",
+ #"image": "res://components/Cursor/mouse-pointer-click.svg",
+ #"category": "MOU"
+ #})
+
+func _on_unhovered():
+ unhovered.emit()
+ #_logger.info("Unhovered mouse handler for object →%s←" % [get_parent().name], {
+ #"color": "#919191",
+ #"image": "res://components/Cursor/mouse-pointer-click.svg",
+ #"category": "MOU"
+ #})
diff --git a/components/Data/Data.tscn b/components/Data/Data.tscn
new file mode 100644
index 0000000..91ec856
--- /dev/null
+++ b/components/Data/Data.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://b4o7cekdsf4pu"]
+
+[ext_resource type="Script" path="res://components/Data/data.gd" id="1_xutp1"]
+
+[node name="Data" type="Node"]
+script = ExtResource("1_xutp1")
diff --git a/components/Data/book-marked.svg b/components/Data/book-marked.svg
new file mode 100644
index 0000000..0b394ea
--- /dev/null
+++ b/components/Data/book-marked.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Data/book-marked.svg.import b/components/Data/book-marked.svg.import
new file mode 100644
index 0000000..4b41724
--- /dev/null
+++ b/components/Data/book-marked.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://05aeafxrb5ph"
+path="res://.godot/imported/book-marked.svg-4d90597eaf7440ba51cd915ad6f20255.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Data/book-marked.svg"
+dest_files=["res://.godot/imported/book-marked.svg-4d90597eaf7440ba51cd915ad6f20255.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Data/data-toolbar.gd b/components/Data/data-toolbar.gd
new file mode 100644
index 0000000..56e1614
--- /dev/null
+++ b/components/Data/data-toolbar.gd
@@ -0,0 +1,18 @@
+const LOG_CATEGORY = "DATAT"
+const NAME = "Data"
+
+
+func send_log(parent):
+ parent.tools[NAME].node.info("Test Debug!", LOG_CATEGORY)
+
+
+func send_debug(parent):
+ parent.tools[NAME].node.debug("Test Log!", LOG_CATEGORY)
+
+
+func send_warning(parent):
+ parent.tools[NAME].node.warn("Test Warning!", LOG_CATEGORY)
+
+
+func send_error(parent):
+ parent.tools[NAME].node.error("Test Error!", LOG_CATEGORY)
diff --git a/components/Data/data.gd b/components/Data/data.gd
new file mode 100644
index 0000000..0a2de61
--- /dev/null
+++ b/components/Data/data.gd
@@ -0,0 +1,241 @@
+@icon("res://components/Data/book-marked.svg")
+class_name DataType
+extends Base
+## Data handler to read in data from files
+##
+## A data handler that will read in data from txt files and other sources for
+## use in various parts of the game. Reads both from the parts folder in the
+## main game directory and the mods folder in the user folder to allow users to
+## easily mod the game.
+
+var _FILE_FUNCTIONS = {
+ "*.txt": _read_txt,
+ "*.png": _read_resource,
+ "*.png.import": _read_png_import,
+ "*.ogg": _read_resource,
+ "*.mp3": _read_resource,
+ "*.wav": _read_resource,
+ "*.gd": _read_script_resource,
+}
+
+var _FILE_LOCATION_OVERRIDES = {
+ "*.gd": "scripts",
+ "*.png.import": "images",
+ "*.png": "images",
+ "*.ogg": "audio",
+ "*.wav": "audio"
+}
+
+var data = {}
+var components = {}
+var location_overrides = {}
+
+
+func _prespawned():
+ _information = {
+ "accent": "#a27155",
+ "log_category": "DATA",
+ "icon": "res://components/Data/book-marked.svg"
+ }
+
+
+func _spawned():
+ reload_data()
+
+
+## Reload the data object by pulling from available sources
+func reload_data() -> void:
+ data = {} # Remove all old mod data
+
+ # Open part folder from project (Ignore if none) and if exists set mod data to it
+ var res_dir = DirAccess.open("res://")
+ if res_dir.dir_exists("parts"):
+ data = _read_directory("res://parts")
+ data = _merge_objects(location_overrides, data)
+
+ if res_dir.dir_exists("components"):
+ location_overrides = {}
+ components = _read_directory("res://components")
+ components = _merge_objects(location_overrides, components)
+
+
+func _merge_objects(object1, object2):
+ var newObject = {}
+ for key in object1:
+ newObject[key] = object1[key]
+ for key in object2:
+ if(newObject.has(key)):
+ if(typeof(newObject[key]) == TYPE_STRING):
+ newObject[key] = object1[key]
+ elif(typeof(newObject[key]) == TYPE_OBJECT):
+ if newObject[key] is Texture:
+ newObject[key] = object1[key]
+ else:
+ newObject[key] = _merge_objects(object1[key], object2[key])
+ elif(typeof(newObject[key]) == TYPE_DICTIONARY):
+ newObject[key] = _merge_objects(object1[key], object2[key])
+ elif(typeof(newObject[key]) == TYPE_ARRAY):
+ newObject[key] = object1[key] + object2[key]
+ else:
+ newObject[key] = object2[key]
+ return newObject
+
+
+func _read_directory(path) -> Dictionary:
+ var dir = DirAccess.open(path)
+ var local_data = {}
+
+ if not dir:
+ _warn("Could not read directory [%s]" % [path])
+ return {}
+
+ dir.list_dir_begin()
+ var file_name = dir.get_next()
+ while file_name != "":
+ if dir.current_is_dir():
+ var micro_data = _read_directory("%s/%s" % [path, file_name])
+ if micro_data:
+ local_data[file_name] = micro_data
+ else:
+ var file_handling = _get_file_handling(file_name)
+
+ if (file_handling):
+ var location_override = _get_location_override(file_name)
+
+ if location_override:
+ var micro_data = file_handling.call("%s/%s" % [path, file_name])
+ if not location_overrides.has(location_override):
+ location_overrides[location_override] = {}
+ location_overrides[location_override][file_name.split(".")[0]] = micro_data
+ else:
+ var micro_data = file_handling.call("%s/%s" % [path, file_name])
+ if micro_data:
+ local_data[file_name.split(".")[0]] = micro_data
+
+ file_name = dir.get_next()
+
+ _info("Read directory ♢%s♢" % [path])
+ return local_data
+
+
+func _get_file_handling(file):
+ var split_file = file.split(".", true, 1)
+
+ for file_function in _FILE_FUNCTIONS:
+ if(file_function == file):
+ return _FILE_FUNCTIONS[file_function]
+
+ var split_file_function = file_function.split(".", true, 1)
+
+ if(split_file_function[0] == "*" and split_file_function[1] == split_file[1]):
+ return _FILE_FUNCTIONS[file_function]
+
+ return null
+
+
+func _read_script_resource(path):
+ return load(path)
+
+
+func _read_resource(path):
+ var song = load(path)
+ return song
+
+
+func _read_png_import(path):
+ var image = load(path.rsplit(".", true, 1)[0])
+ return image
+
+
+func _get_location_override(file):
+ var split_file = file.split(".", true, 1)
+
+ for location_override in _FILE_LOCATION_OVERRIDES:
+ if location_override == file:
+ return _FILE_LOCATION_OVERRIDES[location_override]
+
+ var split_file_function = location_override.split(".", true, 1)
+
+ if(split_file_function[0] == "*" and split_file_function[1] == split_file[1]):
+ return _FILE_LOCATION_OVERRIDES[location_override]
+
+
+func _read_txt(path) -> Dictionary:
+ # Open file for reading
+ var file = FileAccess.open(path, FileAccess.READ)
+ var content = file.get_as_text()
+ var local_data = {}
+
+ # Separate into lines
+ var split_content = content.split("\n", false)
+
+ # Indentation
+ var indentation = 0
+ var indentation_levels = []
+
+ # Iterate over everything
+ for content_piece in split_content:
+ # Fix Indentation to actual content level
+ var actual_indentation = _count_indentation(content_piece)
+
+ while(actual_indentation < indentation):
+ indentation -= 1
+ indentation_levels.pop_back()
+
+ # Navigate to current indentation data
+ var micro_data = local_data
+ var previous_data = null
+ var i = 0
+ while i < indentation:
+ previous_data = micro_data
+ micro_data = micro_data[indentation_levels[i]]
+ i += 1
+
+ # Reading
+ if(content_piece.ends_with("[]")):
+ # Array handling
+ var trimmed_content = content_piece.strip_edges().trim_suffix("[]")
+
+ indentation += 1
+ indentation_levels.append(trimmed_content)
+ micro_data[trimmed_content] = {}
+ else:
+ var split_args = content_piece.split(":", true, 1)
+
+ if split_args.size() == 2:
+ # Dict Handling
+ var key = split_args[0].strip_edges()
+ var value = split_args[1].strip_edges()
+
+ if value.is_valid_int():
+ value = value.to_int()
+
+ micro_data[key] = value
+ elif split_args.size() == 1:
+ # Value Handling
+ var value = split_args[0].strip_edges()
+
+ if typeof(micro_data) == TYPE_DICTIONARY:
+ # TODO: FIX BELOW
+ if indentation_levels.size() == 0:
+ continue
+
+ if previous_data[indentation_levels[indentation_levels.size()-1]].keys().size() == 0:
+ previous_data[indentation_levels[indentation_levels.size()-1]] = [split_args[0].strip_edges()]
+ else:
+ pass
+ else:
+ previous_data[indentation_levels[indentation_levels.size()-1]] += [split_args[0].strip_edges()]
+ return local_data
+
+
+func _count_indentation(string: String):
+ var tabs = 0
+
+ for character in string:
+ if(character == "\t"):
+ tabs += 1
+ else:
+ return tabs
+
+ return tabs
diff --git a/components/Data/information.txt b/components/Data/information.txt
new file mode 100644
index 0000000..3a7bbb4
--- /dev/null
+++ b/components/Data/information.txt
@@ -0,0 +1,10 @@
+name: Data
+short: Data handler to read in data from files
+description: |
+ A data handler that will read in data from txt files and other
+ sources for use in various parts of the game. Reads both from the
+ parts folder in the main game directory and the mods folder in the
+ user folder to allow users to easily mod the game.
+accent: #a27155
+log_category: DATA
+icon: res://components/Data/book-marked.svg
diff --git a/components/Data/toolbar.txt b/components/Data/toolbar.txt
new file mode 100644
index 0000000..82c0550
--- /dev/null
+++ b/components/Data/toolbar.txt
@@ -0,0 +1,20 @@
+debug[]
+ type: button
+ name: Send Debug
+ accent: #11a1a1
+ function: send_debug
+log[]
+ type: button
+ name: Send Log
+ accent: #11a11a
+ function: send_log
+warning[]
+ type: button
+ name: Send Warning
+ accent: #a1a111
+ function: send_warning
+error[]
+ type: button
+ name: Send Error
+ accent: #f13333
+ function: send_error
diff --git a/components/Dialogue/Dialogue.tscn b/components/Dialogue/Dialogue.tscn
new file mode 100644
index 0000000..ba8fedc
--- /dev/null
+++ b/components/Dialogue/Dialogue.tscn
@@ -0,0 +1,100 @@
+[gd_scene load_steps=6 format=3 uid="uid://mhltvrm84abx"]
+
+[ext_resource type="Script" path="res://components/Dialogue/dialogue.gd" id="1_0g08k"]
+[ext_resource type="Script" path="res://components/Dialogue/area.gd" id="3_4w6ab"]
+[ext_resource type="Texture2D" uid="uid://camtxohytwrqd" path="res://components/Dialogue/textbox.png" id="3_o1ce5"]
+[ext_resource type="Texture2D" uid="uid://yg134s8wlxmi" path="res://icon.svg" id="5_s3ups"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_wsoph"]
+size = Vector2(480, 80)
+
+[node name="Dialogue" type="CanvasLayer"]
+script = ExtResource("1_0g08k")
+
+[node name="TextBox" type="Control" parent="."]
+layout_mode = 3
+anchors_preset = 0
+offset_left = 80.0
+offset_top = 280.0
+offset_right = 560.0
+offset_bottom = 360.0
+pivot_offset = Vector2(240, 80)
+
+[node name="MinimizeHandler" type="Area2D" parent="TextBox"]
+visible = false
+z_index = 10
+script = ExtResource("3_4w6ab")
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="TextBox/MinimizeHandler"]
+position = Vector2(240, 40)
+shape = SubResource("RectangleShape2D_wsoph")
+
+[node name="Graphics" type="TextureRect" parent="TextBox"]
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -240.0
+offset_top = -80.0
+offset_right = 240.0
+grow_horizontal = 2
+grow_vertical = 0
+texture = ExtResource("3_o1ce5")
+
+[node name="TextBoxAvatar" type="TextureRect" parent="TextBox/Graphics"]
+layout_mode = 1
+anchors_preset = 7
+anchor_left = 0.5
+anchor_top = 1.0
+anchor_right = 0.5
+anchor_bottom = 1.0
+offset_left = -206.0
+offset_top = -104.0
+offset_right = -158.0
+offset_bottom = -40.0
+grow_horizontal = 2
+grow_vertical = 0
+texture = ExtResource("5_s3ups")
+
+[node name="TextBoxText" type="RichTextLabel" parent="TextBox/Graphics"]
+modulate = Color(0.705882, 0.690196, 0.552941, 1)
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -125.0
+offset_top = -24.0
+offset_right = 226.0
+offset_bottom = 30.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_colors/font_shadow_color = Color(0, 0, 0, 1)
+theme_override_constants/shadow_offset_y = 1
+theme_override_constants/shadow_offset_x = 0
+theme_override_font_sizes/normal_font_size = 16
+text = "This is test text"
+
+[node name="TextBoxName" type="RichTextLabel" parent="TextBox/Graphics"]
+modulate = Color(0.705882, 0.690196, 0.552941, 1)
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -237.0
+offset_top = 14.0
+offset_right = -125.0
+offset_bottom = 38.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_colors/font_shadow_color = Color(0, 0, 0, 1)
+theme_override_constants/shadow_offset_y = 1
+theme_override_constants/shadow_offset_x = 0
+theme_override_font_sizes/normal_font_size = 16
+bbcode_enabled = true
+text = "[center]NAME"
diff --git a/components/Dialogue/area.gd b/components/Dialogue/area.gd
new file mode 100644
index 0000000..4858f31
--- /dev/null
+++ b/components/Dialogue/area.gd
@@ -0,0 +1,9 @@
+extends Area2D
+
+signal clicked
+
+@export var passthrough = false
+
+func _on_clicked():
+ clicked.emit()
+ return passthrough
diff --git a/components/Dialogue/dialogue.gd b/components/Dialogue/dialogue.gd
new file mode 100644
index 0000000..b08075a
--- /dev/null
+++ b/components/Dialogue/dialogue.gd
@@ -0,0 +1,144 @@
+extends Base
+
+var used_lines = []
+var mouse_over = false
+
+@onready var text_box = $"TextBox"
+@onready var text_box_text = $"TextBox/Graphics/TextBoxText"
+@onready var text_box_name = $"TextBox/Graphics/TextBoxName"
+@onready var text_box_image = $"TextBox/Graphics/TextBoxAvatar"
+@onready var minimize_handler = $"TextBox/MinimizeHandler"
+
+var pop_tween
+var last_lines = {}
+
+func _spawned():
+ text_box.modulate = Color.TRANSPARENT
+ text_box.scale = Vector2(0, 0)
+ text_box.position.y = 360
+
+ minimize_handler.clicked.connect(_on_clicked)
+
+ if _triggerer:
+ _triggerer.listen("any", _on_trigger)
+
+
+func _on_trigger(data: Dictionary) -> void:
+ if has_trigger(data.trigger):
+ trigger_dialogue(data.trigger)
+
+
+func _on_clicked():
+ if pop_tween:
+ pop_tween.kill()
+
+ pop_tween = create_tween()
+ pop_tween.set_ease(Tween.EASE_IN)
+ pop_tween.set_trans(Tween.TRANS_BACK)
+ pop_tween.set_parallel()
+ pop_tween.tween_property(text_box, "modulate", Color.TRANSPARENT, 0.5)
+ pop_tween.tween_property(text_box, "scale", Vector2(0, 0), 0.5)
+ pop_tween.tween_property(text_box, "position:y", 360, 0.5)
+ pop_tween.chain()
+
+func get_priority_from_string(string: String):
+ match string:
+ "low": return 1
+ "medium": return 2
+ "high": return 3
+
+func trigger_dialogue(trigger: String):
+ _info("Dialogue trigger %s triggered" % [trigger])
+ var dialogue = get_dialogue(trigger)
+
+ if not dialogue:
+ return
+
+ var lines = dialogue.values()
+
+
+
+
+ if pop_tween:
+ pop_tween.kill()
+
+ #var person = Data.data.people[lines[0].character]
+
+ text_box_text.text = lines[0].text
+ #text_box_image.texture = Data.data.images[lines[0].character]
+ #text_box_name.text = "[center]%s" % [person.short]
+
+ pop_tween = create_tween()
+ pop_tween.set_ease(Tween.EASE_OUT)
+ pop_tween.set_trans(Tween.TRANS_BACK)
+ pop_tween.set_parallel()
+ pop_tween.tween_property(text_box, "modulate", Color.WHITE, 0.5)
+ pop_tween.tween_property(text_box, "scale", Vector2(1, 1), 0.5)
+ pop_tween.tween_property(text_box, "position:y", 280, 0.5)
+ pop_tween.chain()
+
+ #get_parent().move_child(self, get_parent().get_child_count() - 1)
+
+
+func has_trigger(trigger: String):
+ var dialogue_files = Data.data.dialogue.values()
+
+ for file in dialogue_files:
+ for key in file:
+ if file[key].has("trigger"):
+ if file[key].trigger == trigger:
+ return true
+
+ return false
+
+
+func get_dialogue(trigger: String):
+ var dialogue_files = Data.data.dialogue.values()
+
+ var combined_object = {}
+
+ for file in dialogue_files:
+ for key in file:
+ combined_object[key] = file[key]
+ combined_object[key].key = key
+
+ var lines = combined_object.values()
+ var trigger_lines = lines.filter(func(x): return x.trigger == trigger)
+
+ trigger_lines.sort_custom(func(a, b):
+ var apriority = a.rules.priority if a.rules.has("priority") else "medium"
+ var bpriority = b.rules.priority if b.rules.has("priority") else "medium"
+
+ if get_priority_from_string(apriority) == get_priority_from_string(bpriority):
+ return randf() < 0.5
+
+ return get_priority_from_string(apriority) > get_priority_from_string(bpriority)
+ )
+
+ for line in trigger_lines:
+ var invalid = false
+
+ if last_lines.has(line.trigger) and last_lines[line.trigger] == line.key:
+ invalid = true
+
+ for rule in line.rules:
+ if rule == "priority":
+ continue
+ elif rule == "unique":
+ if used_lines.has(line.key):
+ invalid = true
+ break
+ else:
+ var value = Persister.get_value(rule)
+ if value != line.rules[rule]:
+ invalid = true
+ break
+
+ if invalid:
+ continue
+
+ last_lines[line.trigger] = line.key
+ used_lines.push_back(line.key)
+ return line.lines
+
+ return null
diff --git a/components/Dialogue/information.txt b/components/Dialogue/information.txt
new file mode 100644
index 0000000..bfab415
--- /dev/null
+++ b/components/Dialogue/information.txt
@@ -0,0 +1,6 @@
+name: Dialogue
+short: ???
+description: ???
+accent: #2ec778
+log_category: DIA
+icon: res://components/Dialogue/message-square-more.svg
diff --git a/components/Dialogue/message-square-more.svg b/components/Dialogue/message-square-more.svg
new file mode 100644
index 0000000..c9d13fd
--- /dev/null
+++ b/components/Dialogue/message-square-more.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Dialogue/message-square-more.svg.import b/components/Dialogue/message-square-more.svg.import
new file mode 100644
index 0000000..f76a029
--- /dev/null
+++ b/components/Dialogue/message-square-more.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bwghow6a1p84w"
+path="res://.godot/imported/message-square-more.svg-c15333f4fba2c19064690b510c5f7565.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Dialogue/message-square-more.svg"
+dest_files=["res://.godot/imported/message-square-more.svg-c15333f4fba2c19064690b510c5f7565.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Dialogue/textbox.aseprite b/components/Dialogue/textbox.aseprite
new file mode 100644
index 0000000..82f4bd6
Binary files /dev/null and b/components/Dialogue/textbox.aseprite differ
diff --git a/components/Dialogue/textbox.png b/components/Dialogue/textbox.png
new file mode 100644
index 0000000..852ab79
Binary files /dev/null and b/components/Dialogue/textbox.png differ
diff --git a/components/Dialogue/textbox.png.import b/components/Dialogue/textbox.png.import
new file mode 100644
index 0000000..d67d920
--- /dev/null
+++ b/components/Dialogue/textbox.png.import
@@ -0,0 +1,34 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://camtxohytwrqd"
+path="res://.godot/imported/textbox.png-a3656dca7d0232cc5645bc8f60474933.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Dialogue/textbox.png"
+dest_files=["res://.godot/imported/textbox.png-a3656dca7d0232cc5645bc8f60474933.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/components/Leaderboard/Leaderboard.tscn b/components/Leaderboard/Leaderboard.tscn
new file mode 100644
index 0000000..a343f72
--- /dev/null
+++ b/components/Leaderboard/Leaderboard.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://dvjtjq2j522co"]
+
+[ext_resource type="Script" path="res://components/Leaderboard/leaderboard.gd" id="1_gvlwy"]
+
+[node name="Leaderboard" type="Node"]
+script = ExtResource("1_gvlwy")
diff --git a/components/Leaderboard/information.txt b/components/Leaderboard/information.txt
new file mode 100644
index 0000000..4283aae
--- /dev/null
+++ b/components/Leaderboard/information.txt
@@ -0,0 +1,6 @@
+name: Leaderboard
+short: ???
+description: ???
+accent: #2e4fc7
+log_category: LEA
+icon: res://components/Leaderboard/trophy.svg
diff --git a/components/Leaderboard/leaderboard.gd b/components/Leaderboard/leaderboard.gd
new file mode 100644
index 0000000..c934586
--- /dev/null
+++ b/components/Leaderboard/leaderboard.gd
@@ -0,0 +1,2 @@
+@icon("res://components/Leaderboard/trophy.svg")
+extends Base
diff --git a/components/Leaderboard/trophy.svg b/components/Leaderboard/trophy.svg
new file mode 100644
index 0000000..12a06e2
--- /dev/null
+++ b/components/Leaderboard/trophy.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Leaderboard/trophy.svg.import b/components/Leaderboard/trophy.svg.import
new file mode 100644
index 0000000..c53616d
--- /dev/null
+++ b/components/Leaderboard/trophy.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://for54f1or3ir"
+path="res://.godot/imported/trophy.svg-50b272fe3a1d033c20559dd811d9f235.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Leaderboard/trophy.svg"
+dest_files=["res://.godot/imported/trophy.svg-50b272fe3a1d033c20559dd811d9f235.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Logger/Logger.tscn b/components/Logger/Logger.tscn
new file mode 100644
index 0000000..6b955d8
--- /dev/null
+++ b/components/Logger/Logger.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://7p8ecy62n2h8"]
+
+[ext_resource type="Script" path="res://components/Logger/logger.gd" id="1_gse6q"]
+
+[node name="Logger" type="Node"]
+script = ExtResource("1_gse6q")
diff --git a/components/Logger/commands.gd b/components/Logger/commands.gd
new file mode 100644
index 0000000..6428a4d
--- /dev/null
+++ b/components/Logger/commands.gd
@@ -0,0 +1,2 @@
+func send_log():
+ pass
diff --git a/components/Logger/information.txt b/components/Logger/information.txt
new file mode 100644
index 0000000..2f84ff4
--- /dev/null
+++ b/components/Logger/information.txt
@@ -0,0 +1,9 @@
+name: Logger
+short: A logger to log data to relevant locations
+description: A logger that logs various forms of data at different log levels to
+ both built in locations in Godot as well as other components that listen to
+ the provided log created signal.
+accent: #71a255
+log_category: LOG
+icon: res://components/Logger/scroll-text.svg
+version: v1.0.0
diff --git a/components/Logger/logger-toolbar.gd b/components/Logger/logger-toolbar.gd
new file mode 100644
index 0000000..780725b
--- /dev/null
+++ b/components/Logger/logger-toolbar.gd
@@ -0,0 +1,34 @@
+const LOG_CATEGORY = "TOOLT"
+const NAME = "Logger"
+
+
+func send_log(parent):
+ parent.info("Test Log!", {
+ "image": "res://components/Toolbar/hammer.svg",
+ "color": "#818181",
+ "category": "TST"
+ })
+
+
+func send_debug(parent):
+ parent.debug("Test Debug!", {
+ "image": "res://components/Toolbar/hammer.svg",
+ "color": "#818181",
+ "category": "TST"
+ })
+
+
+func send_warning(parent):
+ parent.warn("Test Warn!", {
+ "image": "res://components/Toolbar/hammer.svg",
+ "color": "#818181",
+ "category": "TST"
+ })
+
+
+func send_error(parent):
+ parent.error("Test Error!", {
+ "image": "res://components/Toolbar/hammer.svg",
+ "color": "#818181",
+ "category": "TST"
+ })
diff --git a/components/Logger/logger.gd b/components/Logger/logger.gd
new file mode 100644
index 0000000..f1831bc
--- /dev/null
+++ b/components/Logger/logger.gd
@@ -0,0 +1,133 @@
+@icon("res://components/Logger/scroll-text.svg")
+extends Node
+## A logger to log data to relevant locations
+##
+## A logger that logs various forms of data at different log levels to both
+## built in locations in Godot as well as other components that listen to the
+## provided log created signal.
+
+signal log_created(message: String, level: LogLevel)
+
+enum LogLevel {
+ DEBUG,
+ INFO,
+ WARN,
+ ERROR
+}
+
+## The log level that should be outputted as a minimum.
+@export var log_level: LogLevel
+
+
+## Log a message at log level debug (meant for troubleshooting).
+func debug(message: String, arguments: Dictionary = {}) -> void:
+ _log(message, LogLevel.DEBUG, arguments)
+
+
+## Log a message at log level info (log to indicate something happened).
+func info(message: String, arguments: Dictionary = {}) -> void:
+ _log(message, LogLevel.INFO, arguments)
+
+
+## Log a warning at log level warning (something unexpected happened but it can
+## continue).
+func warn(message: String, arguments: Dictionary = {}) -> void:
+ _log(message, LogLevel.WARN, arguments)
+
+
+## Log an error at log level error (an issue that prevents something from
+## functioning).
+func error(message: String, arguments: Dictionary = {}) -> void:
+ _log(message, LogLevel.ERROR, arguments)
+
+
+func _log(message: String, level: LogLevel, arguments: Dictionary = {}) -> void:
+ var category = arguments.category if arguments.has("category") and arguments.category else "???"
+ var color = arguments.color if arguments.has("color") else "olive"
+ var image = arguments.image if arguments.has("image") and arguments.image else "res://components/Logger/scroll-text.svg"
+
+ var adjusted_message = _clean_message(message)
+
+ var constructed_message = "[color=%s][%s][/color] [img= width=12 height=12 valign=center]%s[/img] %s" % [color, category, image, adjusted_message]
+ print_rich(constructed_message)
+ log_created.emit(constructed_message, level)
+
+
+func _clean_message(message: String) -> String:
+ var cleans = [
+ {
+ "type": "button",
+ "regex": "μ(.*)μ",
+ "color": Color.html("#a4bf37")
+ },
+ {
+ "type": "key",
+ "regex": "<(.*)>",
+ "color": Color.html("#42ad24")
+ },
+ {
+ "type": "tool",
+ "regex": "λ(.*)λ",
+ "color": Color.html("#bf9d37")
+ },
+ {
+ "type": "object",
+ "regex": "→(.*)←",
+ "color": Color.html("#854322")
+ },
+ {
+ "type": "path",
+ "regex": "♢(.*)♢",
+ "color": Color.html("#22852e")
+ },
+ {
+ "type": "function",
+ "regex": "∨(.*)∨",
+ "color": Color.html("#ad2452")
+ },
+ {
+ "type": "trigger",
+ "regex": "∧(.*)∧",
+ "color": Color.html("#ad2d24")
+ },
+ {
+ "type": "category",
+ "regex": "\\{(.*)\\}",
+ "color": Color.html("#ad9b24")
+ },
+ {
+ "type": "value",
+ "regex": "\\|(.*)\\|",
+ "color": Color.TEAL
+ }
+ ]
+
+ var adjusted_message = message
+
+ for clean in cleans:
+ adjusted_message = _replace_regex_color(adjusted_message, clean.regex, clean.color)
+
+ return adjusted_message
+
+
+func _replace_regex_color(message: String, regex_string: String, color: Color) -> String:
+ return _replace_regex(message, regex_string, "[color=" + color.to_html() + "]%s[/color]")
+
+
+func _replace_regex(message: String, regex_string: String, new_content: String) -> String:
+ var adjusted_message = message
+ var stripped_message = message
+
+ var regex = RegEx.new()
+ regex.compile(regex_string)
+ var result = regex.search(adjusted_message)
+
+ while result:
+ var before_content = adjusted_message.substr(0, result.get_start())
+ var after_content = adjusted_message.substr(result.get_end(), adjusted_message.length())
+ var template_string = "%s" + new_content + "%s"
+ adjusted_message = template_string % [before_content, result.get_string(1), after_content]
+
+ result = regex.search(adjusted_message)
+
+ return adjusted_message
diff --git a/components/Logger/scroll-text.svg b/components/Logger/scroll-text.svg
new file mode 100644
index 0000000..e5bc2c0
--- /dev/null
+++ b/components/Logger/scroll-text.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Logger/scroll-text.svg.import b/components/Logger/scroll-text.svg.import
new file mode 100644
index 0000000..8113916
--- /dev/null
+++ b/components/Logger/scroll-text.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bt1irjjewldcq"
+path="res://.godot/imported/scroll-text.svg-3edd1135d80784e9e14ee4871d671a9a.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Logger/scroll-text.svg"
+dest_files=["res://.godot/imported/scroll-text.svg-3edd1135d80784e9e14ee4871d671a9a.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Logger/settings.txt b/components/Logger/settings.txt
new file mode 100644
index 0000000..e5864fc
--- /dev/null
+++ b/components/Logger/settings.txt
@@ -0,0 +1,11 @@
+log_level[]
+ name: Log Level
+ description: The level of info you want as a minimum to be logged to
+ relevant locations.
+ category: misc
+ default: LogLevel.WARN
+ values[]
+ LogLevel.DEBUG
+ LogLevel.INFO
+ LogLevel.WARN
+ LogLevel.ERROR
diff --git a/components/Logger/toolbar.txt b/components/Logger/toolbar.txt
new file mode 100644
index 0000000..8295f47
--- /dev/null
+++ b/components/Logger/toolbar.txt
@@ -0,0 +1,24 @@
+debug[]
+ type: button
+ name: Send Debug
+ accent: #11a1a1
+ function: send_debug
+test[]
+ type: label
+ name: Testing Label
+ accent: #11a1cc
+log[]
+ type: button
+ name: Send Log
+ accent: #11a11a
+ function: send_log
+warning[]
+ type: button
+ name: Send Warning
+ accent: #a1a111
+ function: send_warning
+error[]
+ type: button
+ name: Send Error
+ accent: #f13333
+ function: send_error
diff --git a/components/Menu/Menu.tscn b/components/Menu/Menu.tscn
new file mode 100644
index 0000000..02acd1c
--- /dev/null
+++ b/components/Menu/Menu.tscn
@@ -0,0 +1,691 @@
+[gd_scene load_steps=14 format=3 uid="uid://dcu7jrwg24hg1"]
+
+[ext_resource type="Script" path="res://components/Menu/menu.gd" id="1_f186y"]
+[ext_resource type="Theme" uid="uid://ck7603ob4gflc" path="res://Fonts/Theme.tres" id="1_fowhs"]
+[ext_resource type="Shader" path="res://components/Menu/god_rays.gdshader" id="1_n8upd"]
+[ext_resource type="Script" path="res://components/Menu/populate_from_metadata.gd" id="3_727p5"]
+[ext_resource type="PackedScene" uid="uid://cmyjaeahyipq4" path="res://components/Menu/MenuButton.tscn" id="5_goj86"]
+[ext_resource type="Script" path="res://components/Menu/credits.gd" id="6_b724h"]
+
+[sub_resource type="ShaderMaterial" id="ShaderMaterial_ygqow"]
+shader = ExtResource("1_n8upd")
+shader_parameter/angle = 0.525
+shader_parameter/position = 0.27
+shader_parameter/spread = 0.595
+shader_parameter/cutoff = 0.0710001
+shader_parameter/falloff = 0.845
+shader_parameter/edge_fade = 0.34
+shader_parameter/speed = 1.0
+shader_parameter/ray1_density = 8.0
+shader_parameter/ray2_density = 30.0
+shader_parameter/ray2_intensity = 0.169
+shader_parameter/color = Vector4(1, 0.9, 0.65, 0.1)
+shader_parameter/hdr = false
+shader_parameter/seed = 5.0
+
+[sub_resource type="Animation" id="Animation_5jiy0"]
+resource_name = "show_main"
+length = 5.0
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameTitle:position")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(135, 168), Vector2(185.297, 168), Vector2(226.423, 168), Vector2(259.221, 168), Vector2(284.538, 168), Vector2(303.219, 168), Vector2(316.109, 168), Vector2(324.053, 168), Vector2(327.897, 168), Vector2(328.486, 168), Vector2(326.665, 168), Vector2(323.28, 168), Vector2(319.175, 168), Vector2(315.197, 168), Vector2(312.19, 168), Vector2(311, 168)]
+}
+tracks/1/type = "value"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameUnderline:position")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(650, 194), Vector2(582.556, 194), Vector2(527.41, 194), Vector2(483.431, 194), Vector2(449.483, 194), Vector2(424.434, 194), Vector2(407.15, 194), Vector2(396.497, 194), Vector2(391.343, 194), Vector2(390.553, 194), Vector2(392.995, 194), Vector2(397.534, 194), Vector2(403.038, 194), Vector2(408.372, 194), Vector2(412.404, 194), Vector2(414, 194)]
+}
+tracks/2/type = "value"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("CanvasLayer/MainMenu/GameInfo/Author:position")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(648, 199), Vector2(561.123, 199), Vector2(490.088, 199), Vector2(433.436, 199), Vector2(389.707, 199), Vector2(357.44, 199), Vector2(335.176, 199), Vector2(321.454, 199), Vector2(314.814, 199), Vector2(313.797, 199), Vector2(316.942, 199), Vector2(322.79, 199), Vector2(329.879, 199), Vector2(336.751, 199), Vector2(341.944, 199), Vector2(344, 199)]
+}
+tracks/3/type = "value"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("CanvasLayer/MainMenu/Rays:position")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(0, 0), Vector2(32.0074, 0), Vector2(58.1781, 0), Vector2(79.0498, 0), Vector2(95.1607, 0), Vector2(107.048, 0), Vector2(115.251, 0), Vector2(120.307, 0), Vector2(122.753, 0), Vector2(123.127, 0), Vector2(121.969, 0), Vector2(119.814, 0), Vector2(117.202, 0), Vector2(114.671, 0), Vector2(112.757, 0), Vector2(112, 0)]
+}
+tracks/4/type = "value"
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/path = NodePath("CanvasLayer/MainMenu/MenuButtons/PlayButton:position")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/keys = {
+"times": PackedFloat32Array(0.9, 0.933333, 0.966667, 1, 1.03333, 1.06667, 1.1, 1.13333, 1.16667, 1.2),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(-110, 128), Vector2(-39.3658, 128), Vector2(9.80145, 128), Vector2(41.0148, 128), Vector2(57.7873, 128), Vector2(63.6323, 128), Vector2(62.0629, 128), Vector2(56.5923, 128), Vector2(50.7336, 128), Vector2(48, 128)]
+}
+tracks/5/type = "value"
+tracks/5/imported = false
+tracks/5/enabled = true
+tracks/5/path = NodePath("CanvasLayer/MainMenu/MenuButtons/OptionsButton:position")
+tracks/5/interp = 1
+tracks/5/loop_wrap = true
+tracks/5/keys = {
+"times": PackedFloat32Array(1.15, 1.18333, 1.21667, 1.25, 1.28333, 1.31667, 1.35, 1.38333, 1.41667, 1.45),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(-110, 160), Vector2(-32.2129, 160), Vector2(21.9332, 160), Vector2(56.3074, 160), Vector2(74.7784, 160), Vector2(81.2154, 160), Vector2(79.487, 160), Vector2(73.4624, 160), Vector2(67.0104, 160), Vector2(64, 160)]
+}
+tracks/6/type = "value"
+tracks/6/imported = false
+tracks/6/enabled = true
+tracks/6/path = NodePath("CanvasLayer/MainMenu/MenuButtons/CreditsButton:position")
+tracks/6/interp = 1
+tracks/6/loop_wrap = true
+tracks/6/keys = {
+"times": PackedFloat32Array(1.4, 1.43333, 1.46667, 1.5, 1.53333, 1.56667, 1.6, 1.63333, 1.66667, 1.7),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(-110, 192), Vector2(-25.0601, 192), Vector2(34.065, 192), Vector2(71.6, 192), Vector2(91.7696, 192), Vector2(98.7984, 192), Vector2(96.9111, 192), Vector2(90.3325, 192), Vector2(83.2872, 192), Vector2(80, 192)]
+}
+tracks/7/type = "value"
+tracks/7/imported = false
+tracks/7/enabled = true
+tracks/7/path = NodePath("CanvasLayer/MainMenu/MenuButtons/QuitButton:position")
+tracks/7/interp = 1
+tracks/7/loop_wrap = true
+tracks/7/keys = {
+"times": PackedFloat32Array(1.65, 1.68333, 1.71667, 1.75, 1.78333, 1.81667, 1.85, 1.88333, 1.91667, 1.95),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(-110, 224), Vector2(-17.9072, 224), Vector2(46.1968, 224), Vector2(86.8927, 224), Vector2(108.761, 224), Vector2(116.381, 224), Vector2(114.335, 224), Vector2(107.203, 224), Vector2(99.5641, 224), Vector2(96, 224)]
+}
+
+[sub_resource type="Animation" id="Animation_u1e5j"]
+length = 0.001
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameTitle:position")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(135, 168)]
+}
+tracks/1/type = "value"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameUnderline:position")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(650, 194)]
+}
+tracks/2/type = "value"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("CanvasLayer/MainMenu/GameInfo/Author:position")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(648, 199)]
+}
+tracks/3/type = "value"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("CanvasLayer/MainMenu/Rays:position")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(0, 0)]
+}
+tracks/4/type = "value"
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/path = NodePath("CanvasLayer/MainMenu/MenuButtons/PlayButton:position")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(-110, 128)]
+}
+tracks/5/type = "value"
+tracks/5/imported = false
+tracks/5/enabled = true
+tracks/5/path = NodePath("CanvasLayer/MainMenu/MenuButtons/OptionsButton:position")
+tracks/5/interp = 1
+tracks/5/loop_wrap = true
+tracks/5/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(-110, 160)]
+}
+tracks/6/type = "value"
+tracks/6/imported = false
+tracks/6/enabled = true
+tracks/6/path = NodePath("CanvasLayer/MainMenu/MenuButtons/CreditsButton:position")
+tracks/6/interp = 1
+tracks/6/loop_wrap = true
+tracks/6/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(-110, 192)]
+}
+tracks/7/type = "value"
+tracks/7/imported = false
+tracks/7/enabled = true
+tracks/7/path = NodePath("CanvasLayer/MainMenu/MenuButtons/QuitButton:position")
+tracks/7/interp = 1
+tracks/7/loop_wrap = true
+tracks/7/keys = {
+"times": PackedFloat32Array(0),
+"transitions": PackedFloat32Array(1),
+"update": 0,
+"values": [Vector2(-110, 224)]
+}
+
+[sub_resource type="Animation" id="Animation_ob40t"]
+resource_name = "hide_main"
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("CanvasLayer/MainMenu/MenuButtons/PlayButton:position")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(48, 128), Vector2(27.6356, 128), Vector2(8.67555, 128), Vector2(-8.88, 128), Vector2(-25.0311, 128), Vector2(-39.7778, 128), Vector2(-53.12, 128), Vector2(-65.0578, 128), Vector2(-75.5911, 128), Vector2(-84.72, 128), Vector2(-92.4444, 128), Vector2(-98.7645, 128), Vector2(-103.68, 128), Vector2(-107.191, 128), Vector2(-109.298, 128), Vector2(-110, 128)]
+}
+tracks/1/type = "value"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("CanvasLayer/MainMenu/MenuButtons/OptionsButton:position")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(64, 160), Vector2(41.5733, 160), Vector2(20.6933, 160), Vector2(1.36, 160), Vector2(-16.4267, 160), Vector2(-32.6667, 160), Vector2(-47.36, 160), Vector2(-60.5067, 160), Vector2(-72.1067, 160), Vector2(-82.16, 160), Vector2(-90.6667, 160), Vector2(-97.6267, 160), Vector2(-103.04, 160), Vector2(-106.907, 160), Vector2(-109.227, 160), Vector2(-110, 160)]
+}
+tracks/2/type = "value"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("CanvasLayer/MainMenu/MenuButtons/CreditsButton:position")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(80, 192), Vector2(55.5111, 192), Vector2(32.7111, 192), Vector2(11.6, 192), Vector2(-7.82223, 192), Vector2(-25.5556, 192), Vector2(-41.6, 192), Vector2(-55.9556, 192), Vector2(-68.6222, 192), Vector2(-79.6, 192), Vector2(-88.8889, 192), Vector2(-96.4889, 192), Vector2(-102.4, 192), Vector2(-106.622, 192), Vector2(-109.156, 192), Vector2(-110, 192)]
+}
+tracks/3/type = "value"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("CanvasLayer/MainMenu/MenuButtons/QuitButton:position")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(96, 224), Vector2(69.4489, 224), Vector2(44.7289, 224), Vector2(21.84, 224), Vector2(0.782219, 224), Vector2(-18.4445, 224), Vector2(-35.84, 224), Vector2(-51.4044, 224), Vector2(-65.1378, 224), Vector2(-77.04, 224), Vector2(-87.1111, 224), Vector2(-95.3511, 224), Vector2(-101.76, 224), Vector2(-106.338, 224), Vector2(-109.084, 224), Vector2(-110, 224)]
+}
+tracks/4/type = "value"
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameTitle:position")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(311, 168), Vector2(288.316, 168), Vector2(267.196, 168), Vector2(247.64, 168), Vector2(229.649, 168), Vector2(213.222, 168), Vector2(198.36, 168), Vector2(185.062, 168), Vector2(173.329, 168), Vector2(163.16, 168), Vector2(154.556, 168), Vector2(147.516, 168), Vector2(142.04, 168), Vector2(138.129, 168), Vector2(135.782, 168), Vector2(135, 168)]
+}
+tracks/5/type = "value"
+tracks/5/imported = false
+tracks/5/enabled = true
+tracks/5/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameUnderline:position")
+tracks/5/interp = 1
+tracks/5/loop_wrap = true
+tracks/5/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(414, 194), Vector2(458.84, 194), Vector2(498.96, 194), Vector2(534.36, 194), Vector2(565.04, 194), Vector2(591, 194), Vector2(612.24, 194), Vector2(628.76, 194), Vector2(640.56, 194), Vector2(647.64, 194), Vector2(650, 194)]
+}
+tracks/6/type = "value"
+tracks/6/imported = false
+tracks/6/enabled = true
+tracks/6/path = NodePath("CanvasLayer/MainMenu/GameInfo/Author:position")
+tracks/6/interp = 1
+tracks/6/loop_wrap = true
+tracks/6/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(344, 199), Vector2(383.182, 199), Vector2(419.662, 199), Vector2(453.44, 199), Vector2(484.516, 199), Vector2(512.889, 199), Vector2(538.56, 199), Vector2(561.529, 199), Vector2(581.796, 199), Vector2(599.36, 199), Vector2(614.222, 199), Vector2(626.382, 199), Vector2(635.84, 199), Vector2(642.596, 199), Vector2(646.649, 199), Vector2(648, 199)]
+}
+tracks/7/type = "value"
+tracks/7/imported = false
+tracks/7/enabled = true
+tracks/7/path = NodePath("CanvasLayer/MainMenu/Rays:position")
+tracks/7/interp = 1
+tracks/7/loop_wrap = true
+tracks/7/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(112, 0), Vector2(97.5644, 0), Vector2(84.1244, 0), Vector2(71.68, 0), Vector2(60.2311, 0), Vector2(49.7778, 0), Vector2(40.32, 0), Vector2(31.8578, 0), Vector2(24.3911, 0), Vector2(17.92, 0), Vector2(12.4445, 0), Vector2(7.96444, 0), Vector2(4.48, 0), Vector2(1.99111, 0), Vector2(0.49778, 0), Vector2(0, 0)]
+}
+
+[sub_resource type="Animation" id="Animation_2xxnd"]
+resource_name = "show_credits"
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("CanvasLayer/MainMenu/MenuButtons/PlayButton:position")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(48, 128), Vector2(27.6356, 128), Vector2(8.67555, 128), Vector2(-8.88, 128), Vector2(-25.0311, 128), Vector2(-39.7778, 128), Vector2(-53.12, 128), Vector2(-65.0578, 128), Vector2(-75.5911, 128), Vector2(-84.72, 128), Vector2(-92.4444, 128), Vector2(-98.7645, 128), Vector2(-103.68, 128), Vector2(-107.191, 128), Vector2(-109.298, 128), Vector2(-110, 128)]
+}
+tracks/1/type = "value"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("CanvasLayer/MainMenu/MenuButtons/OptionsButton:position")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(64, 160), Vector2(41.5733, 160), Vector2(20.6933, 160), Vector2(1.36, 160), Vector2(-16.4267, 160), Vector2(-32.6667, 160), Vector2(-47.36, 160), Vector2(-60.5067, 160), Vector2(-72.1067, 160), Vector2(-82.16, 160), Vector2(-90.6667, 160), Vector2(-97.6267, 160), Vector2(-103.04, 160), Vector2(-106.907, 160), Vector2(-109.227, 160), Vector2(-110, 160)]
+}
+tracks/2/type = "value"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("CanvasLayer/MainMenu/MenuButtons/CreditsButton:position")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(80, 192), Vector2(55.5111, 192), Vector2(32.7111, 192), Vector2(11.6, 192), Vector2(-7.82223, 192), Vector2(-25.5556, 192), Vector2(-41.6, 192), Vector2(-55.9556, 192), Vector2(-68.6222, 192), Vector2(-79.6, 192), Vector2(-88.8889, 192), Vector2(-96.4889, 192), Vector2(-102.4, 192), Vector2(-106.622, 192), Vector2(-109.156, 192), Vector2(-110, 192)]
+}
+tracks/3/type = "value"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("CanvasLayer/MainMenu/MenuButtons/QuitButton:position")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(96, 224), Vector2(69.4489, 224), Vector2(44.7289, 224), Vector2(21.84, 224), Vector2(0.782219, 224), Vector2(-18.4445, 224), Vector2(-35.84, 224), Vector2(-51.4044, 224), Vector2(-65.1378, 224), Vector2(-77.04, 224), Vector2(-87.1111, 224), Vector2(-95.3511, 224), Vector2(-101.76, 224), Vector2(-106.338, 224), Vector2(-109.084, 224), Vector2(-110, 224)]
+}
+tracks/4/type = "value"
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameTitle:position")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(311, 168), Vector2(354.049, 168), Vector2(394.129, 168), Vector2(431.24, 168), Vector2(465.382, 168), Vector2(496.556, 168), Vector2(524.76, 168), Vector2(549.996, 168), Vector2(572.262, 168), Vector2(591.56, 168), Vector2(607.889, 168), Vector2(621.249, 168), Vector2(631.64, 168), Vector2(639.062, 168), Vector2(643.516, 168), Vector2(645, 168)]
+}
+tracks/5/type = "value"
+tracks/5/imported = false
+tracks/5/enabled = true
+tracks/5/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameUnderline:position")
+tracks/5/interp = 1
+tracks/5/loop_wrap = true
+tracks/5/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(414, 194), Vector2(458.84, 194), Vector2(498.96, 194), Vector2(534.36, 194), Vector2(565.04, 194), Vector2(591, 194), Vector2(612.24, 194), Vector2(628.76, 194), Vector2(640.56, 194), Vector2(647.64, 194), Vector2(650, 194)]
+}
+tracks/6/type = "value"
+tracks/6/imported = false
+tracks/6/enabled = true
+tracks/6/path = NodePath("CanvasLayer/MainMenu/GameInfo/Author:position")
+tracks/6/interp = 1
+tracks/6/loop_wrap = true
+tracks/6/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(344, 199), Vector2(383.182, 199), Vector2(419.662, 199), Vector2(453.44, 199), Vector2(484.516, 199), Vector2(512.889, 199), Vector2(538.56, 199), Vector2(561.529, 199), Vector2(581.796, 199), Vector2(599.36, 199), Vector2(614.222, 199), Vector2(626.382, 199), Vector2(635.84, 199), Vector2(642.596, 199), Vector2(646.649, 199), Vector2(648, 199)]
+}
+tracks/7/type = "value"
+tracks/7/imported = false
+tracks/7/enabled = true
+tracks/7/path = NodePath("CanvasLayer/MainMenu/Rays:position")
+tracks/7/interp = 1
+tracks/7/loop_wrap = true
+tracks/7/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(112, 0), Vector2(97.5644, 0), Vector2(84.1244, 0), Vector2(71.68, 0), Vector2(60.2311, 0), Vector2(49.7778, 0), Vector2(40.32, 0), Vector2(31.8578, 0), Vector2(24.3911, 0), Vector2(17.92, 0), Vector2(12.4445, 0), Vector2(7.96444, 0), Vector2(4.48, 0), Vector2(1.99111, 0), Vector2(0.49778, 0), Vector2(0, 0)]
+}
+
+[sub_resource type="Animation" id="Animation_awvye"]
+resource_name = "hide_credits"
+tracks/0/type = "value"
+tracks/0/imported = false
+tracks/0/enabled = true
+tracks/0/path = NodePath("CanvasLayer/MainMenu/MenuButtons/PlayButton:position")
+tracks/0/interp = 1
+tracks/0/loop_wrap = true
+tracks/0/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(-110, 128), Vector2(-89.6356, 128), Vector2(-70.6756, 128), Vector2(-53.12, 128), Vector2(-36.9689, 128), Vector2(-22.2222, 128), Vector2(-8.87999, 128), Vector2(3.05778, 128), Vector2(13.5911, 128), Vector2(22.72, 128), Vector2(30.4444, 128), Vector2(36.7645, 128), Vector2(41.68, 128), Vector2(45.1911, 128), Vector2(47.2978, 128), Vector2(48, 128)]
+}
+tracks/1/type = "value"
+tracks/1/imported = false
+tracks/1/enabled = true
+tracks/1/path = NodePath("CanvasLayer/MainMenu/MenuButtons/OptionsButton:position")
+tracks/1/interp = 1
+tracks/1/loop_wrap = true
+tracks/1/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(-110, 160), Vector2(-87.5733, 160), Vector2(-66.6933, 160), Vector2(-47.36, 160), Vector2(-29.5733, 160), Vector2(-13.3333, 160), Vector2(1.36001, 160), Vector2(14.5067, 160), Vector2(26.1067, 160), Vector2(36.16, 160), Vector2(44.6667, 160), Vector2(51.6267, 160), Vector2(57.04, 160), Vector2(60.9067, 160), Vector2(63.2267, 160), Vector2(64, 160)]
+}
+tracks/2/type = "value"
+tracks/2/imported = false
+tracks/2/enabled = true
+tracks/2/path = NodePath("CanvasLayer/MainMenu/MenuButtons/CreditsButton:position")
+tracks/2/interp = 1
+tracks/2/loop_wrap = true
+tracks/2/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(-110, 192), Vector2(-85.5111, 192), Vector2(-62.7111, 192), Vector2(-41.6, 192), Vector2(-22.1778, 192), Vector2(-4.44444, 192), Vector2(11.6, 192), Vector2(25.9556, 192), Vector2(38.6222, 192), Vector2(49.6, 192), Vector2(58.8889, 192), Vector2(66.4889, 192), Vector2(72.4, 192), Vector2(76.6222, 192), Vector2(79.1555, 192), Vector2(80, 192)]
+}
+tracks/3/type = "value"
+tracks/3/imported = false
+tracks/3/enabled = true
+tracks/3/path = NodePath("CanvasLayer/MainMenu/MenuButtons/QuitButton:position")
+tracks/3/interp = 1
+tracks/3/loop_wrap = true
+tracks/3/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(-110, 224), Vector2(-83.4489, 224), Vector2(-58.7289, 224), Vector2(-35.84, 224), Vector2(-14.7822, 224), Vector2(4.44445, 224), Vector2(21.84, 224), Vector2(37.4044, 224), Vector2(51.1378, 224), Vector2(63.04, 224), Vector2(73.1111, 224), Vector2(81.3511, 224), Vector2(87.76, 224), Vector2(92.3378, 224), Vector2(95.0844, 224), Vector2(96, 224)]
+}
+tracks/4/type = "value"
+tracks/4/imported = false
+tracks/4/enabled = true
+tracks/4/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameTitle:position")
+tracks/4/interp = 1
+tracks/4/loop_wrap = true
+tracks/4/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(645, 168), Vector2(601.951, 168), Vector2(561.871, 168), Vector2(524.76, 168), Vector2(490.618, 168), Vector2(459.444, 168), Vector2(431.24, 168), Vector2(406.004, 168), Vector2(383.738, 168), Vector2(364.44, 168), Vector2(348.111, 168), Vector2(334.751, 168), Vector2(324.36, 168), Vector2(316.938, 168), Vector2(312.484, 168), Vector2(311, 168)]
+}
+tracks/5/type = "value"
+tracks/5/imported = false
+tracks/5/enabled = true
+tracks/5/path = NodePath("CanvasLayer/MainMenu/GameInfo/GameUnderline:position")
+tracks/5/interp = 1
+tracks/5/loop_wrap = true
+tracks/5/keys = {
+"times": PackedFloat32Array(0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(650, 194), Vector2(615.089, 194), Vector2(582.97, 194), Vector2(553.645, 194), Vector2(527.112, 194), Vector2(503.373, 194), Vector2(482.426, 194), Vector2(464.272, 194), Vector2(448.911, 194), Vector2(436.343, 194), Vector2(426.568, 194), Vector2(419.586, 194), Vector2(415.396, 194), Vector2(414, 194)]
+}
+tracks/6/type = "value"
+tracks/6/imported = false
+tracks/6/enabled = true
+tracks/6/path = NodePath("CanvasLayer/MainMenu/GameInfo/Author:position")
+tracks/6/interp = 1
+tracks/6/loop_wrap = true
+tracks/6/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(648, 199), Vector2(608.818, 199), Vector2(572.338, 199), Vector2(538.56, 199), Vector2(507.484, 199), Vector2(479.111, 199), Vector2(453.44, 199), Vector2(430.471, 199), Vector2(410.204, 199), Vector2(392.64, 199), Vector2(377.778, 199), Vector2(365.618, 199), Vector2(356.16, 199), Vector2(349.404, 199), Vector2(345.351, 199), Vector2(344, 199)]
+}
+tracks/7/type = "value"
+tracks/7/imported = false
+tracks/7/enabled = true
+tracks/7/path = NodePath("CanvasLayer/MainMenu/Rays:position")
+tracks/7/interp = 1
+tracks/7/loop_wrap = true
+tracks/7/keys = {
+"times": PackedFloat32Array(0, 0.0333333, 0.0666667, 0.1, 0.133333, 0.166667, 0.2, 0.233333, 0.266667, 0.3, 0.333333, 0.366667, 0.4, 0.433333, 0.466667, 0.5),
+"transitions": PackedFloat32Array(1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1),
+"update": 0,
+"values": [Vector2(0, 0), Vector2(14.4356, 0), Vector2(27.8756, 0), Vector2(40.32, 0), Vector2(51.7689, 0), Vector2(62.2222, 0), Vector2(71.68, 0), Vector2(80.1422, 0), Vector2(87.6089, 0), Vector2(94.08, 0), Vector2(99.5555, 0), Vector2(104.036, 0), Vector2(107.52, 0), Vector2(110.009, 0), Vector2(111.502, 0), Vector2(112, 0)]
+}
+
+[sub_resource type="AnimationLibrary" id="AnimationLibrary_twm3r"]
+_data = {
+"RESET": SubResource("Animation_u1e5j"),
+"hide_credits": SubResource("Animation_awvye"),
+"hide_main": SubResource("Animation_ob40t"),
+"show_credits": SubResource("Animation_2xxnd"),
+"show_main": SubResource("Animation_5jiy0")
+}
+
+[node name="Menu" type="Node"]
+script = ExtResource("1_f186y")
+
+[node name="CanvasLayer" type="CanvasLayer" parent="."]
+
+[node name="MainMenu" type="Control" parent="CanvasLayer"]
+layout_mode = 3
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+theme = ExtResource("1_fowhs")
+
+[node name="Rays" type="ColorRect" parent="CanvasLayer/MainMenu"]
+z_index = 100
+material = SubResource("ShaderMaterial_ygqow")
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="AnimationPlayer" type="AnimationPlayer" parent="CanvasLayer/MainMenu"]
+root_node = NodePath("../../..")
+libraries = {
+"": SubResource("AnimationLibrary_twm3r")
+}
+
+[node name="GameInfo" type="Control" parent="CanvasLayer/MainMenu"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="GameTitle" type="RichTextLabel" parent="CanvasLayer/MainMenu/GameInfo"]
+z_index = 200
+layout_mode = 1
+anchors_preset = 8
+anchor_left = 0.5
+anchor_top = 0.5
+anchor_right = 0.5
+anchor_bottom = 0.5
+offset_left = -185.0
+offset_top = -12.0
+offset_right = 183.0
+offset_bottom = 28.0
+grow_horizontal = 2
+grow_vertical = 2
+theme_override_font_sizes/normal_font_size = 24
+bbcode_enabled = true
+text = "[center]Game Title"
+script = ExtResource("3_727p5")
+key = "name"
+
+[node name="GameUnderline" type="ColorRect" parent="CanvasLayer/MainMenu/GameInfo"]
+z_index = 200
+layout_mode = 1
+anchors_preset = 6
+anchor_left = 1.0
+anchor_top = 0.5
+anchor_right = 1.0
+anchor_bottom = 0.5
+offset_left = 10.0
+offset_top = 14.0
+offset_right = 330.525
+offset_bottom = 16.0
+grow_horizontal = 0
+grow_vertical = 2
+
+[node name="Author" type="RichTextLabel" parent="CanvasLayer/MainMenu/GameInfo"]
+z_index = 200
+layout_mode = 1
+anchors_preset = 6
+anchor_left = 1.0
+anchor_top = 0.5
+anchor_right = 1.0
+anchor_bottom = 0.5
+offset_left = 8.0
+offset_top = 19.0
+offset_right = 296.0
+offset_bottom = 59.0
+grow_horizontal = 0
+grow_vertical = 2
+bbcode_enabled = true
+text = "[right]by Team Auboreal"
+script = ExtResource("3_727p5")
+key = "author"
+alignment = "right"
+
+[node name="MenuButtons" type="Control" parent="CanvasLayer/MainMenu"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+
+[node name="PlayButton" parent="CanvasLayer/MainMenu/MenuButtons" instance=ExtResource("5_goj86")]
+layout_mode = 0
+anchors_preset = 0
+anchor_top = 0.0
+anchor_bottom = 0.0
+offset_left = -110.0
+offset_top = 128.0
+offset_right = -19.13
+offset_bottom = 152.515
+grow_vertical = 1
+
+[node name="OptionsButton" parent="CanvasLayer/MainMenu/MenuButtons" instance=ExtResource("5_goj86")]
+layout_mode = 0
+anchors_preset = 0
+anchor_top = 0.0
+anchor_bottom = 0.0
+offset_left = -110.0
+offset_top = 160.0
+offset_right = -19.13
+offset_bottom = 184.515
+grow_vertical = 1
+text = "Options"
+
+[node name="CreditsButton" parent="CanvasLayer/MainMenu/MenuButtons" instance=ExtResource("5_goj86")]
+layout_mode = 0
+anchors_preset = 0
+anchor_top = 0.0
+anchor_bottom = 0.0
+offset_left = -110.0
+offset_top = 192.0
+offset_right = -19.13
+offset_bottom = 216.515
+grow_vertical = 1
+text = "Credits"
+
+[node name="QuitButton" parent="CanvasLayer/MainMenu/MenuButtons" instance=ExtResource("5_goj86")]
+layout_mode = 0
+anchors_preset = 0
+anchor_top = 0.0
+anchor_bottom = 0.0
+offset_left = -110.0
+offset_top = 224.0
+offset_right = -19.13
+offset_bottom = 248.515
+grow_vertical = 1
+text = "Quit"
+
+[node name="Credits" type="Control" parent="CanvasLayer/MainMenu"]
+layout_mode = 1
+anchors_preset = 15
+anchor_right = 1.0
+anchor_bottom = 1.0
+grow_horizontal = 2
+grow_vertical = 2
+script = ExtResource("6_b724h")
+
+[connection signal="clicked" from="CanvasLayer/MainMenu/MenuButtons/PlayButton" to="." method="_on_play_button_clicked"]
+[connection signal="clicked" from="CanvasLayer/MainMenu/MenuButtons/OptionsButton" to="." method="_on_options_button_clicked"]
+[connection signal="clicked" from="CanvasLayer/MainMenu/MenuButtons/CreditsButton" to="." method="_on_credits_button_clicked"]
+[connection signal="clicked" from="CanvasLayer/MainMenu/MenuButtons/QuitButton" to="." method="_on_quit_button_clicked"]
diff --git a/components/Menu/MenuButton.tscn b/components/Menu/MenuButton.tscn
new file mode 100644
index 0000000..a866a05
--- /dev/null
+++ b/components/Menu/MenuButton.tscn
@@ -0,0 +1,45 @@
+[gd_scene load_steps=5 format=3 uid="uid://cmyjaeahyipq4"]
+
+[ext_resource type="Script" path="res://components/Menu/menu_button.gd" id="1_kxfx8"]
+[ext_resource type="PackedScene" uid="uid://dykc1mgg5uopw" path="res://components/Cursor/MouseHandler.tscn" id="2_g6apf"]
+[ext_resource type="Script" path="res://components/Menu/background_highlight.gd" id="3_cxaw3"]
+
+[sub_resource type="RectangleShape2D" id="RectangleShape2D_oahx4"]
+size = Vector2(147, 22)
+
+[node name="MenuButton" type="RichTextLabel"]
+clip_contents = false
+anchors_preset = 4
+anchor_top = 0.5
+anchor_bottom = 0.5
+offset_left = 48.0
+offset_top = -52.0
+offset_right = 138.87
+offset_bottom = -27.485
+grow_vertical = 2
+theme_override_font_sizes/normal_font_size = 16
+bbcode_enabled = true
+text = "Play"
+script = ExtResource("1_kxfx8")
+
+[node name="MouseHandler" parent="." instance=ExtResource("2_g6apf")]
+
+[node name="CollisionShape2D" type="CollisionShape2D" parent="MouseHandler"]
+position = Vector2(36.5, 7)
+shape = SubResource("RectangleShape2D_oahx4")
+
+[node name="BackgroundHighlight" type="ColorRect" parent="."]
+show_behind_parent = true
+layout_mode = 0
+offset_left = -420.0
+offset_top = -8.0
+offset_right = -142.0
+offset_bottom = 22.0
+color = Color(0, 0, 0, 1)
+script = ExtResource("3_cxaw3")
+
+[connection signal="clicked" from="MouseHandler" to="." method="_on_play_mouse_handler_clicked"]
+[connection signal="hovered" from="MouseHandler" to="." method="_on_mouse_handler_hovered"]
+[connection signal="hovered" from="MouseHandler" to="BackgroundHighlight" method="_on_mouse_handler_hovered"]
+[connection signal="unhovered" from="MouseHandler" to="." method="_on_mouse_handler_unhovered"]
+[connection signal="unhovered" from="MouseHandler" to="BackgroundHighlight" method="_on_mouse_handler_unhovered"]
diff --git a/components/Menu/background_highlight.gd b/components/Menu/background_highlight.gd
new file mode 100644
index 0000000..388900b
--- /dev/null
+++ b/components/Menu/background_highlight.gd
@@ -0,0 +1,35 @@
+extends ColorRect
+
+var tween
+
+func _on_mouse_handler_hovered() -> void:
+ if tween:
+ tween.kill()
+
+ tween = create_tween()
+ tween.set_ease(Tween.EASE_OUT)
+ tween.set_trans(Tween.TRANS_BACK)
+ tween.tween_property(self, "position:x", -136, 0.5)
+ tween.set_ease(Tween.EASE_IN_OUT)
+ tween.set_trans(Tween.TRANS_QUAD)
+ tween.tween_property(self, "position:x", -128, 0.25)
+ tween.set_ease(Tween.EASE_IN_OUT)
+ tween.tween_property(self, "position:x", -132, 0.25)
+ tween.tween_callback(func():
+ tween = create_tween()
+ tween.set_loops()
+ tween.set_ease(Tween.EASE_IN_OUT)
+ tween.set_trans(Tween.TRANS_QUAD)
+ tween.tween_property(self, "position:x", -136, 1)
+ tween.tween_property(self, "position:x", -132, 1)
+ )
+
+
+func _on_mouse_handler_unhovered() -> void:
+ if tween:
+ tween.kill()
+
+ tween = create_tween()
+ tween.set_ease(Tween.EASE_OUT)
+ tween.set_trans(Tween.TRANS_BACK)
+ tween.tween_property(self, "position:x", -420, 0.5)
diff --git a/components/Menu/credits.gd b/components/Menu/credits.gd
new file mode 100644
index 0000000..77bb034
--- /dev/null
+++ b/components/Menu/credits.gd
@@ -0,0 +1,69 @@
+extends Control
+
+var _data
+
+const TEXT_PADDING = 30
+
+var items = []
+var tween
+
+func _ready() -> void:
+ if get_tree().root.has_node("Data"):
+ _data = get_tree().root.get_node("Data")
+
+ if _data.data.has("metadata") and _data.data.metadata.has("credits"):
+ var credits = _data.data.metadata.credits
+ var sections = credits.keys()
+ var y = 50
+
+ for section in sections:
+ var section_text = RichTextLabel.new()
+ section_text.position = Vector2(-120, y)
+ section_text.size = Vector2(40, TEXT_PADDING)
+ section_text.bbcode_enabled = true
+ section_text.text = "[center]%s" % [section]
+ add_child(section_text)
+ items.push_back(section_text)
+ y += TEXT_PADDING
+ var people = credits[section].keys()
+
+ for person in people:
+ var person_text = RichTextLabel.new()
+ person_text.position = Vector2(-120, y)
+ person_text.size = Vector2(40, TEXT_PADDING)
+ person_text.bbcode_enabled = true
+ person_text.text = "[center]%s" % [person]
+ add_child(person_text)
+ items.push_back(person_text)
+ y += TEXT_PADDING
+
+ var links = credits[section][person].keys()
+
+ for link in links:
+ var url = credits[section][person][link]
+
+ y += TEXT_PADDING
+
+
+func show_credits():
+ if tween:
+ tween.kill()
+
+ tween = create_tween()
+ tween.set_ease(Tween.EASE_OUT)
+ tween.set_trans(Tween.TRANS_BACK)
+
+ for item in items:
+ tween.tween_property(item, "position:x", 320, 0.25)
+
+func hide_credits():
+ if tween:
+ tween.kill()
+
+ tween = create_tween()
+ tween.set_parallel()
+ tween.set_ease(Tween.EASE_OUT)
+ tween.set_trans(Tween.TRANS_QUAD)
+
+ for item in items:
+ tween.tween_property(item, "position:x", -120, 0.25)
diff --git a/components/Menu/god_rays.gdshader b/components/Menu/god_rays.gdshader
new file mode 100644
index 0000000..b94552c
--- /dev/null
+++ b/components/Menu/god_rays.gdshader
@@ -0,0 +1,109 @@
+/*
+Shader from Godot Shaders - the free shader library.
+godotshaders.com/shader/god-rays
+
+Feel free to use, improve and change this shader according to your needs
+and consider sharing the modified result on godotshaders.com.
+*/
+
+shader_type canvas_item;
+
+uniform float angle = -0.3;
+uniform float position = -0.2;
+uniform float spread : hint_range(0.0, 1.0) = 0.5;
+uniform float cutoff : hint_range(-1.0, 1.0) = 0.1;
+uniform float falloff : hint_range(0.0, 1.0) = 0.2;
+uniform float edge_fade : hint_range(0.0, 1.0) = 0.15;
+
+uniform float speed = 1.0;
+uniform float ray1_density = 8.0;
+uniform float ray2_density = 30.0;
+uniform float ray2_intensity : hint_range(0.0, 1.0) = 0.3;
+
+uniform vec4 color = vec4(1.0, 0.9, 0.65, 0.8);
+
+uniform bool hdr = false;
+uniform float seed = 5.0;
+
+uniform sampler2D SCREEN_TEXTURE : hint_screen_texture, filter_linear_mipmap;
+
+// Random and noise functions from Book of Shader's chapter on Noise.
+float random(vec2 _uv) {
+ return fract(sin(dot(_uv.xy,
+ vec2(12.9898, 78.233))) *
+ 43758.5453123);
+}
+
+float noise (in vec2 uv) {
+ vec2 i = floor(uv);
+ vec2 f = fract(uv);
+
+ // Four corners in 2D of a tile
+ float a = random(i);
+ float b = random(i + vec2(1.0, 0.0));
+ float c = random(i + vec2(0.0, 1.0));
+ float d = random(i + vec2(1.0, 1.0));
+
+
+ // Smooth Interpolation
+
+ // Cubic Hermine Curve. Same as SmoothStep()
+ vec2 u = f * f * (3.0-2.0 * f);
+
+ // Mix 4 coorners percentages
+ return mix(a, b, u.x) +
+ (c - a)* u.y * (1.0 - u.x) +
+ (d - b) * u.x * u.y;
+}
+
+mat2 rotate(float _angle){
+ return mat2(vec2(cos(_angle), -sin(_angle)),
+ vec2(sin(_angle), cos(_angle)));
+}
+
+vec4 screen(vec4 base, vec4 blend){
+ return 1.0 - (1.0 - base) * (1.0 - blend);
+}
+
+void fragment()
+{
+
+ // Rotate, skew and move the UVs
+ vec2 transformed_uv = ( rotate(angle) * (UV - position) ) / ( (UV.y + spread) - (UV.y * spread) );
+
+ // Animate the ray according the the new transformed UVs
+ vec2 ray1 = vec2(transformed_uv.x * ray1_density + sin(TIME * 0.1 * speed) * (ray1_density * 0.2) + seed, 1.0);
+ vec2 ray2 = vec2(transformed_uv.x * ray2_density + sin(TIME * 0.2 * speed) * (ray1_density * 0.2) + seed, 1.0);
+
+ // Cut off the ray's edges
+ float cut = step(cutoff, transformed_uv.x) * step(cutoff, 1.0 - transformed_uv.x);
+ ray1 *= cut;
+ ray2 *= cut;
+
+ // Apply the noise pattern (i.e. create the rays)
+ float rays;
+
+ if (hdr){
+ // This is not really HDR, but check this to not clamp the two merged rays making
+ // their values go over 1.0. Can make for some nice effect
+ rays = noise(ray1) + (noise(ray2) * ray2_intensity);
+ }
+ else{
+ rays = clamp(noise(ray1) + (noise(ray2) * ray2_intensity), 0., 1.);
+ }
+
+ // Fade out edges
+ rays *= smoothstep(0.0, falloff, (1.0 - UV.y)); // Bottom
+ rays *= smoothstep(0.0 + cutoff, edge_fade + cutoff, transformed_uv.x); // Left
+ rays *= smoothstep(0.0 + cutoff, edge_fade + cutoff, 1.0 - transformed_uv.x); // Right
+
+ // Color to the rays
+ vec3 shine = vec3(rays) * color.rgb;
+
+ // Try different blending modes for a nicer effect. "Screen" is included in the code,
+ // but take a look at https://godotshaders.com/snippet/blending-modes/ for more.
+ // With "Screen" blend mode:
+ shine = screen(texture(SCREEN_TEXTURE, SCREEN_UV), vec4(color)).rgb;
+
+ COLOR = vec4(shine, rays * color.a);
+}
\ No newline at end of file
diff --git a/components/Menu/menu.gd b/components/Menu/menu.gd
new file mode 100644
index 0000000..dbc7bac
--- /dev/null
+++ b/components/Menu/menu.gd
@@ -0,0 +1,41 @@
+extends Base
+
+@onready var animation_player: AnimationPlayer = $CanvasLayer/MainMenu/AnimationPlayer
+@onready var credits: Control = $CanvasLayer/MainMenu/Credits
+
+var menu_state = "start"
+
+func _process(delta: float) -> void:
+ if menu_state == "start" and Input.is_action_just_pressed("left_click"):
+ menu_state = "main"
+ animation_player.play("show_main")
+ if Input.is_action_just_pressed("escape"):
+ match menu_state:
+ "main":
+ menu_state = "start"
+ animation_player.play("hide_main")
+ "credits":
+ menu_state = "main"
+ animation_player.play("hide_credits")
+ credits.hide_credits()
+
+
+func _on_play_button_clicked() -> void:
+ if _data:
+ get_tree().change_scene_to_packed(load(_data.data.metadata.start_scene))
+ else:
+ _error("No start scene defined")
+
+
+func _on_options_button_clicked() -> void:
+ pass # Replace with function body.
+
+
+func _on_credits_button_clicked() -> void:
+ menu_state = "credits"
+ animation_player.play("show_credits")
+ credits.show_credits()
+
+
+func _on_quit_button_clicked() -> void:
+ get_tree().quit()
diff --git a/components/Menu/menu_button.gd b/components/Menu/menu_button.gd
new file mode 100644
index 0000000..67a0437
--- /dev/null
+++ b/components/Menu/menu_button.gd
@@ -0,0 +1,24 @@
+extends RichTextLabel
+
+signal clicked
+
+var color_tween
+
+func _on_play_mouse_handler_clicked() -> void:
+ clicked.emit()
+
+
+func _on_mouse_handler_hovered() -> void:
+ if color_tween:
+ color_tween.kill()
+
+ color_tween = create_tween()
+ color_tween.tween_property(self, "self_modulate", Color.GOLD, 0.25)
+
+
+func _on_mouse_handler_unhovered() -> void:
+ if color_tween:
+ color_tween.kill()
+
+ color_tween = create_tween()
+ color_tween.tween_property(self, "self_modulate", Color.WHITE, 0.25)
diff --git a/components/Menu/populate_from_metadata.gd b/components/Menu/populate_from_metadata.gd
new file mode 100644
index 0000000..b9ce4e7
--- /dev/null
+++ b/components/Menu/populate_from_metadata.gd
@@ -0,0 +1,13 @@
+extends RichTextLabel
+
+@export var key := ""
+@export var alignment := "center"
+
+var _data
+
+func _ready() -> void:
+ if get_tree().root.has_node("Data"):
+ _data = get_tree().root.get_node("Data")
+
+ if _data.data.has("metadata"):
+ text = "[%s]%s" % [alignment, _data.data.metadata[key]]
diff --git a/components/Menu/square-menu.svg b/components/Menu/square-menu.svg
new file mode 100644
index 0000000..b52cd87
--- /dev/null
+++ b/components/Menu/square-menu.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Menu/square-menu.svg.import b/components/Menu/square-menu.svg.import
new file mode 100644
index 0000000..6a7b3ee
--- /dev/null
+++ b/components/Menu/square-menu.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cp8tbups5d6p7"
+path="res://.godot/imported/square-menu.svg-7299523024a845ac9eafa86412b4d5c3.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Menu/square-menu.svg"
+dest_files=["res://.godot/imported/square-menu.svg-7299523024a845ac9eafa86412b4d5c3.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Persister/Persister.tscn b/components/Persister/Persister.tscn
new file mode 100644
index 0000000..8bf81ad
--- /dev/null
+++ b/components/Persister/Persister.tscn
@@ -0,0 +1,12 @@
+[gd_scene load_steps=2 format=3 uid="uid://pht0rn54n3t8"]
+
+[ext_resource type="Script" path="res://components/Persister/persister.gd" id="1_taxaa"]
+
+[node name="Persister" type="Node"]
+script = ExtResource("1_taxaa")
+
+[node name="Timer" type="Timer" parent="."]
+wait_time = 20.0
+autostart = true
+
+[connection signal="timeout" from="Timer" to="." method="_on_timer_timeout"]
diff --git a/components/Persister/data_scope.gd b/components/Persister/data_scope.gd
new file mode 100644
index 0000000..e71d732
--- /dev/null
+++ b/components/Persister/data_scope.gd
@@ -0,0 +1,11 @@
+class_name PersisterEnums
+
+enum Scope {
+ PERMANENT, # Stays forever
+ SAVE, # Persists through a save
+ GAME, # Persists through runs
+ RUN, # Persists through an entire run of the game
+ ROUND, # Persists through a round (if applicable, e.g. between ifa round is 2 mins between shops)
+ ROOM, # Persists within a game room (if applicable)
+ UNKNOWN
+}
diff --git a/components/Persister/hard-drive.svg b/components/Persister/hard-drive.svg
new file mode 100644
index 0000000..02da606
--- /dev/null
+++ b/components/Persister/hard-drive.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Persister/hard-drive.svg.import b/components/Persister/hard-drive.svg.import
new file mode 100644
index 0000000..9549ea2
--- /dev/null
+++ b/components/Persister/hard-drive.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://bru35jf108ajj"
+path="res://.godot/imported/hard-drive.svg-f9677d2ad3a8226891a78779d376d996.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Persister/hard-drive.svg"
+dest_files=["res://.godot/imported/hard-drive.svg-f9677d2ad3a8226891a78779d376d996.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Persister/information.txt b/components/Persister/information.txt
new file mode 100644
index 0000000..c004363
--- /dev/null
+++ b/components/Persister/information.txt
@@ -0,0 +1,10 @@
+name: Persister
+short: Data persister to persist data through locations
+description: A data persister that is used to persist data through the game,
+ run, round, or room. A function is called to set data with a key in one
+ of the categories and then other functions can be called to clear
+ the data for a key or all data in a category.
+accent: #a1a166
+log_category: PER
+icon: res://components/Persister/hard-drive.svg
+version: v1.0.0
diff --git a/components/Persister/persister-toolbar.gd b/components/Persister/persister-toolbar.gd
new file mode 100644
index 0000000..752ce0c
--- /dev/null
+++ b/components/Persister/persister-toolbar.gd
@@ -0,0 +1,5 @@
+func save_one(parent):
+ parent.change_save(1)
+
+func save_two(parent):
+ parent.change_save(2)
diff --git a/components/Persister/persister.gd b/components/Persister/persister.gd
new file mode 100644
index 0000000..706d642
--- /dev/null
+++ b/components/Persister/persister.gd
@@ -0,0 +1,334 @@
+@icon("res://components/Persister/hard-drive.svg")
+extends Base
+## Data persister to persist data through locations
+##
+## A data persister that is used to persist data through the game, run, round,
+## or room. A function is called to set data with a key in one of the categories
+## and then other functions can be called to clear the data for a key or all
+## data in a category.
+
+signal data_persisted(key: String, value, category: PersisterEnums.Scope)
+
+var _persisted = {}
+
+
+func _spawned():
+ if _triggerer:
+ _info("Connecting to data scope triggers")
+ _triggerer.listen("game", _on_game_triggered)
+ _triggerer.listen("run", _on_run_triggered)
+ _triggerer.listen("round", _on_round_triggered)
+ _triggerer.listen("room", _on_room_triggered)
+ _load()
+
+
+## Store data in a category
+func persist_data(key: String, value, category := PersisterEnums.Scope.RUN) -> void:
+ # Only allow ints, bools or strings to be persisted and as Strings
+ if value is int:
+ value = str(value)
+
+ if value is bool:
+ value = str(value)
+
+ if not value is String:
+ _warn("Attempted to persist data for key <%s> with an invalid data type |%s|" % [key, type_string(typeof(value))])
+ return
+
+ # Create category if does not exist
+ if not _persisted.has(category):
+ _info("Created new persister category {%s}" % [_get_category_name(category)])
+ _persisted[category] = {}
+
+ # If key is already set to value exit early
+ if _persisted[category].has(key) and _persisted[category][key] == value:
+ return
+
+ _info("Set key <%s> in category {%s} to value |%s|" % [key, _get_category_name(category), value])
+ _persisted[category][key] = value
+
+ _emit_change(key, value, category)
+
+
+## Get the value associated with a key in the highest priority or specified category
+func get_value(key: String, category: PersisterEnums.Scope = PersisterEnums.Scope.UNKNOWN):
+ if category == PersisterEnums.Scope.UNKNOWN:
+ category = _get_key_category(key)
+
+ if category == PersisterEnums.Scope.UNKNOWN:
+ return null
+
+ if _persisted[category][key].is_valid_int():
+ return int(_persisted[category][key])
+
+ if _persisted[category][key] == "true" || _persisted[category][key] == "false":
+ return _persisted[category][key] == "true"
+
+ return _persisted[category][key]
+
+
+## Delete data associated with a key from a certain category
+func clear_data(key: String, category := PersisterEnums.Scope.RUN) -> void:
+ if not _persisted.has(category):
+ _info("Attempted to clear key <%s> in category {%s} but the category did not exist" % [key, _get_category_name(category)])
+ return
+
+ if not _persisted[category].has(key):
+ _info("Attempted to clear key <%s> in category {%s} but it did not exist" % [key, _get_category_name(category)])
+ return
+
+ _info("Cleared key <%s> in category {%s}" % [key, _get_category_name(category)])
+ _persisted[category].erase(key)
+
+ _emit_removal(key, category)
+
+
+## Delete a category including all data within it
+func clear_category(category := PersisterEnums.Scope.RUN) -> void:
+ if not _persisted.has(category):
+ _error("Attempted to clear category {%s} but it did not exist" % [_get_category_name(category)])
+ return
+
+ var keys = _persisted[category].keys()
+
+ _info("Cleared category {%s}" % [_get_category_name(category)])
+ _persisted.erase(category)
+
+ for key in keys:
+ _emit_removal(key, category)
+
+
+## Add a number to a number value with the given key from a certain category
+func change_value(key: String, value: int, category := PersisterEnums.Scope.RUN) -> void:
+ if not _persisted.has(category):
+ _persisted[category] = {}
+
+ if not _persisted[category].has(key):
+ persist_data(key, value, category)
+ return
+
+ if is_nan(int(_persisted[category][key])):
+ _error("Attempted to add number |%d| to key <%s> in category {%s} that is not a number (value: |%s|)" % [value, key, category, _persisted[category][key]])
+ return
+
+ var old_value = int(_persisted[category][key])
+ _info("Added number |%d| to key <%s> in category {%s} (old: |%d|) (new: |%d|)" % [value, key, _get_category_name(category), old_value, old_value + value])
+ persist_data(key, value + old_value, category)
+
+
+func change_save(index: int):
+ _save()
+ persist_data("save", index)
+ _load()
+
+
+func save():
+ _save()
+
+
+func _emit_change(key: String, value: String, category: PersisterEnums.Scope) -> void:
+ var key_category = _get_key_category(key)
+
+ # Only emit change if the category set has the highest priority
+ if key_category == category:
+ if is_nan(int(value)):
+ data_persisted.emit(key, value, category)
+ else:
+ data_persisted.emit(key, int(value), category)
+
+
+func _emit_removal(key: String, category: PersisterEnums.Scope) -> void:
+ var key_category = _get_key_category(key)
+
+ if key_category == PersisterEnums.Scope.UNKNOWN:
+ return
+
+ var category_priority = _get_category_priority(category)
+ var key_category_priority = _get_category_priority(key_category)
+
+ if category_priority > key_category_priority:
+ var value = _persister[key_category][key]
+
+ if is_nan(int(value)):
+ data_persisted.emit(key, value, key_category)
+ else:
+ data_persisted.emit(key, int(value), key_category)
+
+
+func _get_category_name(category: PersisterEnums.Scope) -> String:
+ match category:
+ PersisterEnums.Scope.PERMANENT:
+ return "permanent"
+ PersisterEnums.Scope.SAVE:
+ return "save"
+ PersisterEnums.Scope.GAME:
+ return "game"
+ PersisterEnums.Scope.RUN:
+ return "run"
+ PersisterEnums.Scope.ROUND:
+ return "round"
+ PersisterEnums.Scope.ROOM:
+ return "room"
+ _:
+ return "unknown"
+
+
+func _get_category_priority(category: PersisterEnums.Scope) -> int:
+ match category:
+ PersisterEnums.Scope.PERMANENT:
+ return 1
+ PersisterEnums.Scope.SAVE:
+ return 2
+ PersisterEnums.Scope.GAME:
+ return 3
+ PersisterEnums.Scope.RUN:
+ return 4
+ PersisterEnums.Scope.ROUND:
+ return 5
+ PersisterEnums.Scope.ROOM:
+ return 6
+ _:
+ return 0
+
+
+func _get_key_category(key: String) -> PersisterEnums.Scope:
+ var category_order = []
+
+ for category in PersisterEnums.Scope:
+ var category_value = PersisterEnums.Scope[category]
+
+ category_order.push_back(
+ {
+ "priority": _get_category_priority(category_value),
+ "category": category_value
+ }
+ )
+
+ category_order.sort_custom(func(a, b):
+ return a.priority > b.priority
+ )
+
+ for value in category_order:
+ var category = value.category
+
+ if _persisted.has(category):
+ if _persisted[category].has(key):
+ return category
+
+ return PersisterEnums.Scope.UNKNOWN
+
+
+func _get_all_category_data(category: PersisterEnums.Scope) -> Dictionary:
+ if not _persisted.has(category):
+ return {}
+
+ return _persisted[category]
+
+
+func _on_game_triggered(data: Dictionary) -> void:
+ clear_category(PersisterEnums.Scope.GAME)
+
+
+func _on_run_triggered(data: Dictionary) -> void:
+ clear_category(PersisterEnums.Scope.RUN)
+
+
+func _on_round_triggered(data: Dictionary) -> void:
+ clear_category(PersisterEnums.Scope.ROUND)
+
+
+func _on_room_triggered(data: Dictionary) -> void:
+ clear_category(PersisterEnums.Scope.ROOM)
+
+
+func _save():
+ _info("Saving")
+ _save_permanent_data()
+ _save_save_data()
+
+
+func _load():
+ _info("Loading")
+ _load_permanent_data()
+
+ if not get_value("save"):
+ _info("Creating starting save")
+ persist_data("save", 1, PersisterEnums.Scope.PERMANENT)
+
+ clear_category(PersisterEnums.Scope.SAVE)
+ _load_save_data()
+
+
+func _save_permanent_data():
+ var save_file = FileAccess.open("user://data.json", FileAccess.WRITE)
+
+ var data = _get_all_category_data(PersisterEnums.Scope.PERMANENT)
+ var data_string = JSON.stringify(data)
+
+ _info("Saving permanent save data")
+
+ save_file.store_line(data_string)
+
+
+func _save_save_data():
+ if not FileAccess.file_exists("user://saves"):
+ DirAccess.make_dir_absolute("user://saves")
+
+ var save = get_value("save")
+ var save_file = FileAccess.open("user://saves/save-%d.json" % [save], FileAccess.WRITE)
+
+ var data = _get_all_category_data(PersisterEnums.Scope.SAVE)
+ var data_string = JSON.stringify(data)
+
+ _info("Saving save save data")
+
+ save_file.store_line(data_string)
+
+
+func _load_permanent_data():
+ var save_file = FileAccess.open("user://data.json", FileAccess.READ)
+
+ if not save_file:
+ return
+
+ _info("Loading permanent save data")
+ var json_string = ""
+
+ while save_file.get_position() < save_file.get_length():
+ json_string += save_file.get_line()
+
+ var json = JSON.new()
+
+ var result = json.parse(json_string)
+ var data = json.get_data()
+
+ for key in data:
+ Persister.persist_data(key, data[key], PersisterEnums.Scope.PERMANENT)
+
+
+func _load_save_data():
+ var save = get_value("save")
+ var save_file = FileAccess.open("user://saves/save-%s.json" % [save], FileAccess.READ)
+
+ if not save_file:
+ return
+
+ _info("Loading save save data")
+ var json_string = ""
+
+ while save_file.get_position() < save_file.get_length():
+ json_string += save_file.get_line()
+
+ var json = JSON.new()
+
+ var result = json.parse(json_string)
+ var data = json.get_data()
+
+ Persister.clear_category(PersisterEnums.Scope.SAVE)
+
+ for key in data:
+ Persister.persist_data(key, data[key], PersisterEnums.Scope.SAVE)
+
+
+func _on_timer_timeout():
+ _save()
diff --git a/components/Persister/toolbar.txt b/components/Persister/toolbar.txt
new file mode 100644
index 0000000..a5a1bd8
--- /dev/null
+++ b/components/Persister/toolbar.txt
@@ -0,0 +1,10 @@
+save[]
+ type: button
+ name: Swap Save 1
+ accent: #11a1a1
+ function: save_one
+save2[]
+ type: button
+ name: Swap Save 2
+ accent: #11a1a1
+ function: save_two
diff --git a/components/Settings/Settings.tscn b/components/Settings/Settings.tscn
new file mode 100644
index 0000000..a9306aa
--- /dev/null
+++ b/components/Settings/Settings.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://d3uc4ryq5tqby"]
+
+[ext_resource type="Script" path="res://components/Settings/settings.gd" id="1_wmbcp"]
+
+[node name="Settings" type="Node"]
+script = ExtResource("1_wmbcp")
diff --git a/components/Settings/information.txt b/components/Settings/information.txt
new file mode 100644
index 0000000..e69de29
diff --git a/components/Settings/settings.gd b/components/Settings/settings.gd
new file mode 100644
index 0000000..dc34858
--- /dev/null
+++ b/components/Settings/settings.gd
@@ -0,0 +1,16 @@
+@icon("res://components/Audio/music.svg")
+extends Base
+
+
+func _ready():
+ #_log_category = "AUD"
+ #_log_icon = "res://components/Audio/music.svg"
+ #_log_color = "#32ad61"
+ super()
+ _info("Audio is active")
+ if _triggerer:
+ _triggerer.listen("any", _on_trigger)
+
+
+func _on_trigger(data: Dictionary) -> void:
+ pass
diff --git a/components/Settings/settings.svg b/components/Settings/settings.svg
new file mode 100644
index 0000000..2d72c63
--- /dev/null
+++ b/components/Settings/settings.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Settings/settings.svg.import b/components/Settings/settings.svg.import
new file mode 100644
index 0000000..aef76a8
--- /dev/null
+++ b/components/Settings/settings.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://c1toq8e1yg4he"
+path="res://.godot/imported/settings.svg-0c63f5959ea52eb948e9c6dfb59e2f91.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Settings/settings.svg"
+dest_files=["res://.godot/imported/settings.svg-0c63f5959ea52eb948e9c6dfb59e2f91.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Triggerer/TriggerReceiver.tscn b/components/Triggerer/TriggerReceiver.tscn
new file mode 100644
index 0000000..f762c18
--- /dev/null
+++ b/components/Triggerer/TriggerReceiver.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://cgivlj3yp8nsy"]
+
+[ext_resource type="Script" path="res://components/Triggerer/trigger_receiver.gd" id="1_rb2wc"]
+
+[node name="TriggerReceiver" type="Node"]
+script = ExtResource("1_rb2wc")
diff --git a/components/Triggerer/Triggerer.tscn b/components/Triggerer/Triggerer.tscn
new file mode 100644
index 0000000..bf52ae0
--- /dev/null
+++ b/components/Triggerer/Triggerer.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://rjec7f6cseh"]
+
+[ext_resource type="Script" path="res://components/Triggerer/triggerer.gd" id="1_e3bhf"]
+
+[node name="Triggerer" type="Node"]
+script = ExtResource("1_e3bhf")
diff --git a/components/Triggerer/antenna.svg b/components/Triggerer/antenna.svg
new file mode 100644
index 0000000..d448672
--- /dev/null
+++ b/components/Triggerer/antenna.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Triggerer/antenna.svg.import b/components/Triggerer/antenna.svg.import
new file mode 100644
index 0000000..a4034d5
--- /dev/null
+++ b/components/Triggerer/antenna.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://hnqq5c8erlns"
+path="res://.godot/imported/antenna.svg-9e84cc3637193460d89c85029a059bef.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Triggerer/antenna.svg"
+dest_files=["res://.godot/imported/antenna.svg-9e84cc3637193460d89c85029a059bef.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Triggerer/editor.gd b/components/Triggerer/editor.gd
new file mode 100644
index 0000000..ca3abe1
--- /dev/null
+++ b/components/Triggerer/editor.gd
@@ -0,0 +1,8 @@
+@tool
+extends EditorPlugin
+
+func _enter_tree():
+ add_custom_type("TriggerReceiver", "Node", preload("res://components/Triggerer/trigger_receiver.gd"), preload("res://components/Triggerer/antenna.svg"))
+
+func _exit_tree():
+ remove_custom_type("TriggerReceiver")
diff --git a/components/Triggerer/information.txt b/components/Triggerer/information.txt
new file mode 100644
index 0000000..a14833c
--- /dev/null
+++ b/components/Triggerer/information.txt
@@ -0,0 +1,11 @@
+name: Triggerer
+short: A trigger object to trigger things and read in triggers
+description: An object used for other objects to connect to for triggers.
+ Objects can call the read function with a callback function to
+ read in future triggers of a key to that callback. Objects can
+ then call the trigger function to trigger everything listening
+ to a key.
+accent: #dada00
+log_category: TRI
+icon: res://components/Triggerer/radio.svg
+version: v1.0.0
diff --git a/components/Triggerer/radio.svg b/components/Triggerer/radio.svg
new file mode 100644
index 0000000..2173a3f
--- /dev/null
+++ b/components/Triggerer/radio.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/components/Triggerer/radio.svg.import b/components/Triggerer/radio.svg.import
new file mode 100644
index 0000000..f672b6c
--- /dev/null
+++ b/components/Triggerer/radio.svg.import
@@ -0,0 +1,37 @@
+[remap]
+
+importer="texture"
+type="CompressedTexture2D"
+uid="uid://cxpp5fn5c75wj"
+path="res://.godot/imported/radio.svg-17a71d0d8df639a42271899eb9c43d1e.ctex"
+metadata={
+"vram_texture": false
+}
+
+[deps]
+
+source_file="res://components/Triggerer/radio.svg"
+dest_files=["res://.godot/imported/radio.svg-17a71d0d8df639a42271899eb9c43d1e.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
+svg/scale=1.0
+editor/scale_with_editor_scale=false
+editor/convert_colors_with_editor_theme=false
diff --git a/components/Triggerer/toolbar.txt b/components/Triggerer/toolbar.txt
new file mode 100644
index 0000000..10d80ab
--- /dev/null
+++ b/components/Triggerer/toolbar.txt
@@ -0,0 +1,20 @@
+trigger-event[]
+ type: modal
+ name: Trigger Event
+ accent: #11a1a1
+ modal[]
+ name[]
+ type: input
+ name: Event Name
+
+trigger-event-data[]
+ type: modal
+ name: Trigger Event w/ Data
+ accent: #2181c1
+ modal[]
+ name[]
+ type: input
+ name: Event Name
+ data[]
+ type: dictionary
+ name: Event Data
diff --git a/components/Triggerer/trigger_receiver.gd b/components/Triggerer/trigger_receiver.gd
new file mode 100644
index 0000000..8a12829
--- /dev/null
+++ b/components/Triggerer/trigger_receiver.gd
@@ -0,0 +1,22 @@
+@icon("res://components/Triggerer/antenna.svg")
+class_name TriggerReceiver
+extends Node
+
+signal received(data: Dictionary)
+
+@export var keys: Array[String]
+
+var _triggerer
+
+
+func _ready():
+ if get_tree().root.has_node("Triggerer"):
+ _triggerer = get_tree().root.get_node("Triggerer")
+
+ if _triggerer:
+ for key in keys:
+ _triggerer.listen(key, _on_received)
+
+
+func _on_received(data: Dictionary):
+ received.emit(data)
diff --git a/components/Triggerer/triggerer.gd b/components/Triggerer/triggerer.gd
new file mode 100644
index 0000000..ada5aa0
--- /dev/null
+++ b/components/Triggerer/triggerer.gd
@@ -0,0 +1,36 @@
+@icon("res://components/Triggerer/radio.svg")
+extends Base
+## A trigger object to trigger things and read in triggers
+##
+## An object used for other objects to connect to for triggers.
+## Objects can call the read function with a callback function to
+## read in future triggers of a key to that callback. Objects can
+## then call the trigger function to trigger everything listening
+## to a key.
+
+var _connections = {}
+
+
+## Trigger an event to be read in by other objects
+func trigger(key: String, data: Dictionary = {}) -> void:
+ _info("Triggered key ∧%s∧" % [key])
+
+ data.trigger = key
+
+ if _connections.has(key):
+ for callback in _connections[key]:
+ callback.call(data)
+
+ if _connections.has("any"):
+ for callback in _connections["any"]:
+ callback.call(data)
+
+
+## Read in future triggers of a key to a callback
+func listen(key: String, callback: Callable) -> void:
+ if not _connections.has(key):
+ _info("Created new connection key ∧%s∧" % [key])
+ _connections[key] = []
+
+ _info("Callback ∨%s∨ on →%s← added to key ∧%s∧" % [callback.get_method(), callback.get_object().name, key])
+ _connections[key].push_back(callback)
diff --git a/components/Unlocks/Unlocks.tscn b/components/Unlocks/Unlocks.tscn
new file mode 100644
index 0000000..7b26b7a
--- /dev/null
+++ b/components/Unlocks/Unlocks.tscn
@@ -0,0 +1,6 @@
+[gd_scene load_steps=2 format=3 uid="uid://cwcttvh7s42ca"]
+
+[ext_resource type="Script" path="res://components/Unlocks/unlocks.gd" id="1_ege4w"]
+
+[node name="Unlocks" type="Node"]
+script = ExtResource("1_ege4w")
diff --git a/components/Unlocks/information.txt b/components/Unlocks/information.txt
new file mode 100644
index 0000000..e69de29
diff --git a/components/Unlocks/unlocks.gd b/components/Unlocks/unlocks.gd
new file mode 100644
index 0000000..a31c3d6
--- /dev/null
+++ b/components/Unlocks/unlocks.gd
@@ -0,0 +1,127 @@
+class_name UnlocksType
+extends Base
+
+# Signals
+
+# Constants
+const LOG_CATEGORY = "LOCK"
+
+var unlocks = {}
+
+func unlock_item(key: String, category: String):
+ if not unlocks.has(category):
+ if _logger: _logger.error("Attempted to unlock item %s in invalid category %s" % [key, category], LOG_CATEGORY)
+ return
+
+ if not unlocks[category].has(key):
+ if _logger: _logger.error("Attempted to unlock invalid item %s in category %s" % [key, category], LOG_CATEGORY)
+ return
+
+ if unlocks[category][key]:
+ if _logger: _logger.warn("Unlocked already unlocked item %s in category %s" % [key, category], LOG_CATEGORY)
+
+ unlocks[category][key].status = true
+ if _logger: _logger.info("Unlocked item %s in category %s" % [key, category], LOG_CATEGORY)
+
+
+func lock_item(key: String, category: String):
+ if not unlocks.has(category):
+ if _logger: _logger.error("Attempted to lock item %s in invalid category %s" % [key, category], LOG_CATEGORY)
+ return
+
+ if not unlocks[category].has(key):
+ if _logger: _logger.error("Attempted to lock invalid item %s in category %s" % [key, category], LOG_CATEGORY)
+ return
+
+ if not unlocks[category][key]:
+ if _logger: _logger.warn("Locked already locked item %s in category %s" % [key, category], LOG_CATEGORY)
+
+ unlocks[category][key].status = false
+ if _logger: _logger.info("Locked item %s in category %s" % [key, category], LOG_CATEGORY)
+
+
+func reset_unlocks():
+ if (_data):
+ unlocks = {}
+
+ for category in _data.data:
+ var category_data = _data.data[category]
+
+ if category_data.has("!_config"):
+ if category_data["!_config"].has("unlock"):
+ if category_data["!_config"]["unlock"] == "true":
+ unlocks[category] = {}
+
+ for key in category_data:
+ if key == "!_config":
+ continue
+
+ var element = category_data[key]
+
+ if element.has("locked"):
+ if element["locked"] == "true":
+ if not element.has("unlock_trigger"):
+ if _logger: _logger.warn("Locked element %s in category %s has no unlock trigger" % [key, category], LOG_CATEGORY)
+ unlocks[category][key] = {
+ "status": false
+ }
+ continue
+
+ if not element.has("unlock_amount"):
+ if _logger: _logger.warn("Locked element %s in category %s has no unlock amount" % [key, category], LOG_CATEGORY)
+ unlocks[category][key] = {
+ "status": false
+ }
+ continue
+
+ if is_nan(int(element["unlock_amount"])):
+ if _logger: _logger.warn("Locked element %s in category %s unlock amount %s is not a number" % [key, category, element["unlock_amount"]], LOG_CATEGORY)
+ unlocks[category][key] = {
+ "status": false
+ }
+ continue
+
+ unlocks[category][key] = {
+ "status": false,
+ "trigger": element["unlock_trigger"],
+ "amount": int(element["unlock_amount"])
+ }
+ else:
+ unlocks[category][key] = {
+ "status": true
+ }
+ else:
+ unlocks[category][key] = {
+ "status": true
+ }
+ if _logger: _logger.info("Reset Unlocks", LOG_CATEGORY)
+
+func _ready():
+ super()
+
+ reset_unlocks()
+
+
+#func _on_number_persisted(key: String, value, category: PersisterType.DataCategory):
+ #_check_trigger(key, value)
+
+
+func _check_trigger(key: String, value: int):
+ for category in unlocks:
+ var category_data = unlocks[category]
+
+ for element in category_data:
+ var element_data = category_data[element]
+
+ if not element_data.has("trigger"):
+ continue
+
+ if element_data["trigger"] != key:
+ continue
+
+ if element_data["status"]:
+ continue
+
+ if _logger: _logger.debug("Checking trigger %s for item %s in category %s: %d (required) vs %d (incoming)" % [key, element, category, element_data["amount"], value], LOG_CATEGORY)
+ if value > element_data["amount"]:
+ unlock_item(element, category)
diff --git a/parts/metadata.txt b/parts/metadata.txt
new file mode 100644
index 0000000..2a0abe1
--- /dev/null
+++ b/parts/metadata.txt
@@ -0,0 +1,3 @@
+name: Name Pending
+author: Team Auboreal
+start_scene: res://Main.tscn
diff --git a/project.godot b/project.godot
index 749bbb6..75254fd 100644
--- a/project.godot
+++ b/project.godot
@@ -11,9 +11,47 @@ config_version=5
[application]
config/name="ld-56"
+run/main_scene="res://components/Menu/Menu.tscn"
config/features=PackedStringArray("4.3", "GL Compatibility")
config/icon="res://icon.svg"
+[autoload]
+
+Logger="*res://components/Logger/Logger.tscn"
+Data="*res://components/Data/Data.tscn"
+Triggerer="*res://components/Triggerer/Triggerer.tscn"
+Persister="*res://components/Persister/Persister.tscn"
+Cursor="*res://components/Cursor/Cursor.tscn"
+Achievements="*res://components/Achievements/Achievements.tscn"
+Dialogue="*res://components/Dialogue/Dialogue.tscn"
+
+[display]
+
+window/size/viewport_width=640
+window/size/viewport_height=360
+
+[editor_plugins]
+
+enabled=PackedStringArray("res://addons/laia_highlighter/plugin.cfg")
+
+[input]
+
+left_click={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":1,"position":Vector2(201, 7),"global_position":Vector2(210, 53),"factor":1.0,"button_index":1,"canceled":false,"pressed":true,"double_click":false,"script":null)
+]
+}
+right_click={
+"deadzone": 0.5,
+"events": [Object(InputEventMouseButton,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"button_mask":2,"position":Vector2(204, 2),"global_position":Vector2(213, 48),"factor":1.0,"button_index":2,"canceled":false,"pressed":true,"double_click":false,"script":null)
+]
+}
+escape={
+"deadzone": 0.5,
+"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
+]
+}
+
[rendering]
renderer/rendering_method="gl_compatibility"