Merge pull request #35 from Excello-Recherche-Education/2.1.3

Merge 2.1.3 into main
This commit is contained in:
Adrien Ufferte
2025-07-04 11:44:18 +02:00
committed by GitHub
25 changed files with 140 additions and 88 deletions
+1
View File
@@ -3,6 +3,7 @@
.DS_Store
addons/godot-sqlite/bin
addons/script-ide
language_resources/
+4 -4
View File
@@ -28,12 +28,12 @@ gradle_build/android_source_template=""
gradle_build/compress_native_libraries=false
gradle_build/export_format=1
gradle_build/min_sdk=""
gradle_build/target_sdk=""
gradle_build/target_sdk="35"
architectures/armeabi-v7a=false
architectures/arm64-v8a=true
architectures/x86=false
architectures/x86_64=false
version/code=44
version/code=45
version/name=""
package/unique_name="org.godotengine.kalulu"
package/name=""
@@ -831,7 +831,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../Export/MacOS/Kalulu.dmg"
export_path="../Export/MacOS/kalulu_app.dmg"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
@@ -1088,7 +1088,7 @@ custom_features=""
export_filter="all_resources"
include_filter=""
exclude_filter=""
export_path="../Export/Web/Kalulu.html"
export_path="../Export/web/Kalulu.html"
patches=PackedStringArray()
encryption_include_filters=""
encryption_exclude_filters=""
+2 -2
View File
@@ -11,7 +11,7 @@ config_version=5
[application]
config/name="Kalulu"
config/version="2.1.2"
config/version="2.1.3"
run/main_scene="res://sources/menus/splash_screen/splash_screen.tscn"
config/features=PackedStringArray("4.4", "Forward Plus")
boot_splash/bg_color=Color(0.141176, 0.141176, 0.141176, 1)
@@ -54,7 +54,7 @@ window/stretch/mode="canvas_items"
[editor_plugins]
enabled=PackedStringArray("res://addons/EnvironmentSwitcher/plugin.cfg", "res://addons/export_tool_manager/plugin.cfg", "res://addons/godot-form-validator/plugin.cfg", "res://addons/godot-sqlite/plugin.cfg")
enabled=PackedStringArray("res://addons/EnvironmentSwitcher/plugin.cfg", "res://addons/export_tool_manager/plugin.cfg", "res://addons/godot-form-validator/plugin.cfg", "res://addons/godot-sqlite/plugin.cfg", "res://addons/script-ide/plugin.cfg")
[file_customization]
+1
View File
@@ -25,6 +25,7 @@ const SUPPORTED_LOCALES: Array[String] = [
@export var teacher: String
@export var device_id: int
@export var language_versions: Dictionary = {} # locale: datetime
@export var game_version: String = "0.0.1"
@export var master_volume: float = 0.0:
set(volume):
+6 -12
View File
@@ -239,8 +239,7 @@ func _ready() -> void:
starting_garden = 0
scroll_container.scroll_horizontal = GARDEN_SIZE * starting_garden
@warning_ignore("integer_division")
scroll_beginning_garden = scroll_container.scroll_horizontal / GARDEN_SIZE
scroll_beginning_garden = int(float(scroll_container.scroll_horizontal) / GARDEN_SIZE)
current_garden = garden_parent.get_child(starting_garden)
@@ -345,15 +344,13 @@ func _ready() -> void:
# Check if we need to scroll to the next garden
if is_last_lesson_of_garden:
@warning_ignore("integer_division")
scroll_beginning_garden = scroll_container.scroll_horizontal / GARDEN_SIZE
scroll_beginning_garden = int(float(scroll_container.scroll_horizontal) / GARDEN_SIZE)
var target_scroll: int = scroll_beginning_garden * GARDEN_SIZE + GARDEN_SIZE
var tween: Tween = create_tween()
tween.set_ease(Tween.EASE_IN_OUT)
tween.tween_property(scroll_container, "scroll_horizontal", target_scroll, 4)
@warning_ignore("integer_division")
scroll_beginning_garden = target_scroll / GARDEN_SIZE
scroll_beginning_garden = int(float(target_scroll) / GARDEN_SIZE)
current_garden = garden_parent.get_child(scroll_beginning_garden)
@@ -674,8 +671,7 @@ func _on_scroll_container_gui_input(event: InputEvent) -> void:
return
if event.is_action_pressed("left_click"):
is_scrolling = true
@warning_ignore("integer_division")
scroll_beginning_garden = scroll_container.scroll_horizontal / GARDEN_SIZE
scroll_beginning_garden = int(float(scroll_container.scroll_horizontal) / GARDEN_SIZE)
if scroll_tween:
scroll_tween.stop()
scroll_tween = null
@@ -697,13 +693,11 @@ func _on_scroll_container_gui_input(event: InputEvent) -> void:
scroll_tween.set_trans(Tween.TRANS_SPRING)
scroll_tween.tween_property(scroll_container, "scroll_horizontal", target_scroll, 1)
if is_garden_changed:
@warning_ignore("integer_division")
current_garden = garden_parent.get_child(target_scroll / GARDEN_SIZE)
current_garden = garden_parent.get_child(int(float(target_scroll) / GARDEN_SIZE))
current_garden.pop_animation()
await scroll_tween.finished
@warning_ignore("integer_division")
scroll_beginning_garden = scroll_container.scroll_horizontal / GARDEN_SIZE
scroll_beginning_garden = int(float(scroll_container.scroll_horizontal) / GARDEN_SIZE)
if is_scrolling and event is InputEventMouseMotion:
var motion_event: InputEventMouseMotion = event
+1 -2
View File
@@ -41,8 +41,7 @@ func _init_gardens_layout() -> void:
garden_layout = GardenLayout.new()
gardens_layout.gardens.append(garden_layout)
@warning_ignore("integer_division")
garden_layout.color = (index / 4) % GARDEN_TEXTURES_NB
garden_layout.color = int(index / 4.0) % GARDEN_TEXTURES_NB
# Add the flowers to the garden
for flower_i: int in 5:
+20 -10
View File
@@ -46,13 +46,19 @@ func _on_gp_dropped(before: bool, data: Dictionary, gp_label: Control) -> void:
func _can_drop_in_gp_container(_at_position: Vector2, data: Variant) -> bool:
@warning_ignore("unsafe_method_access")
return data.has("gp_id")
if data is Dictionary:
return (data as Dictionary).has("gp_id")
else:
Logger.error("LessonContainer: Can not drop data (that is not of type Dictionary) in GP Container")
return false
func _drop_data_in_gp_container(_at_position: Vector2, data: Variant) -> void:
@warning_ignore("unsafe_method_access")
if not data.has("gp_id"):
if not data is Dictionary:
Logger.error("LessonContainer: Cancel drop data in GP container because data is not of type Dictionary")
return
if not (data as Dictionary).has("gp_id"):
Logger.trace("LessonContainer: Cancel drop data in GP container because data has no key gp_id")
return
var new_gp_label: LessonGPLabel = gp_label_scene.instantiate()
new_gp_label.grapheme = data.grapheme
@@ -73,16 +79,20 @@ func _get_drag_data(_at_position: Vector2) -> Variant:
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
@warning_ignore("unsafe_method_access")
return (data.has("number") and number != data.number) or _can_drop_in_gp_container(at_position, data)
if data is Dictionary:
return ((data as Dictionary).has("number") and number != (data as Dictionary).number) or _can_drop_in_gp_container(at_position, data)
else:
Logger.error("LessonContainer: Can not drop data that is not of type Dictionary")
return false
func _drop_data(at_position: Vector2, data: Variant) -> void:
@warning_ignore("unsafe_method_access")
if not (data.has("number") or data.has("gp_id")):
if not data is Dictionary:
Logger.error("LessonContainer: drop data failed because data is not of type Dictionary")
return
@warning_ignore("unsafe_method_access")
if data.has("number"):
if not (data as Dictionary).has("number") or (data as Dictionary).has("gp_id"):
return
if (data as Dictionary).has("number"):
var before: bool = at_position.y < size.y
lesson_dropped.emit(before, number, data.number)
else:
+8 -2
View File
@@ -32,11 +32,17 @@ func _get_drag_data(_at_position: Vector2) -> Variant:
func _can_drop_data(_at_position: Vector2, data: Variant) -> bool:
return data.has("gp_id") and data.gp_id != gp_id
if data is Dictionary:
return (data as Dictionary).has("gp_id") and data.gp_id != gp_id
else:
Logger.error("LessonGPLabel: Can not drop data that is not of type Dictionary")
return false
func _drop_data(at_position: Vector2, data: Variant) -> void:
if not data.has("gp_id"):
if not data is Dictionary:
Logger.error("LessonGPLabel: drop data failed because data is not of type Dictionary")
if not (data as Dictionary).has("gp_id"):
return
var before: bool = at_position.x < size.x / 2
gp_dropped.emit(before, data)
+3 -6
View File
@@ -472,8 +472,7 @@ func _create_words_csv() -> void:
var index: int = graphemes.size() - 1
gpmatch += graphemes[index] + "-" + phonemes[index] + ")"
var lesson: int = -1
@warning_ignore("unsafe_method_access")
for gp_id: String in element.GPIDs.split(' '):
for gp_id: String in (element.GPIDs as String).split(' '):
var gp_id_lesson: int = Database.get_min_lesson_for_gp_id(int(gp_id))
if gp_id_lesson < 0:
lesson = -1
@@ -501,8 +500,7 @@ func _create_syllable_csv() -> void:
var last_index: int = graphemes.size() - 1
gpmatch += graphemes[last_index] + "-" + phonemes[last_index] + ")"
var lesson: int = -1
@warning_ignore("unsafe_method_access")
for gp_id: String in element.GPIDs.split(' '):
for gp_id: String in (element.GPIDs as String).split(' '):
var gp_id_lesson: int = Database.get_min_lesson_for_gp_id(int(gp_id))
if gp_id_lesson < 0:
lesson = -1
@@ -523,8 +521,7 @@ func _create_sentence_csv() -> void:
var result: Array[Dictionary] = Database.db.query_result
for element: Dictionary in result:
var lesson: int = -1
@warning_ignore("unsafe_method_access")
for word_id: String in element.WordIDs.split(' '):
for word_id: String in (element.WordIDs as String).split(' '):
var ind: int = Database.get_min_lesson_for_word_id(int(word_id))
if ind < 0:
lesson = -1
+3 -3
View File
@@ -187,10 +187,10 @@ text = "Open language resource Folder"
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 0
offset_left = 85.0
offset_left = 50.0
offset_top = 117.0
offset_right = 626.0
offset_bottom = 515.0
offset_right = 667.0
offset_bottom = 547.0
theme_override_constants/separation = 50
[node name="ImportLanguageButton" type="Button" parent="VBoxContainer"]
+5 -5
View File
@@ -1,4 +1,4 @@
extends "res://sources/language_tool/word_list.gd"
extends WordList
var not_found_list: String = ""
@onready var export_not_found_button: Button = %ExportNotFoundButton
@@ -28,7 +28,7 @@ func get_lesson_for_element(id: int) -> int:
func _on_list_title_new_search(new_text: String) -> void:
for element: SentenceListElement in elements_container.get_children():
for element: WordListElement in elements_container.get_children():
var found: bool = false
for word: String in element.word.split(" "):
if word.begins_with(new_text):
@@ -43,9 +43,9 @@ func _ready() -> void:
func connect_not_found() -> void:
for element: SentenceListElement in elements_container.get_children():
if not element.not_found.is_connected(_on_not_found):
element.not_found.connect(_on_not_found)
for element: WordListElement in elements_container.get_children():
if element is SentenceListElement and not (element as SentenceListElement).not_found.is_connected(_on_not_found):
(element as SentenceListElement).not_found.connect(_on_not_found)
if _element is SentenceListElement and not (_element as SentenceListElement).not_found.is_connected(_on_not_found_csv):
(_element as SentenceListElement).not_found.connect(_on_not_found_csv)
+3 -1
View File
@@ -1,11 +1,13 @@
[gd_scene load_steps=4 format=3 uid="uid://dbmcpgfokf0in"]
[gd_scene load_steps=5 format=3 uid="uid://dbmcpgfokf0in"]
[ext_resource type="PackedScene" uid="uid://dfw7m0a7148fu" path="res://sources/language_tool/word_list.tscn" id="1_bfjhw"]
[ext_resource type="Script" uid="uid://7pymixi5vsat" path="res://sources/language_tool/sentence_list.gd" id="2_q3ll5"]
[ext_resource type="PackedScene" uid="uid://dx33ijlgd8m3w" path="res://sources/language_tool/word_list_element.tscn" id="3_jl0ks"]
[ext_resource type="PackedScene" uid="uid://cy14lm72cd7cy" path="res://sources/language_tool/sentence_list_element.tscn" id="3_ys88k"]
[node name="SentenceList" instance=ExtResource("1_bfjhw")]
script = ExtResource("2_q3ll5")
element_scene = ExtResource("3_ys88k")
[node name="ExportNotFoundButton" type="Button" parent="VBoxContainer" index="5"]
unique_name_in_owner = true
+24 -7
View File
@@ -5,7 +5,7 @@ class_name WordList
@onready var elements_container: VBoxContainer = %ElementsContainer
@onready var new_gp_layer: CanvasLayer = $NewGPLayer
@onready var new_gp: GPListElement = %NewGP
@onready var new_gp: Variant = %NewGP
@onready var title: ListTitle = %ListTitle
@onready var lesson_title: Label = %Lesson
@onready var word_title: Label = %Word
@@ -131,7 +131,12 @@ func _on_element_new_gp_asked(ind: int, element: WordListElement) -> void:
in_new_gp_mode = true
new_gp_asked_element = element
new_gp_asked_ind = ind
new_gp.edit_mode()
if new_gp is WordListElement:
(new_gp as WordListElement).edit_mode()
elif new_gp is GPListElement:
(new_gp as GPListElement).edit_mode()
else:
Logger.error("WordList: Variable new_gp is of unknown type %s" % type_string(typeof(new_gp)))
func set_in_new_gp_mode(p_in_new_gp_mode: bool) -> void:
@@ -140,15 +145,27 @@ func set_in_new_gp_mode(p_in_new_gp_mode: bool) -> void:
func _on_gp_list_element_validated() -> void:
new_gp.insert_in_database()
if new_gp.has_method("update_lesson"):
@warning_ignore("unsafe_method_access")
new_gp.update_lesson()
if new_gp is WordListElement:
(new_gp as WordListElement).insert_in_database()
elif new_gp is GPListElement:
(new_gp as GPListElement).insert_in_database()
else:
Logger.error("WordList: Variable new_gp is of unknown type %s" % type_string(typeof(new_gp)))
if new_gp is Object:
if (new_gp as Object).has_method("update_lesson"):
if new_gp is WordListElement:
(new_gp as WordListElement).update_lesson()
elif new_gp is SentenceListElement:
(new_gp as SentenceListElement).update_lesson()
else:
Logger.error("WordList: Variable new_gp is of unknown type %s" % type_string(typeof(new_gp)))
else:
Logger.error("WordList: Variable new_gp is of unknown type %s" % type_string(typeof(new_gp)))
in_new_gp_mode = false
create_sub_elements_list()
for element: WordListElement in elements_container.get_children():
element.sub_elements_list = sub_elements_list
new_gp_asked_element.new_gp_asked_added(new_gp_asked_ind, new_gp.id)
new_gp_asked_element.new_gp_asked_added(new_gp_asked_ind, new_gp.id as int)
func _on_gps_updated() -> void:
+3 -6
View File
@@ -116,15 +116,13 @@ func add_gp_list_button(gp_id: int, ind_gp_id: int) -> void:
func _on_gp_list_button_selected(gp_id: int, element: Node) -> void:
@warning_ignore("integer_division")
var ind_gp_id: int = element.get_index() / 2
var ind_gp_id: int = int(element.get_index() / 2.0)
unvalidated_gp_ids[ind_gp_id] = gp_id
word_edit.text = get_graphemes(unvalidated_gp_ids)
func _on_gp_list_button_new_selected(element: Node) -> void:
@warning_ignore("integer_division")
var ind_gp_id: int = element.get_index() / 2
var ind_gp_id: int = int(element.get_index() / 2.0)
new_gp_asked.emit(ind_gp_id)
@@ -363,8 +361,7 @@ func new_gp_asked_added(ind: int, gp_id: int) -> void:
func _on_add_gp_button_pressed(element: Node) -> void:
@warning_ignore("integer_division")
var ind_gp_id: int = element.get_index() / 2
var ind_gp_id: int = int(element.get_index() / 2.0)
var gp_id: int = sub_elements_list.keys()[0]
unvalidated_gp_ids.insert(ind_gp_id + 1, gp_id)
add_gp_list_button(gp_id, ind_gp_id + 1)
@@ -117,10 +117,8 @@ func is_language_directory_valid(path: String) -> bool:
func _process(_delta: float) -> void:
if http_request.get_body_size() > 0:
@warning_ignore("integer_division")
var maximum: int = int(http_request.get_body_size()/1024)
@warning_ignore("integer_division")
var current: int = int(http_request.get_downloaded_bytes()/1024)
var maximum: int = int(http_request.get_body_size()/1024.0)
var current: int = int(http_request.get_downloaded_bytes()/1024.0)
download_bar.max_value = maximum
download_bar.value = current
download_info.text = str(current) + "KB/" + str(maximum) + "KB"
+2 -4
View File
@@ -244,10 +244,8 @@ func _on_word_answer(stimulus: String, expected_stimulus: String, word: TextureB
current_progression += 1
else:
for index: int in range(words.get_child_count() - 1):
@warning_ignore("UNSAFE_METHOD_ACCESS")
words.get_child(index).wrong()
@warning_ignore("UNSAFE_METHOD_ACCESS")
await words.get_child(words.get_child_count() - 1).wrong()
(words.get_child(index) as Word).wrong()
await (words.get_child(words.get_child_count() - 1) as Word).wrong()
current_lives -= 1
+2 -3
View File
@@ -30,9 +30,8 @@ func _process(_delta: float) -> void:
if follow_mouse:
global_position = get_global_mouse_position() - size / 2.0
else:
if current_anchor is Area2D:
@warning_ignore("UNSAFE_PROPERTY_ACCESS")
global_position = current_anchor.anchor.global_position
if current_anchor is Ant:
global_position = (current_anchor as Ant).anchor.global_position
else:
@warning_ignore("UNSAFE_PROPERTY_ACCESS")
global_position = current_anchor.global_position
+2 -2
View File
@@ -400,7 +400,6 @@ func set_current_progression(p_current_progression: int) -> void:
if p_current_progression == max_progression and previous_progression != max_progression:
await _win()
else:
@warning_ignore("redundant_await")
await _on_current_progression_changed()
#endregion
@@ -436,6 +435,7 @@ func _on_minigame_ui_restart_button_pressed() -> void:
func _on_current_progression_changed() -> void:
pass
# Make Godot understands that this function is a coroutine even if it does nothing, to avoid warning
await get_tree().create_timer(0).timeout
#endregion
@@ -69,8 +69,7 @@ func _find_stimuli_and_distractions() -> void:
stimuli.append_array(current_lesson_stimuli)
# If there are not enough stimuli from current lesson, we want at least half the target number of stimuli
@warning_ignore("integer_division")
var minimal_stimuli: int = current_lesson_stimuli_number/2
var minimal_stimuli: int = floori(current_lesson_stimuli_number / 2.0)
if stimuli.size() < minimal_stimuli:
while stimuli.size() < minimal_stimuli:
stimuli.append(current_lesson_stimuli.pick_random())
@@ -58,8 +58,7 @@ func _setup_minigame() -> void:
branches_spawn_indexes.append(index)
# Move the caterpillar to the right branch
@warning_ignore("integer_division")
if index == int(settings.branches/2):
if index == int(settings.branches/2.0):
_on_branch_pressed(branch)
# Setups the timer
+1 -1
View File
@@ -16,7 +16,7 @@
[node name="FrogMinigame" instance=ExtResource("1_ng5er")]
script = ExtResource("2_0xg3r")
max_number_of_GPs = 6
max_number_of_gps = 6
distractors_queue_size = 6
time_between_words = 3.0
minigame_name = 5
+41 -4
View File
@@ -35,8 +35,47 @@ func _ready() -> void:
if get_device_settings().teacher:
_load_teacher_settings()
purge_user_folders_if_needed()
user_database_synchronizer = UserDatabaseSynchronizer.new()
func purge_user_folders_if_needed() -> void:
var current_version: String = ProjectSettings.get_setting("application/config/version")
var previous_version: String = _device_settings.game_version
if previous_version == "" or compare_versions(previous_version, "2.1.3") < 0:
Logger.trace("UserDataManager: Version difference detected, need to purge user folder to avoid data incompatibility")
var dir: DirAccess = DirAccess.open("user://")
if dir:
dir.list_dir_begin()
var file_name: String = dir.get_next()
while file_name != "":
if dir.current_is_dir() and file_name != "." and file_name != "..":
var full_path: String = "user://".path_join(file_name)
_delete_dir(full_path)
var err: Error = DirAccess.remove_absolute(full_path)
if err != OK:
Logger.error("UserDataManager: Failed to delete folder %s. Error %s" % [full_path, error_string(err)])
file_name = dir.get_next()
dir.list_dir_end()
Logger.trace("UserDataManager: Purge completed")
_device_settings.game_version = current_version
ResourceSaver.save(_device_settings, "user://device_settings.tres")
func compare_versions(version_a: String, version_b: String) -> int:
var va: PackedStringArray = version_a.split(".")
var vb: PackedStringArray = version_b.split(".")
for index: int in 3:
var ai: int = int(va[index]) if index < va.size() else 0
var bi: int = int(vb[index]) if index < vb.size() else 0
if ai < bi:
return -1
elif ai > bi:
return 1
return 0
func _process(_delta: float) -> void:
if Engine.is_editor_hint():
@@ -694,15 +733,13 @@ func move_user_device_folder(old_device: String, new_device: String, student_cod
save_teacher_settings()
func find_student_dir(student_code: int) -> String:
@warning_ignore("unused_parameter")
return _scan_teacher_devices(func(device_dir: String, lang_dir: String, sub_file: String) -> String:
return _scan_teacher_devices(func(_device_dir: String, lang_dir: String, sub_file: String) -> String:
if sub_file == str(student_code):
return lang_dir.path_join(sub_file)
return "")
func find_device_dir_for_student(student_code: int) -> String:
@warning_ignore("unused_parameter")
return _scan_teacher_devices(func(device_dir: String, lang_dir: String, sub_file: String) -> String:
return _scan_teacher_devices(func(device_dir: String, _lang_dir: String, sub_file: String) -> String:
if sub_file == str(student_code):
return device_dir
return "")
@@ -389,8 +389,8 @@ func _apply_server_response(response_body: Dictionary) -> void:
@warning_ignore("unsafe_call_argument")
new_unlock_data[int(key_lesson)] = {"games": [], "look_and_learn": int(received_unlock_data[key_lesson]["look_and_learn"])}
for game_result: Variant in received_unlock_data[key_lesson]["games"]:
@warning_ignore("unsafe_call_argument", "unsafe_method_access")
new_unlock_data[int(key_lesson)]["games"].push_back(int(game_result))
@warning_ignore("unsafe_call_argument")
(new_unlock_data[int(key_lesson)]["games"] as Array).push_back(int(game_result))
UserDataManager.set_student_progression_data(int(response_student_code), response_student_data.progression.version as String, new_unlock_data, response_student_data.progression.updated_at as String)
if response_student_data.has("remediation_gp") && (response_student_data.remediation_gp as Dictionary).has("score_remediation") && (response_student_data.remediation_gp as Dictionary).has("updated_at"):
# TODO ADD SECURITY
+1 -2
View File
@@ -43,8 +43,7 @@ func _calculate_best_font_size() -> int:
var best_size: int = low
while low <= high:
@warning_ignore("integer_division")
var mid: int = int((low + high) / 2)
var mid: int = int((low + high) / 2.0)
var text_size: Vector2 = font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, mid)
# Si le texte rentre, on essaie de l'augmenter
+1 -2
View File
@@ -11,6 +11,5 @@ func _process(_delta: float) -> void:
if OS.has_feature("mobile"):
var margin: int = DisplayServer.virtual_keyboard_get_height()
if OS.get_name() == "Android":
@warning_ignore("narrowing_conversion")
margin *= screen_scale
margin = int(margin * screen_scale)
self["theme_override_constants/margin_bottom"] = maxi(floori(margin), 0)