extends Node class UnlockGoalRequirement extends RefCounted: var currency_id: StringName var amount: BigNumber func _init(req_currency_id: StringName, req_amount: BigNumber) -> void: currency_id = req_currency_id amount = req_amount class UnlockGoal extends RefCounted: var id: StringName var target_generator_id: StringName var requirements: Array[UnlockGoalRequirement] func _init(goal_id: StringName, target_id: StringName, goal_requirements: Array[UnlockGoalRequirement]) -> void: id = goal_id target_generator_id = target_id requirements = goal_requirements const GOALS_FILE_PATH: String = "res://generator-unlock-goals/generator_unlock_goals.json" const SCHEMA_VERSION: int = 1 var _goals: Array[UnlockGoal] = [] var _known_generator_ids: Dictionary = {} var _warned_unknown_target_ids: Dictionary = {} func _ready() -> void: _collect_known_generator_ids() _load_goals() GameState.currency_changed.connect(_on_currency_changed) GameState.generator_state_changed.connect(_on_generator_state_changed) _evaluate_all_goals() _evaluate_all_goals.call_deferred() func _on_currency_changed(_changed_currency_id: StringName, _new_amount: BigNumber) -> void: _evaluate_all_goals() func _on_generator_state_changed(generator_id: StringName, _state: Dictionary) -> void: var generator_key: String = String(generator_id) if not generator_key.is_empty(): _known_generator_ids[generator_key] = true _evaluate_all_goals() func _evaluate_all_goals() -> void: if _goals.is_empty(): return for goal in _goals: _evaluate_goal(goal) func _evaluate_goal(goal: UnlockGoal) -> bool: if _is_goal_completed(goal): return false if not _can_resolve_target(goal.target_generator_id): return false for requirement in goal.requirements: var total_amount: BigNumber = _get_total_currency_amount(requirement.currency_id) if total_amount.is_less_than(requirement.amount): return false GameState.set_generator_unlocked(goal.target_generator_id, true) GameState.set_generator_available(goal.target_generator_id, true) print("[UnlockSystem] Goal completed: %s -> %s" % [String(goal.id), String(goal.target_generator_id)]) return true func _is_goal_completed(goal: UnlockGoal) -> bool: return ( GameState.is_generator_unlocked(goal.target_generator_id) and GameState.is_generator_available(goal.target_generator_id) ) func _can_resolve_target(target_generator_id: StringName) -> bool: var target_key: String = String(target_generator_id) if target_key.is_empty(): return false if _known_generator_ids.has(target_key) or GameState.generator_states.has(target_key): return true if not _warned_unknown_target_ids.has(target_key): _warned_unknown_target_ids[target_key] = true push_warning("Unlock goal target generator not found in scene/state: '%s'" % target_key) return false func _load_goals() -> void: _goals.clear() if not FileAccess.file_exists(GOALS_FILE_PATH): push_warning("Unlock goals file not found: %s" % GOALS_FILE_PATH) return var file: FileAccess = FileAccess.open(GOALS_FILE_PATH, FileAccess.READ) if file == null: push_warning("Unable to open unlock goals file: %s" % GOALS_FILE_PATH) return var parsed: Variant = JSON.parse_string(file.get_as_text()) if not (parsed is Dictionary): push_warning("Unlock goals root must be a Dictionary: %s" % GOALS_FILE_PATH) return var root: Dictionary = parsed var version: int = int(root.get("version", SCHEMA_VERSION)) if version != SCHEMA_VERSION: push_warning("Unlock goals schema version %d is different from expected %d" % [version, SCHEMA_VERSION]) var raw_goals: Variant = root.get("goals", []) if not (raw_goals is Array): push_warning("Unlock goals file has invalid 'goals' field (expected Array)") return var seen_goal_ids: Dictionary = {} for raw_goal in raw_goals: var goal: UnlockGoal = _parse_goal(raw_goal) if goal == null: continue var goal_key: String = String(goal.id) if seen_goal_ids.has(goal_key): push_warning("Duplicate unlock goal id skipped: '%s'" % goal_key) continue seen_goal_ids[goal_key] = true _goals.append(goal) if _goals.is_empty(): push_warning("Unlock goals loaded, but no valid entries were found.") func _parse_goal(raw_goal: Variant) -> UnlockGoal: if not (raw_goal is Dictionary): push_warning("Skipping unlock goal: entry is not a Dictionary") return null var goal_dict: Dictionary = raw_goal var goal_id: String = String(goal_dict.get("id", "")).strip_edges() var target_id: String = String(goal_dict.get("target_generator_id", "")).strip_edges() if goal_id.is_empty(): push_warning("Skipping unlock goal: missing 'id'") return null if target_id.is_empty(): push_warning("Skipping unlock goal '%s': missing 'target_generator_id'" % goal_id) return null var raw_requirements: Variant = goal_dict.get("requirements", []) if not (raw_requirements is Array): push_warning("Skipping unlock goal '%s': 'requirements' must be an Array" % goal_id) return null var parsed_requirements: Array[UnlockGoalRequirement] = [] for raw_requirement in raw_requirements: var requirement: UnlockGoalRequirement = _parse_requirement(raw_requirement, goal_id) if requirement != null: parsed_requirements.append(requirement) if parsed_requirements.is_empty(): push_warning("Skipping unlock goal '%s': no valid requirements" % goal_id) return null return UnlockGoal.new(StringName(goal_id), StringName(target_id), parsed_requirements) func _parse_requirement(raw_requirement: Variant, goal_id: String) -> UnlockGoalRequirement: if not (raw_requirement is Dictionary): push_warning("Skipping requirement in goal '%s': entry is not a Dictionary" % goal_id) return null var requirement_dict: Dictionary = raw_requirement var currency_id: StringName = _parse_currency(requirement_dict.get("currency", "")) if currency_id == &"" or not GameState.is_known_currency_id(currency_id): push_warning("Skipping requirement in goal '%s': unknown currency '%s'" % [goal_id, String(requirement_dict.get("currency", ""))]) return null var raw_amount: Variant = requirement_dict.get("amount", {}) if not (raw_amount is Dictionary): push_warning("Skipping requirement in goal '%s': 'amount' must be a Dictionary" % goal_id) return null var amount: BigNumber = BigNumber.deserialize(raw_amount) if amount.mantissa < 0.0: push_warning("Skipping requirement in goal '%s': amount must be non-negative" % goal_id) return null return UnlockGoalRequirement.new(currency_id, amount) func _parse_currency(raw_currency: Variant) -> StringName: return GameState.parse_currency_id(raw_currency) func _get_total_currency_amount(currency_id: StringName) -> BigNumber: return GameState.get_total_currency_acquired_by_id(currency_id) func _collect_known_generator_ids() -> void: _known_generator_ids.clear() var scene_root: Node = get_tree().current_scene var search_root: Node = scene_root if scene_root != null else self var generator_nodes: Array[Node] = search_root.find_children("*", "CurrencyGenerator", true, false) for generator_node in generator_nodes: var generator: CurrencyGenerator = generator_node as CurrencyGenerator if generator == null: continue var generator_key: String = String(generator.get_generator_id()) if generator_key.is_empty(): continue _known_generator_ids[generator_key] = true