diff --git a/.gitignore b/.gitignore index 0af181c..4b46458 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ # Godot 4+ specific ignores .godot/ /android/ +/keystore/ +/dist/ + diff --git a/export_presets.cfg b/export_presets.cfg index a0a2f90..d02c066 100644 --- a/export_presets.cfg +++ b/export_presets.cfg @@ -9,7 +9,7 @@ custom_features="" export_filter="all_resources" include_filter="" exclude_filter="" -export_path="./Repeat After Me.apk" +export_path="dist/Repeat-After-Me-1.0.1.apk" patches=PackedStringArray() encryption_include_filters="" encryption_exclude_filters="" diff --git a/project.godot b/project.godot index a64ee04..0006c17 100644 --- a/project.godot +++ b/project.godot @@ -11,7 +11,7 @@ config_version=5 [application] config/name="Repeat After Me" -config/version="0.1.0" +config/version="1.0.1" run/main_scene="uid://cv38ubwerjlpx" config/features=PackedStringArray("4.4", "Mobile") run/low_processor_mode=true diff --git a/scenes/main.tscn b/scenes/main.tscn index 4f6fc1c..ea6a8fb 100644 --- a/scenes/main.tscn +++ b/scenes/main.tscn @@ -179,17 +179,10 @@ offset_right = -40.0 offset_bottom = 320.0 alignment = 1 keep_editing_on_text_submit = true - -[node name="SubmitButton" type="Button" parent="."] -unique_name_in_owner = true -layout_mode = 2 -anchor_left = 0.5 -anchor_right = 0.5 -offset_left = -40.0 -offset_top = 344.0 -offset_right = 40.0 -offset_bottom = 376.0 -text = "Submit" +context_menu_enabled = false +middle_mouse_paste_enabled = false +selecting_enabled = false +drag_and_drop_selection_enabled = false [node name="SettingsMenu" parent="." instance=ExtResource("8_85g3d")] unique_name_in_owner = true @@ -214,6 +207,7 @@ offset_right = 32.0 offset_bottom = -33.7 grow_horizontal = 2 grow_vertical = 0 +focus_mode = 2 theme_override_styles/panel = SubResource("StyleBoxFlat_ya4ey") script = ExtResource("11_ya4ey") @@ -228,4 +222,3 @@ vertical_alignment = 1 [connection signal="pressed" from="ShowSettingsButton" to="." method="_on_settings_button_pressed"] [connection signal="pressed" from="ShowPhrasesButton" to="." method="_on_show_phrases_button_pressed"] [connection signal="text_changed" from="AnswerInput" to="." method="_on_answer_input_text_changed"] -[connection signal="pressed" from="SubmitButton" to="." method="_on_submit_button_pressed"] diff --git a/scenes/phrases_menu.tscn b/scenes/phrases_menu.tscn index 17ed404..c6bb51b 100644 --- a/scenes/phrases_menu.tscn +++ b/scenes/phrases_menu.tscn @@ -57,6 +57,10 @@ offset_top = 56.0 offset_right = 184.0 offset_bottom = 75.0 keep_editing_on_text_submit = true +context_menu_enabled = false +middle_mouse_paste_enabled = false +selecting_enabled = false +drag_and_drop_selection_enabled = false [node name="AddPhraseButton" type="Button" parent="."] layout_mode = 1 diff --git a/scenes/phrases_menu_single_phrase.tscn b/scenes/phrases_menu_single_phrase.tscn index 4acdf84..0f4c406 100644 --- a/scenes/phrases_menu_single_phrase.tscn +++ b/scenes/phrases_menu_single_phrase.tscn @@ -22,6 +22,13 @@ offset_bottom = 16.0 clip_text = true text_overrun_behavior = 3 +[node name="LabelButton" type="TextureButton" parent="."] +layout_mode = 1 +anchors_preset = -1 +anchor_right = 1.143 +offset_right = -48.032 +offset_bottom = 16.0 + [node name="RemoveButton" type="TextureButton" parent="."] layout_mode = 2 anchor_left = 1.036 @@ -32,4 +39,5 @@ offset_bottom = 16.0 texture_normal = ExtResource("2_cv30m") stretch_mode = 0 +[connection signal="pressed" from="LabelButton" to="." method="_on_label_button_pressed"] [connection signal="pressed" from="RemoveButton" to="." method="_on_remove_button_pressed"] diff --git a/scenes/settings_menu.tscn b/scenes/settings_menu.tscn index 7886b5e..3214909 100644 --- a/scenes/settings_menu.tscn +++ b/scenes/settings_menu.tscn @@ -32,6 +32,15 @@ theme = ExtResource("2_lwwgp") theme_override_styles/panel = SubResource("StyleBoxTexture_choun") script = ExtResource("2_labj1") +[node name="MenuLabel" type="Label" parent="."] +layout_mode = 0 +offset_left = 16.0 +offset_top = 16.0 +offset_right = 104.0 +offset_bottom = 39.0 +text = "Settings" +label_settings = SubResource("LabelSettings_labj1") + [node name="CloseSettingsButton" type="TextureButton" parent="."] layout_mode = 1 anchors_preset = 1 @@ -45,22 +54,44 @@ grow_horizontal = 0 texture_normal = ExtResource("2_wswnh") stretch_mode = 0 -[node name="Label" type="Label" parent="."] +[node name="ImportExportLabel" type="Label" parent="."] layout_mode = 0 offset_left = 16.0 -offset_top = 16.0 -offset_right = 104.0 -offset_bottom = 39.0 -text = "Settings" -label_settings = SubResource("LabelSettings_labj1") +offset_top = 80.0 +offset_right = 144.0 +offset_bottom = 114.8 +text = "Import | Export +to/from Clipboard" -[node name="ResetXPButton" type="Button" parent="."] +[node name="ImportButton" type="Button" parent="ImportExportLabel"] +layout_mode = 0 +offset_top = 48.0 +offset_right = 61.0 +offset_bottom = 72.0 +text = "Import" + +[node name="ExportButton" type="Button" parent="ImportExportLabel"] +layout_mode = 0 +offset_left = 75.0 +offset_top = 48.0 +offset_right = 136.0 +offset_bottom = 72.0 +text = "Export" + +[node name="ResetLabel" type="Label" parent="."] layout_mode = 0 offset_left = 16.0 -offset_top = 72.0 -offset_right = 92.0 -offset_bottom = 96.0 -text = "Reset XP" +offset_top = 192.0 +offset_right = 91.0 +offset_bottom = 207.9 +text = "Reset Data" + +[node name="ResetXpAndStatsButton" type="Button" parent="ResetLabel"] +layout_mode = 0 +offset_top = 24.0 +offset_right = 76.0 +offset_bottom = 48.0 +text = "Reset XP & Stats" [node name="Info Footer" type="Label" parent="."] layout_mode = 1 @@ -84,4 +115,6 @@ text_overrun_behavior = 3 script = ExtResource("5_lwwgp") [connection signal="pressed" from="CloseSettingsButton" to="." method="_on_close_settings_button_pressed"] -[connection signal="pressed" from="ResetXPButton" to="." method="_on_reset_xp_button_pressed"] +[connection signal="pressed" from="ImportExportLabel/ImportButton" to="." method="_on_import_button_pressed"] +[connection signal="pressed" from="ImportExportLabel/ExportButton" to="." method="_on_export_button_pressed"] +[connection signal="pressed" from="ResetLabel/ResetXpAndStatsButton" to="." method="_on_reset_xp_and_stats_button_pressed"] diff --git a/src/global/CoreGameplayManager.gd b/src/global/CoreGameplayManager.gd index 2a14529..fa8074c 100644 --- a/src/global/CoreGameplayManager.gd +++ b/src/global/CoreGameplayManager.gd @@ -14,15 +14,13 @@ var _last_autocleanup: int = 0 # ms since engine start var current_phrase: String = "" var current_status: String = "" -var last_played_phrases: Dictionary = {} # p: [timestamp1, timestamp2, ...] - +# represents the order of last played phrases +var last_played_phrases: Array = [] func register_current_phrase_played(): - var t = Time.get_unix_time_from_system() if current_phrase in last_played_phrases: - last_played_phrases[current_phrase].append(t) - else: - last_played_phrases[current_phrase] = [t] + last_played_phrases.erase(current_phrase) + last_played_phrases.append(current_phrase) SaveManager.save_game() func answer(p_in: String) -> bool: @@ -38,23 +36,33 @@ func next_phrase() -> void: current_phrase = "" current_status = STATUS_NONE_AVAILABLE return - # search for a non played phrase + # pick a random non-played phrase, if possible + var phrases_not_played = [] for p in PhrasesManager.phrases: if not p in last_played_phrases: - current_phrase = p - current_status = STATUS_PLEASE_REPEAT - return - # find the phrase that was played the longest ago - var phrases_last_ts: Dictionary = {} # timestamp: phrase - var phrases_last_ts_keys: Array[float] = [] - for p in last_played_phrases: - if p in PhrasesManager.phrases: - var t_max = last_played_phrases[p].max() - phrases_last_ts[t_max] = p - phrases_last_ts_keys.append(t_max) - # return the phrase with the smallest timestamp (-> longest ago) - current_phrase = phrases_last_ts[phrases_last_ts_keys.min()] - current_status = STATUS_PLEASE_REPEAT + phrases_not_played.append(p) + if len(phrases_not_played) > 0: + current_phrase = phrases_not_played.pick_random() + current_status = STATUS_PLEASE_REPEAT + return + # find the half of phrases that were repeated longest ago + var i_max = max(1, roundi(float(len(last_played_phrases)) / 2)) + var phrases_played_longest_ago = last_played_phrases.slice(0, i_max) + # pick random phrase + var phrase = phrases_played_longest_ago.pick_random() + if phrase == null: # this shouldn't happen! + current_phrase = "" + current_status = STATUS_NONE_AVAILABLE + else: + current_phrase = phrase + current_status = STATUS_PLEASE_REPEAT + +func try_overwrite_next_phrase(p: String): + if p in PhrasesManager.phrases: + current_phrase = p + current_status = STATUS_PLEASE_REPEAT + else: # if this didn't work, choose one automatically + next_phrase() func cleanup_last_played_phrases(): var changed = false diff --git a/src/global/SaveManager.gd b/src/global/SaveManager.gd index e8b979c..1a9358b 100644 --- a/src/global/SaveManager.gd +++ b/src/global/SaveManager.gd @@ -5,13 +5,33 @@ const SAVEFILE = "user://save.dat" func _ready() -> void: load_game() -func save_game(): - var data: Dictionary = { +func _to_dict() -> Dictionary: + return { "player_xp": XpLevelManager.player_xp, "phrases": PhrasesManager.phrases, "last_played_phrases": CoreGameplayManager.last_played_phrases } - var data_json = JSON.stringify(data) + +func _from_dict(data: Dictionary) -> int: + var successfully_set = 0 + # set variables + if "player_xp" in data: + XpLevelManager.loading = true + XpLevelManager.player_xp = data["player_xp"] + XpLevelManager.loading = false + successfully_set += 1 #! + if "phrases" in data and data["phrases"] is Array: + PhrasesManager.phrases = [] + for p in data["phrases"]: + PhrasesManager.phrases.append(p) + successfully_set += 1 #! + if "last_played_phrases" in data and data["last_played_phrases"] is Array: + CoreGameplayManager.last_played_phrases = data["last_played_phrases"] + successfully_set += 1 #! + return successfully_set + +func save_game(): + var data_json = JSON.stringify(_to_dict()) var f = FileAccess.open(SAVEFILE, FileAccess.WRITE) f.store_string(data_json) f.close() @@ -20,14 +40,19 @@ func load_game(): if FileAccess.file_exists(SAVEFILE): var data_json = FileAccess.get_file_as_string(SAVEFILE) var data = JSON.parse_string(data_json) - # set variables - if "player_xp" in data: - XpLevelManager.loading = true - XpLevelManager.player_xp = data["player_xp"] - XpLevelManager.loading = false - if "phrases" in data and data["phrases"] is Array: - PhrasesManager.phrases = [] - for p in data["phrases"]: - PhrasesManager.phrases.append(p) - if "last_played_phrases" in data and data["last_played_phrases"] is Dictionary: - CoreGameplayManager.last_played_phrases = data["last_played_phrases"] + _from_dict(data) + +func export_to_base64() -> String: + var data_json = JSON.stringify(_to_dict()) + return Marshalls.utf8_to_base64(data_json) + +func import_from_base64(encoded_data: String) -> bool: + var data_json = Marshalls.base64_to_utf8(encoded_data) + var data_dict = JSON.parse_string(data_json) + if data_dict == null: + return false + var n_successful: int = _from_dict(data_dict) > 0 + if n_successful > 0: + save_game() + return true + else: return false diff --git a/src/ui/main.gd b/src/ui/main.gd index 04a4f7a..b608e66 100644 --- a/src/ui/main.gd +++ b/src/ui/main.gd @@ -2,7 +2,6 @@ extends Control func _ready() -> void: CoreGameplayManager.next_phrase() - %SubmitButton.hide() %AnswerInput.hide() func _on_settings_button_pressed() -> void: @@ -24,17 +23,10 @@ func _process(_delta: float) -> void: else: %CurrentPhrase.text = "" %AnswerInput.hide() - %SubmitButton.hide() if last_known_status != CoreGameplayManager.current_status: last_known_status = CoreGameplayManager.current_status %CurrentStatus.text = last_known_status -func _on_submit_button_pressed() -> void: - if CoreGameplayManager.answer(%AnswerInput.text): - %AnswerInput.clear() - func _on_answer_input_text_changed(new_text: String) -> void: - if new_text.to_lower() == CoreGameplayManager.current_phrase.to_lower(): - %SubmitButton.show() - else: - %SubmitButton.hide() + if CoreGameplayManager.answer(new_text): + %AnswerInput.clear() diff --git a/src/ui/notification_container.gd b/src/ui/notification_container.gd index ca3c14a..ebbefe8 100644 --- a/src/ui/notification_container.gd +++ b/src/ui/notification_container.gd @@ -1,6 +1,5 @@ extends PanelContainer -var showing_notification: bool = false var notification_started: int = 0 # in ms var current_notification_timeout: int = 0 # in ms @@ -9,18 +8,14 @@ func _ready() -> void: func _process(_delta: float) -> void: var t = Time.get_ticks_msec() - if showing_notification: + if visible: if t - notification_started > current_notification_timeout: - showing_notification = false + hide() else: var n = NotificationQueue.get_next() # [text, timeout] or null if n != null: - showing_notification = true notification_started = t $Label.text = n[0] current_notification_timeout = n[1] - # - if not showing_notification and visible: - hide() - elif showing_notification and not visible: - show() + show() + grab_focus() diff --git a/src/ui/phrases_menu_phrase.gd b/src/ui/phrases_menu_phrase.gd index 01b32e4..5779008 100644 --- a/src/ui/phrases_menu_phrase.gd +++ b/src/ui/phrases_menu_phrase.gd @@ -8,3 +8,6 @@ var text: String: func _on_remove_button_pressed() -> void: PhrasesManager.remove_phrase(text) + +func _on_label_button_pressed() -> void: + CoreGameplayManager.try_overwrite_next_phrase(text) diff --git a/src/ui/settings_menu.gd b/src/ui/settings_menu.gd index aa45e01..91810f3 100644 --- a/src/ui/settings_menu.gd +++ b/src/ui/settings_menu.gd @@ -6,7 +6,22 @@ func _ready() -> void: func _on_close_settings_button_pressed() -> void: hide() -func _on_reset_xp_button_pressed() -> void: +func _on_reset_xp_and_stats_button_pressed() -> void: XpLevelManager.player_xp = 0 + CoreGameplayManager.last_played_phrases = [] SaveManager.save_game() - NotificationQueue.add("Reset XP.") + CoreGameplayManager.next_phrase() + NotificationQueue.add("Reset XP & Stats.") + +func _on_import_button_pressed() -> void: + var data = DisplayServer.clipboard_get() + if SaveManager.import_from_base64(data): + NotificationQueue.add("Import successful", 4000) + CoreGameplayManager.next_phrase() + else: + NotificationQueue.add("Import failed", 4000) + +func _on_export_button_pressed() -> void: + var data = SaveManager.export_to_base64() + DisplayServer.clipboard_set(data) + NotificationQueue.add("Exported to clipboard", 4000)