Files
idle/generator-unlock-goals/generator_unlock_system.gd
Michele Rossi 1041fc917d Add goals
2026-03-14 11:12:05 +01:00

212 lines
7.1 KiB
GDScript

extends Node
class UnlockGoalRequirement extends RefCounted:
var currency: GameState.CurrencyType
var amount: BigNumber
func _init(req_currency: GameState.CurrencyType, req_amount: BigNumber) -> void:
currency = req_currency
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)
_evaluate_all_goals()
func _on_currency_changed(_changed_currency: GameState.CurrencyType, _new_amount: BigNumber) -> void:
_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)
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_value: int = _parse_currency(requirement_dict.get("currency", ""))
if currency_value < 0:
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_value, amount)
func _parse_currency(raw_currency: Variant) -> int:
if raw_currency is int:
var currency_int: int = raw_currency
if currency_int == GameState.CurrencyType.gold or currency_int == GameState.CurrencyType.gems:
return currency_int
return -1
var currency_text: String = String(raw_currency).to_lower().strip_edges()
match currency_text:
"gold":
return GameState.CurrencyType.gold
"gems", "gem":
return GameState.CurrencyType.gems
_:
return -1
func _get_total_currency_amount(currency: GameState.CurrencyType) -> BigNumber:
return GameState.get_total_currency_acquired(currency)
func _collect_known_generator_ids() -> void:
_known_generator_ids.clear()
var generator_nodes: Array[Node] = 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