Files
Kalulu/resources/user/student_progression.gd
T
2025-10-06 16:30:25 +02:00

214 lines
6.9 KiB
GDScript

class_name StudentProgression
extends Resource
signal unlocks_changed()
enum Status{
Locked,
Unlocked,
Completed,
}
@export var version: String = ProjectSettings.get_setting("application/config/version")
@export var unlocks: Dictionary = {}:
set(value):
unlocks = check_data_integrity(value)
@export var level_times: Dictionary[int, PackedInt32Array] = {} # Level ID, Last number of seconds for minigames 0, 1 and 2
@export var level_total_times: Dictionary[int, PackedInt32Array] = {} # Level ID, Total number of seconds for minigames 0, 1 and 2
@export var last_modified: String
func _init() -> void:
init_unlocks()
# Make sure the unlocks are correct
func init_unlocks() -> void:
if not unlocks:
unlocks = {}
# Verify the lessons
var number_of_lessons: int = Database.get_lessons_count()
if unlocks.size() != number_of_lessons:
for index: int in range(number_of_lessons):
if not unlocks.has(index+1):
unlocks[index + 1] = {
"look_and_learn": Status.Locked,
"games": [
Status.Locked,
Status.Locked,
Status.Locked,
]
}
while unlocks.size() > number_of_lessons:
unlocks.erase(unlocks.size())
# Make sure that the first garden is always accessible
if unlocks[1]["look_and_learn"] == Status.Locked:
unlocks[1]["look_and_learn"] = Status.Unlocked
func check_data_integrity(data: Dictionary) -> Dictionary:
var result: Dictionary = data.duplicate(true)
# Check missin keys
var min_key: int = 1
var max_key: int = 1
if result.size() > 0:
if result.keys()[0] is int:
min_key = result.keys().min() as int
max_key = result.keys().max() as int
else:
Log.error("StudentProgression: Received keys with wrong datatype")
for index: int in range(min_key, max_key + 1):
if not result.has(index):
Log.warn("StudentProgression: Garden %d missing → added with default values." % index)
result[index] = {
"games": [0, 0, 0],
"look_and_learn": 0,
"last_duration": 0.0,
"total_duration": 0.0
}
# Check internal structure
for index: int in result.keys():
var garden: Dictionary = result[index]
# Check missing keys
if not garden.has("games"):
Log.warn("Garden %d : Add missing key 'games'." % index)
garden["games"] = [0, 0, 0]
if not garden.has("look_and_learn"):
Log.warn("Garden %d : Add missing key 'look_and_learn'." % index)
garden["look_and_learn"] = 0
if not garden.has("last_duration"):
garden["last_duration"] = 0.0
if not garden.has("total_duration"):
garden["total_duration"] = 0.0
# Check array "games"
if typeof(garden["games"]) != TYPE_ARRAY or (garden["games"] as Array).size() != 3:
Log.warn("Garden %d : invalid format for 'games' → reset." % index)
garden["games"] = [0, 0, 0]
# Check value outside of 0/1/2
for game_index: int in range(3):
if garden["games"][game_index] not in [0, 1, 2]:
garden["games"][game_index] = 0
if garden["look_and_learn"] not in [0, 1, 2]:
garden["look_and_learn"] = 0
# Check progression rules
for index: int in range(min_key, max_key + 1):
var garden: Dictionary = result[index]
var prev_completed: bool = false
# Check previous garden is completed
if result.has(index - 1):
var prev: Dictionary = result[index - 1]
prev_completed = (
prev["look_and_learn"] == 2 and
(prev["games"] as Array).all(func(x: int) -> bool: return x == 2)
)
else:
# First garden (key 1) is always unlocked
prev_completed = true
# Case : previous garden not completed
if not prev_completed:
for game_index: int in range(3):
if garden["games"][game_index] != 0 or garden["look_and_learn"] != 0:
Log.warn("Garden %d : invalid progression (previous not finished) → reset." % index)
garden["games"] = [0, 0, 0]
garden["look_and_learn"] = 0
break
continue
# Case : lesson completed → games unlocked if needed
if garden["look_and_learn"] == 2:
for game_index: int in range(3):
if garden["games"][game_index] == 0:
garden["games"][game_index] = 1
Log.warn("Garden %d : game %d unlocked because lesson is completed" % [index, game_index + 1])
# Case : previous garden completed → lesson unlocked if needed
elif garden["look_and_learn"] == 0:
garden["look_and_learn"] = 1
Log.warn("Garden %d : lesson unlocked because previous garden is completed" % index)
return result
func get_max_unlocked_lesson_index() -> int:
var max_unlocked_level: int = 1
for index: int in range(unlocks.size()):
if unlocks[index + 1]["look_and_learn"] >= Status.Unlocked:
max_unlocked_level = index
else:
break
return max_unlocked_level
func is_lesson_completed(lesson_number: int) -> bool:
return unlocks[lesson_number]["look_and_learn"] == Status.Completed and unlocks[lesson_number]["games"][0] == Status.Completed and unlocks[lesson_number]["games"][1] == Status.Completed and unlocks[lesson_number]["games"][2] == Status.Completed
# Return true if the progression is saved or false if the look and learn was already completed
func look_and_learn_completed(lesson_number: int) -> bool:
if unlocks[lesson_number]["look_and_learn"] == Status.Completed:
return false
unlocks[lesson_number]["look_and_learn"] = Status.Completed
for index: int in range(3):
unlocks[lesson_number]["games"][index] = Status.Unlocked
last_modified = Time.get_datetime_string_from_system(true)
unlocks_changed.emit()
return true
# Return true if the progression is saved or false if the game was already completed
func game_completed(lesson_number: int, game_number: int) -> bool:
# If the game is already completed, do nothing
if unlocks[lesson_number]["games"][game_number] == Status.Completed:
return false
unlocks[lesson_number]["games"][game_number] = Status.Completed
var all_completed: bool = true
for index: int in range(3):
all_completed = all_completed and unlocks[lesson_number]["games"][index] == Status.Completed
if all_completed and unlocks.has(lesson_number + 1):
unlocks[lesson_number + 1]["look_and_learn"] = Status.Unlocked
last_modified = Time.get_datetime_string_from_system(true)
unlocks_changed.emit()
return true
func add_level_time(lesson_number: int, game_number: int, time_spent: int) -> void:
Log.trace("StudentProgression: Add time to level %d, minigame %d. Time added: %s" % [lesson_number, game_number, time_spent])
if game_number > 2:
Log.error("StudentProgression: Cannot log a level time for a minigame number superior to 2")
return
if level_times.has(lesson_number):
if level_times[lesson_number].size() != 3:
level_times[lesson_number] = [0, 0, 0]
else:
level_times.set(lesson_number, PackedInt32Array([0, 0, 0]))
level_times[lesson_number][game_number] = time_spent
if level_total_times.has(lesson_number):
if level_total_times[lesson_number].size() != 3:
level_total_times[lesson_number] = PackedInt32Array([0, 0, 0])
else:
level_total_times.set(lesson_number, PackedInt32Array([0, 0, 0]))
level_total_times[lesson_number][game_number] += time_spent
last_modified = Time.get_datetime_string_from_system(true)