commit 31c1d81f77ea70cb97a973f8caad04c25652e840 Author: Ategon Date: Sun Sep 8 13:34:41 2024 -0400 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8ad74f7 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Normalize EOL for all files that Git considers text files. +* text=auto eol=lf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0af181c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +# Godot 4+ specific ignores +.godot/ +/android/ diff --git a/Main.tscn b/Main.tscn new file mode 100644 index 0000000..336395e --- /dev/null +++ b/Main.tscn @@ -0,0 +1,12 @@ +[gd_scene load_steps=2 format=3 uid="uid://5ske2hm55rce"] + +[ext_resource type="Theme" uid="uid://d035h7upxrw3h" path="res://theme.tres" id="1_xbn5h"] + +[node name="Main" type="Node2D"] + +[node name="Control" type="Control" parent="."] +layout_mode = 3 +anchors_preset = 0 +offset_right = 40.0 +offset_bottom = 40.0 +theme = ExtResource("1_xbn5h") diff --git a/components/Achievements/Achievements.tscn b/components/Achievements/Achievements.tscn new file mode 100644 index 0000000..152910c --- /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://d035h7upxrw3h" path="res://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..7502606 --- /dev/null +++ b/components/Achievements/achievements.gd @@ -0,0 +1,263 @@ +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): + if Persister.get_value("paused") or not Persister.get_value("active"): + return + 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/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..f40ce7e --- /dev/null +++ b/components/Cursor/Cursor.tscn @@ -0,0 +1,22 @@ +[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://q4wsbkhirtoh" path="res://components/Cursor/cursor.png" id="2_luk0m"] + +[sub_resource type="RectangleShape2D" id="RectangleShape2D_4cq27"] + +[node name="Cursor" type="CanvasLayer"] +layer = 10 +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_luk0m") + +[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-ui.aseprite b/components/Cursor/cursor-ui.aseprite new file mode 100644 index 0000000..5685618 Binary files /dev/null and b/components/Cursor/cursor-ui.aseprite differ diff --git a/components/Cursor/cursor-ui.png b/components/Cursor/cursor-ui.png new file mode 100644 index 0000000..dd476d3 Binary files /dev/null and b/components/Cursor/cursor-ui.png differ diff --git a/components/Cursor/cursor-ui.png.import b/components/Cursor/cursor-ui.png.import new file mode 100644 index 0000000..0141787 --- /dev/null +++ b/components/Cursor/cursor-ui.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cg140nu46vm2p" +path="res://.godot/imported/cursor-ui.png-342ebc343c2c58c466eef67d0e8042ef.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://components/Cursor/cursor-ui.png" +dest_files=["res://.godot/imported/cursor-ui.png-342ebc343c2c58c466eef67d0e8042ef.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/Cursor/cursor.aseprite b/components/Cursor/cursor.aseprite new file mode 100644 index 0000000..33e8373 Binary files /dev/null and b/components/Cursor/cursor.aseprite differ diff --git a/components/Cursor/cursor.gd b/components/Cursor/cursor.gd new file mode 100644 index 0000000..b040e4a --- /dev/null +++ b/components/Cursor/cursor.gd @@ -0,0 +1,87 @@ +@icon("res://components/Cursor/mouse-pointer-2.svg") +extends Base + +@onready var mouse_control = $"MouseControl" +@onready var sprite_2d: Sprite2D = $MouseControl/Sprite2D + +const CURSOR_UI = preload("res://components/Cursor/cursor-ui.png") +const CURSOR = preload("res://components/Cursor/cursor.png") +var cursor_state = "normal" +var rotation_tween + +func _ready() -> void: + Persister.data_persisted.connect(_on_data_persisted) + Triggerer.listen("fire", _on_fire) + + +func _on_fire(_data): + if cursor_state == "ui": + return + + sprite_2d.scale = Vector2.ONE * 1.25 * 0.156 + + +func _on_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"): + var current_node = area + var hidden = false + while current_node: + if "modulate" in current_node: + if current_node.modulate.a < 1: + hidden = true + break + current_node = current_node.get_parent() + + if hidden: + continue + + if not area._on_clicked(): + break + +func _on_rclick(): + 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_rclicked"): + var current_node = area + var hidden = false + while current_node: + if "modulate" in current_node: + if current_node.modulate.a < 1: + hidden = true + break + current_node = current_node.get_parent() + + if hidden: + continue + + if not area._on_rclicked(): + break + + +func _process(delta): + if Input.is_action_just_pressed("lclick"): + _on_click() + if Input.is_action_just_pressed("rclick"): + _on_rclick() + Input.set_mouse_mode(Input.MOUSE_MODE_HIDDEN) + mouse_control.position = mouse_control.get_global_mouse_position() + + +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/cursor.png b/components/Cursor/cursor.png new file mode 100644 index 0000000..366ced9 Binary files /dev/null and b/components/Cursor/cursor.png differ diff --git a/components/Cursor/cursor.png.import b/components/Cursor/cursor.png.import new file mode 100644 index 0000000..c5407f3 --- /dev/null +++ b/components/Cursor/cursor.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://q4wsbkhirtoh" +path="res://.godot/imported/cursor.png-ab5b6c93c7e19595ea2eadb14e17d081.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://components/Cursor/cursor.png" +dest_files=["res://.godot/imported/cursor.png-ab5b6c93c7e19595ea2eadb14e17d081.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/Cursor/grinder-mini-icon.aseprite b/components/Cursor/grinder-mini-icon.aseprite new file mode 100644 index 0000000..1c519f5 Binary files /dev/null and b/components/Cursor/grinder-mini-icon.aseprite differ diff --git a/components/Cursor/grinder-mini-icon.png b/components/Cursor/grinder-mini-icon.png new file mode 100644 index 0000000..3b83fad Binary files /dev/null and b/components/Cursor/grinder-mini-icon.png differ diff --git a/components/Cursor/grinder-mini-icon.png.import b/components/Cursor/grinder-mini-icon.png.import new file mode 100644 index 0000000..8720884 --- /dev/null +++ b/components/Cursor/grinder-mini-icon.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://lrvl2uddu856" +path="res://.godot/imported/grinder-mini-icon.png-1f6177a95ae870b905ce582bb42a531b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://components/Cursor/grinder-mini-icon.png" +dest_files=["res://.godot/imported/grinder-mini-icon.png-1f6177a95ae870b905ce582bb42a531b.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/Cursor/grinder.aseprite b/components/Cursor/grinder.aseprite new file mode 100644 index 0000000..dbdbee9 Binary files /dev/null and b/components/Cursor/grinder.aseprite differ diff --git a/components/Cursor/grinder.png b/components/Cursor/grinder.png new file mode 100644 index 0000000..74a5e6c Binary files /dev/null and b/components/Cursor/grinder.png differ diff --git a/components/Cursor/grinder.png.import b/components/Cursor/grinder.png.import new file mode 100644 index 0000000..e3995ba --- /dev/null +++ b/components/Cursor/grinder.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://cygdk25d83qp5" +path="res://.godot/imported/grinder.png-138a6003a032d52389ff94554d883d54.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://components/Cursor/grinder.png" +dest_files=["res://.godot/imported/grinder.png-138a6003a032d52389ff94554d883d54.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/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..e5ebee2 --- /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 +signal rclicked + +@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() + return passthrough + +func _on_rclicked(): + rclicked.emit() + 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/Cursor/saw.aseprite b/components/Cursor/saw.aseprite new file mode 100644 index 0000000..edca06c Binary files /dev/null and b/components/Cursor/saw.aseprite differ diff --git a/components/Cursor/saw.png b/components/Cursor/saw.png new file mode 100644 index 0000000..ac5b165 Binary files /dev/null and b/components/Cursor/saw.png differ diff --git a/components/Cursor/saw.png.import b/components/Cursor/saw.png.import new file mode 100644 index 0000000..90594cc --- /dev/null +++ b/components/Cursor/saw.png.import @@ -0,0 +1,34 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://mnwnajp2e87v" +path="res://.godot/imported/saw.png-36e8ff0c640bc68b02eebb4f43290f75.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://components/Cursor/saw.png" +dest_files=["res://.godot/imported/saw.png-36e8ff0c640bc68b02eebb4f43290f75.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/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/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..e69de29 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..d15ebfc --- /dev/null +++ b/components/Logger/logger.gd @@ -0,0 +1,138 @@ +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 = 0, + INFO = 1, + WARN = 2, + ERROR = 3 +} + +## 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: + if log_level > LogLevel.DEBUG: + return + + _log(message, LogLevel.DEBUG, arguments) + + +## Log a message at log level info (log to indicate something happened). +func info(message: String, arguments: Dictionary = {}) -> void: + if log_level > LogLevel.INFO: + return + + _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/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..e18a771 --- /dev/null +++ b/components/Persister/persister.gd @@ -0,0 +1,384 @@ +@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 + var original_pass = value + + 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 + + if _triggerer: + _triggerer.trigger(key, {"value": original_pass}) + + _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, default = null): + if category == PersisterEnums.Scope.UNKNOWN: + category = _get_key_category(key) + + if category == PersisterEnums.Scope.UNKNOWN: + return default + + 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_value_clamp_min(key: String, value: int, min: int, category := PersisterEnums.Scope.RUN) -> void: + change_value_clamp(key, value, min, INF, category) + + +func change_value_clamp_max(key: String, value: int, max: int, category := PersisterEnums.Scope.RUN) -> void: + change_value_clamp(key, value, -INF, max, category) + + +func change_value_clamp(key: String, value: int, min: int, max: int, category := PersisterEnums.Scope.RUN) -> void: + if min > max and not min == -9223372036854775808 and not max == -9223372036854775808: + _warn("Attempted to change clamp value %s with higher min %s than max %s" % [key, min, max]) + return + + if not _persisted.has(category): + _persisted[category] = {} + + if not _persisted[category].has(key): + if value < min and not min == -9223372036854775808: + value = min + + if value > max and not max == -9223372036854775808: + value = max + + 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]) + var new_value = value + old_value + + if new_value < min and not min == -9223372036854775808: + new_value = min + + if new_value > max and not max == -9223372036854775808: + new_value = max + + if old_value == new_value: + return + + _info("Added clamped number |%d| to key <%s> in category {%s} (old: |%d|) (new: |%d|)" % [value, key, _get_category_name(category), old_value, new_value]) + persist_data(key, new_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/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..8e9b8a6 --- /dev/null +++ b/components/Triggerer/triggerer.gd @@ -0,0 +1,40 @@ +@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): + _connections[key] = _connections[key].filter(func(x): return x.is_valid()) + + for callback in _connections[key]: + callback.call(data) + + if _connections.has("any"): + _connections["any"] = _connections["any"].filter(func(x): return x.is_valid()) + + 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/icon.svg b/icon.svg new file mode 100644 index 0000000..9d8b7fa --- /dev/null +++ b/icon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/icon.svg.import b/icon.svg.import new file mode 100644 index 0000000..f6295fc --- /dev/null +++ b/icon.svg.import @@ -0,0 +1,37 @@ +[remap] + +importer="texture" +type="CompressedTexture2D" +uid="uid://bi03hh1ousovx" +path="res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.ctex" +metadata={ +"vram_texture": false +} + +[deps] + +source_file="res://icon.svg" +dest_files=["res://.godot/imported/icon.svg-218a8f2b3041327d8a5756f3a245f83b.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/project.godot b/project.godot new file mode 100644 index 0000000..f400f09 --- /dev/null +++ b/project.godot @@ -0,0 +1,36 @@ +; Engine configuration file. +; It's best edited using the editor UI and not directly, +; since the parameters that go here are not all obvious. +; +; Format: +; [section] ; section goes between [] +; param=value ; assign values to parameters + +config_version=5 + +[application] + +config/name="brackeys-12" +run/main_scene="res://Main.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" +Achievements="*res://components/Achievements/Achievements.tscn" +Cursor="*res://components/Cursor/Cursor.tscn" + +[display] + +window/size/viewport_width=640 +window/size/viewport_height=360 +window/stretch/mode="canvas_items" + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" diff --git a/theme.tres b/theme.tres new file mode 100644 index 0000000..4b0acce --- /dev/null +++ b/theme.tres @@ -0,0 +1,3 @@ +[gd_resource type="Theme" format=3 uid="uid://d035h7upxrw3h"] + +[resource]