class_name ProgressionService extends RefCounted ## Monotonic World Tree rank progression with explicit eligibility evidence. ## ## The service owns only progression facts. World expansion and GameState ## mutation are supplied as capture/apply/rollback callbacks so a failed rank-up ## cannot leave the map and recorded rank out of sync. const SNAPSHOT_VERSION := 2 const LEGACY_SNAPSHOT_VERSION := 1 const MIN_RANK := 1 const MAX_RANK := 7 const MAX_COMPLETED_QUESTS := 256 const MAX_PROCESSED_REQUESTS := MAX_RANK - MIN_RANK const MAX_SAFE_INTEGER := 9_007_199_254_740_991 const QUARANTINE_REASONS: Array[StringName] = [ &"RANK_CALLBACK_TAMPERED", &"RANK_ROLLBACK_FAILED", ] const METRIC_IDS: Array[StringName] = [ &"active_above_ground_facilities", &"active_underground_facilities", &"asset_gold", &"faith_lifetime", &"guest_lifetime", &"reputation", &"underground_influence", ] const PERCENT_METRICS: Array[StringName] = [&"reputation", &"underground_influence"] const LIFETIME_METRICS: Array[StringName] = [&"faith_lifetime", &"guest_lifetime"] const DEFAULT_METRICS := { &"active_above_ground_facilities": 0, &"active_underground_facilities": 0, &"asset_gold": 0, &"faith_lifetime": 0, &"guest_lifetime": 0, &"reputation": 0, &"underground_influence": 0, } const RANK_BOUNDS := { 1: {&"min_x": -12, &"max_x": 12, &"min_y": 0, &"max_y": 6}, 2: {&"min_x": -18, &"max_x": 18, &"min_y": -4, &"max_y": 10}, 3: {&"min_x": -24, &"max_x": 24, &"min_y": -8, &"max_y": 16}, 4: {&"min_x": -32, &"max_x": 32, &"min_y": -14, &"max_y": 24}, 5: {&"min_x": -40, &"max_x": 40, &"min_y": -20, &"max_y": 32}, 6: {&"min_x": -48, &"max_x": 48, &"min_y": -28, &"max_y": 42}, 7: {&"min_x": -60, &"max_x": 60, &"min_y": -40, &"max_y": 56}, } var current_rank := MIN_RANK var metrics: Dictionary = DEFAULT_METRICS.duplicate(true) var completed_quest_ids: Array[StringName] = [] var quarantined := false var quarantine_reason: StringName = &"" var last_error := "" var _processed_requests: Dictionary = {} var _transaction_active := false func configure( initial_rank: int = MIN_RANK, initial_metrics: Dictionary = {}, initial_quests: Array = [] ) -> bool: if _transaction_active: return _fail("Progression cannot be configured during a rank transaction.") var candidate := ProgressionService.new() candidate.current_rank = initial_rank if initial_rank != MIN_RANK: return _fail("New progression must start at Rank 1; later ranks require recorded promotions.") if initial_quests.size() > MAX_COMPLETED_QUESTS: return _fail("Completed quest limit exceeded.") if not candidate._replace_metrics(initial_metrics, true): return _fail(candidate.last_error) for raw_quest_id: Variant in initial_quests: if not _is_string_variant(raw_quest_id): return _fail("Quest IDs must be strings.") var quest_id := StringName(raw_quest_id) if not _is_stable_id(String(quest_id)) or candidate.completed_quest_ids.has(quest_id): return _fail("Quest IDs must be unique stable IDs.") candidate.completed_quest_ids.append(quest_id) if candidate.completed_quest_ids.size() > MAX_COMPLETED_QUESTS: return _fail("Completed quest limit exceeded.") candidate.completed_quest_ids.sort_custom(_name_less) _copy_from(candidate) last_error = "" return true func update_metrics(patch: Dictionary) -> bool: if _transaction_active: return _fail("Progression facts cannot change during a rank transaction.") if quarantined: return _fail("Progression is quarantined until a valid snapshot is restored.") if patch.size() > METRIC_IDS.size(): return _fail("Metric patch is too large.") var candidate := metrics.duplicate(true) var seen: Dictionary = {} for raw_metric_id: Variant in patch: if not _is_string_variant(raw_metric_id): return _fail("Metric IDs must be strings.") var metric_id := StringName(raw_metric_id) if not METRIC_IDS.has(metric_id) or seen.has(metric_id): return _fail("Metric patch contains an unknown or duplicate canonical ID.") seen[metric_id] = true var raw_value: Variant = patch[raw_metric_id] if not _is_integer_variant(raw_value): return _fail("Progression metrics must be integers.") var value := int(raw_value) if value < 0 or value > MAX_SAFE_INTEGER \ or (PERCENT_METRICS.has(metric_id) and value > 100): return _fail("Progression metric is outside its valid range.") if LIFETIME_METRICS.has(metric_id) and value < int(metrics[metric_id]): return _fail("Lifetime progression metrics cannot decrease.") candidate[metric_id] = value metrics = candidate last_error = "" return true func complete_quest(quest_id: StringName) -> bool: if _transaction_active: return _fail("Progression facts cannot change during a rank transaction.") if quarantined: return _fail("Progression is quarantined until a valid snapshot is restored.") if not _is_stable_id(String(quest_id)): return _fail("Quest ID must be a stable lowercase ID.") if completed_quest_ids.has(quest_id): last_error = "" return true if completed_quest_ids.size() >= MAX_COMPLETED_QUESTS: return _fail("Completed quest limit exceeded.") completed_quest_ids.append(quest_id) completed_quest_ids.sort_custom(_name_less) last_error = "" return true func evaluate_rank(target_rank: int = -1) -> Dictionary: var target := current_rank + 1 if target_rank < 0 else target_rank var requirements: Array[Dictionary] = [] if target != current_rank + 1 or target < 2 or target > MAX_RANK: return { &"eligible": false, &"current_rank": current_rank, &"target_rank": target, &"requirements": requirements, &"reason": &"TARGET_MUST_BE_NEXT_RANK", } match target: 2: _require_metric(requirements, &"reputation", 25) _require_metric(requirements, &"asset_gold", 10_000) 3: _require_metric(requirements, &"faith_lifetime", 100) _require_metric(requirements, &"guest_lifetime", 300) 4: _require_metric(requirements, &"reputation", 45) _require_metric(requirements, &"asset_gold", 50_000) 5: _require_metric(requirements, &"faith_lifetime", 500) _require_quest(requirements, &"important_rank_5") 6: _require_either_metric( requirements, &"reputation_or_underground_influence", &"reputation", &"underground_influence", 65 ) 7: _require_metric(requirements, &"faith_lifetime", 1_500) _require_metric(requirements, &"reputation", 60) _require_metric(requirements, &"underground_influence", 40) _require_quest(requirements, &"main_chapter_complete") if target >= 4: _require_metric(requirements, &"active_above_ground_facilities", 1) _require_metric(requirements, &"active_underground_facilities", 1) var eligible := not quarantined for requirement: Dictionary in requirements: if not bool(requirement[&"met"]): eligible = false return { &"eligible": eligible, &"current_rank": current_rank, &"target_rank": target, &"requirements": requirements, &"reason": &"OK" if eligible else (&"QUARANTINED" if quarantined else &"REQUIREMENTS_NOT_MET"), &"bounds": get_rank_bounds(target), } func request_rank_up( request_id: StringName, target_rank: int, capture_callback: Callable = Callable(), apply_callback: Callable = Callable(), rollback_callback: Callable = Callable() ) -> CommandResult: if not _is_stable_id(String(request_id)): return _failure(CommandResult.Code.INVALID_PAYLOAD, "Rank request ID must be a stable lowercase ID.") var fingerprint := "rank:%d" % target_rank if _processed_requests.has(request_id): var previous: Dictionary = _processed_requests[request_id] if String(previous[&"fingerprint"]) != fingerprint: return _failure( CommandResult.Code.DUPLICATE_IDEMPOTENCY_KEY, "Rank request ID was already used for different input." ) var replay := CommandResult.succeeded(0, previous[&"details"]) replay.is_replay = true last_error = "" return replay if _transaction_active: return _failure(CommandResult.Code.TRANSACTION_CLOSED, "Another rank transaction is active.") if quarantined: return _failure( CommandResult.Code.TRANSACTION_CLOSED, "Progression is quarantined until a valid snapshot is restored." ) if _processed_requests.size() >= MAX_PROCESSED_REQUESTS: return _failure(CommandResult.Code.VALIDATION_FAILED, "Processed rank request limit exceeded.") var callback_count := int(capture_callback.is_valid()) + int(apply_callback.is_valid()) + int(rollback_callback.is_valid()) if callback_count not in [0, 3]: return _failure( CommandResult.Code.INVALID_PAYLOAD, "External rank mutation requires capture, apply, and rollback callbacks together." ) var eligibility := evaluate_rank(target_rank) if not bool(eligibility[&"eligible"]): return _failure( CommandResult.Code.VALIDATION_FAILED, "World Tree rank requirements are not met.", eligibility ) if callback_count == 3: var internal_before := ProgressionService.new() if not internal_before.restore_snapshot(create_snapshot()): return _failure(CommandResult.Code.TRANSACTION_CLOSED, "Progression could not capture its internal transaction state.") _transaction_active = true var capture_result: Variant = capture_callback.call(current_rank, target_rank) var capture_ok: Variant = ( capture_result.get(&"ok", capture_result.get("ok", null)) if capture_result is Dictionary else null ) if not capture_result is Dictionary \ or not capture_ok is bool \ or not bool(capture_ok) \ or (not capture_result.has(&"snapshot") and not capture_result.has("snapshot")): var capture_tampered := create_snapshot() != internal_before.create_snapshot() _transaction_active = false _copy_from(internal_before) if capture_tampered: quarantined = true quarantine_reason = &"RANK_CALLBACK_TAMPERED" return _failure(CommandResult.Code.TRANSACTION_CLOSED, "Rank capture mutated progression and the external state is uncertain.") return _failure(CommandResult.Code.TRANSACTION_APPLY_FAILED, "Rank mutation capture failed.") if create_snapshot() != internal_before.create_snapshot(): var capture_rollback_result: Variant = rollback_callback.call( capture_result.get(&"snapshot", capture_result.get("snapshot")) ) _transaction_active = false _copy_from(internal_before) if not _callback_succeeded(capture_rollback_result): quarantined = true quarantine_reason = &"RANK_ROLLBACK_FAILED" return _failure( CommandResult.Code.TRANSACTION_CLOSED, "Rank capture mutated progression and rollback could not restore external state." ) return _failure( CommandResult.Code.TRANSACTION_APPLY_FAILED, "Rank capture mutated progression and was rolled back before apply." ) var apply_result: Variant = apply_callback.call(target_rank, get_rank_bounds(target_rank)) var callback_tampered := create_snapshot() != internal_before.create_snapshot() if not _callback_succeeded(apply_result) or callback_tampered: var rollback_result: Variant = rollback_callback.call( capture_result.get(&"snapshot", capture_result.get("snapshot")) ) _transaction_active = false _copy_from(internal_before) if not _callback_succeeded(rollback_result): quarantined = true quarantine_reason = &"RANK_ROLLBACK_FAILED" return _failure( CommandResult.Code.TRANSACTION_CLOSED, "Rank mutation failed and rollback could not restore external state." ) return _failure( CommandResult.Code.TRANSACTION_APPLY_FAILED, "Rank callback mutated progression and was rolled back." if callback_tampered \ else "Rank mutation failed and was rolled back." ) _transaction_active = false current_rank = target_rank var details := { &"rank": current_rank, &"bounds": get_rank_bounds(current_rank), &"eligibility": eligibility.duplicate(true), &"request_id": request_id, } _processed_requests[request_id] = { &"fingerprint": fingerprint, &"details": details.duplicate(true), } last_error = "" return CommandResult.succeeded(0, details) func get_rank_bounds(rank: int = current_rank) -> Dictionary: var source: Variant = RANK_BOUNDS.get(rank, {}) return source.duplicate(true) if source is Dictionary else {} func create_snapshot() -> Dictionary: var requests: Array[Dictionary] = [] var request_ids: Array[StringName] = [] for raw_request_id: Variant in _processed_requests: request_ids.append(StringName(raw_request_id)) request_ids.sort_custom(_name_less) for request_id: StringName in request_ids: var record: Dictionary = _processed_requests[request_id] requests.append({ &"request_id": String(request_id), &"fingerprint": String(record[&"fingerprint"]), &"details": record[&"details"].duplicate(true), }) return { &"version": SNAPSHOT_VERSION, &"current_rank": current_rank, &"metrics": _string_key_dictionary(metrics), &"completed_quest_ids": _names_to_strings(completed_quest_ids), &"processed_requests": requests, &"quarantined": quarantined, &"quarantine_reason": String(quarantine_reason), } func restore_snapshot(snapshot: Dictionary) -> bool: if _transaction_active: return _fail("Progression cannot restore during a rank transaction.") if not _keys_match(snapshot, [ &"completed_quest_ids", &"current_rank", &"metrics", &"processed_requests", &"quarantine_reason", &"quarantined", &"version", ]): return _fail("Progression snapshot root fields are invalid.") var version_value: Variant = snapshot.get(&"version", null) if not _is_integer_variant(version_value) \ or int(version_value) not in [LEGACY_SNAPSHOT_VERSION, SNAPSHOT_VERSION]: return _fail("Unsupported Progression snapshot version.") var snapshot_version := int(version_value) if not snapshot.get(&"metrics", null) is Dictionary \ or not snapshot.get(&"completed_quest_ids", null) is Array \ or not snapshot.get(&"processed_requests", null) is Array \ or not snapshot.get(&"quarantined", null) is bool \ or not _is_string_variant(snapshot.get(&"quarantine_reason", null)): return _fail("Progression snapshot shape is invalid.") var rank_value: Variant = snapshot.get(&"current_rank", null) if not _is_integer_variant(rank_value): return _fail("Progression snapshot rank is invalid.") var candidate := ProgressionService.new() candidate.current_rank = int(rank_value) if candidate.current_rank < MIN_RANK or candidate.current_rank > MAX_RANK: return _fail("Progression snapshot rank is outside 1 through 7.") if not candidate._replace_metrics(snapshot[&"metrics"], false): return _fail(candidate.last_error) if snapshot[&"completed_quest_ids"].size() > MAX_COMPLETED_QUESTS: return _fail("Progression snapshot quest limit exceeded.") for raw_quest_id: Variant in snapshot[&"completed_quest_ids"]: if not _is_string_variant(raw_quest_id): return _fail("Progression snapshot quest ID is invalid.") var quest_id := StringName(raw_quest_id) if not _is_stable_id(String(quest_id)) or candidate.completed_quest_ids.has(quest_id): return _fail("Progression snapshot contains duplicate or invalid quest IDs.") candidate.completed_quest_ids.append(quest_id) var sorted_quests := candidate.completed_quest_ids.duplicate() sorted_quests.sort_custom(_name_less) if sorted_quests != candidate.completed_quest_ids: return _fail("Progression snapshot quest IDs are not canonical.") if snapshot[&"processed_requests"].size() > MAX_PROCESSED_REQUESTS: return _fail("Progression snapshot request limit exceeded.") if snapshot_version == LEGACY_SNAPSHOT_VERSION: if candidate.current_rank != MIN_RANK or not snapshot[&"processed_requests"].is_empty(): return _fail("Legacy progressed snapshots lack verifiable promotion evidence.") candidate.quarantined = bool(snapshot[&"quarantined"]) candidate.quarantine_reason = StringName(snapshot[&"quarantine_reason"]) if not _quarantine_state_is_valid(candidate.quarantined, candidate.quarantine_reason): return _fail("Progression quarantine state is inconsistent.") _copy_from(candidate) last_error = "" return true var previous_request_id := "" var recorded_target_ranks: Dictionary = {} for raw_record: Variant in snapshot[&"processed_requests"]: if not raw_record is Dictionary: return _fail("Progression snapshot request record is invalid.") var record := raw_record as Dictionary if not _keys_match(record, [&"details", &"fingerprint", &"request_id"]): return _fail("Progression snapshot request fields are invalid.") if not _is_string_variant(record.get(&"request_id", null)) \ or not _is_string_variant(record.get(&"fingerprint", null)) \ or not record.get(&"details", null) is Dictionary: return _fail("Progression snapshot request record shape is invalid.") var request_id := StringName(record[&"request_id"]) var request_text := String(request_id) if not _is_stable_id(request_text) \ or request_text <= previous_request_id \ or candidate._processed_requests.has(request_id): return _fail("Progression snapshot request IDs are not canonical.") var fingerprint := String(record[&"fingerprint"]) var details: Dictionary = record[&"details"] if not _keys_match(details, [&"bounds", &"eligibility", &"rank", &"request_id"]): return _fail("Progression snapshot request detail fields are invalid.") var detail_rank: Variant = details.get(&"rank", null) var detail_request_id: Variant = details.get(&"request_id", null) var detail_bounds: Variant = details.get(&"bounds", null) var detail_eligibility: Variant = details.get(&"eligibility", null) if not _is_integer_variant(detail_rank) \ or not _is_string_variant(detail_request_id) \ or not detail_bounds is Dictionary \ or not detail_eligibility is Dictionary \ or not _bounds_shape_is_strict(detail_bounds) \ or fingerprint != "rank:%d" % int(detail_rank) \ or int(detail_rank) < 2 \ or int(detail_rank) > candidate.current_rank \ or StringName(detail_request_id) != request_id \ or not _bounds_equal_expected(detail_bounds, candidate.get_rank_bounds(int(detail_rank))) \ or recorded_target_ranks.has(int(detail_rank)): return _fail("Progression snapshot request details are inconsistent.") if not candidate._eligibility_evidence_is_strict(detail_eligibility, int(detail_rank)): return _fail(candidate.last_error) recorded_target_ranks[int(detail_rank)] = true var canonical_details := candidate._canonical_promotion_details( request_id, int(detail_rank), detail_eligibility ) candidate._processed_requests[request_id] = { &"fingerprint": fingerprint, &"details": canonical_details, } previous_request_id = request_text if recorded_target_ranks.size() != candidate.current_rank - MIN_RANK: return _fail("Progression snapshot promotion history is incomplete.") for expected_rank in range(MIN_RANK + 1, candidate.current_rank + 1): if not recorded_target_ranks.has(expected_rank): return _fail("Progression snapshot promotion history is not contiguous.") if not candidate._historical_evidence_matches_monotonic_facts(): return _fail(candidate.last_error) candidate.quarantined = bool(snapshot[&"quarantined"]) candidate.quarantine_reason = StringName(snapshot[&"quarantine_reason"]) if not _quarantine_state_is_valid(candidate.quarantined, candidate.quarantine_reason): return _fail("Progression quarantine state is inconsistent.") _copy_from(candidate) last_error = "" return true func _replace_metrics(source: Dictionary, allow_partial: bool) -> bool: if source.size() > METRIC_IDS.size(): return _fail("Metric set is too large.") var candidate := DEFAULT_METRICS.duplicate(true) if allow_partial else {} var seen: Dictionary = {} for raw_metric_id: Variant in source: if not _is_string_variant(raw_metric_id): return _fail("Metric IDs must be strings.") var metric_id := StringName(raw_metric_id) if not METRIC_IDS.has(metric_id) or seen.has(metric_id): return _fail("Metrics contain an unknown or duplicate canonical ID.") seen[metric_id] = true var raw_value: Variant = source[raw_metric_id] if not _is_integer_variant(raw_value): return _fail("Progression metrics must be integers.") var value := int(raw_value) if value < 0 or value > MAX_SAFE_INTEGER \ or (PERCENT_METRICS.has(metric_id) and value > 100): return _fail("Progression metric is outside its valid range.") candidate[metric_id] = value if not allow_partial and seen.size() != METRIC_IDS.size(): return _fail("Progression snapshot must contain every metric exactly once.") metrics = candidate last_error = "" return true func _eligibility_evidence_is_strict(evidence: Dictionary, target_rank: int) -> bool: if not _keys_match(evidence, [ &"bounds", &"current_rank", &"eligible", &"reason", &"requirements", &"target_rank", ]): return _fail("Progression promotion evidence fields are invalid.") var evidence_current_rank: Variant = evidence.get(&"current_rank", null) var evidence_target_rank: Variant = evidence.get(&"target_rank", null) var evidence_eligible: Variant = evidence.get(&"eligible", null) var evidence_reason: Variant = evidence.get(&"reason", null) var evidence_bounds: Variant = evidence.get(&"bounds", null) var raw_requirements: Variant = evidence.get(&"requirements", null) if not _is_integer_variant(evidence_current_rank) \ or not _is_integer_variant(evidence_target_rank) \ or not evidence_eligible is bool \ or not _is_string_variant(evidence_reason) \ or not evidence_bounds is Dictionary \ or not raw_requirements is Array: return _fail("Progression promotion evidence shape is invalid.") if int(evidence_current_rank) != target_rank - 1 \ or int(evidence_target_rank) != target_rank \ or not bool(evidence_eligible) \ or StringName(evidence_reason) != &"OK" \ or not _bounds_shape_is_strict(evidence_bounds) \ or not _bounds_equal_expected(evidence_bounds, get_rank_bounds(target_rank)): return _fail("Progression promotion evidence header is inconsistent.") var expected := _expected_requirement_descriptors(target_rank) var requirements := raw_requirements as Array if requirements.size() != expected.size(): return _fail("Progression promotion evidence requirements are incomplete.") for index in requirements.size(): var raw_requirement: Variant = requirements[index] if not raw_requirement is Dictionary: return _fail("Progression promotion evidence requirement is invalid.") var requirement := raw_requirement as Dictionary var descriptor: Dictionary = expected[index] var kind := StringName(descriptor[&"kind"]) var expected_fields: Array = [&"comparison", &"current", &"id", &"met", &"required"] if kind == &"either": expected_fields.append(&"values") if not _keys_match(requirement, expected_fields): return _fail("Progression promotion evidence requirement fields are invalid.") var raw_id: Variant = requirement.get(&"id", null) var raw_comparison: Variant = requirement.get(&"comparison", null) var raw_current: Variant = requirement.get(&"current", null) var raw_required: Variant = requirement.get(&"required", null) var raw_met: Variant = requirement.get(&"met", null) if not _is_string_variant(raw_id) \ or not _is_string_variant(raw_comparison) \ or not _is_integer_variant(raw_current) \ or not _is_integer_variant(raw_required) \ or not raw_met is bool: return _fail("Progression promotion evidence requirement shape is invalid.") var current := int(raw_current) if StringName(raw_id) != StringName(descriptor[&"id"]) \ or StringName(raw_comparison) != StringName(descriptor[&"comparison"]) \ or int(raw_required) != int(descriptor[&"required"]) \ or not bool(raw_met) \ or current < 0 \ or current > MAX_SAFE_INTEGER: return _fail("Progression promotion evidence requirement values are inconsistent.") match kind: &"quest": if current != 1: return _fail("Progression promotion quest evidence is not complete.") &"either": var raw_values: Variant = requirement.get(&"values", null) if not raw_values is Dictionary \ or not _keys_match(raw_values, [&"reputation", &"underground_influence"]): return _fail("Progression alternative evidence values are invalid.") var first: Variant = raw_values.get("reputation", raw_values.get(&"reputation", null)) var second: Variant = raw_values.get("underground_influence", raw_values.get(&"underground_influence", null)) if not _is_integer_variant(first) or not _is_integer_variant(second) \ or int(first) < 0 or int(first) > 100 \ or int(second) < 0 or int(second) > 100 \ or current != maxi(int(first), int(second)) \ or (int(first) < int(raw_required) and int(second) < int(raw_required)): return _fail("Progression alternative evidence does not meet its threshold.") _: if current < int(raw_required): return _fail("Progression metric evidence does not meet its threshold.") var metric_id := StringName(descriptor[&"id"]) if PERCENT_METRICS.has(metric_id) and current > 100: return _fail("Progression percentage evidence is outside its valid range.") last_error = "" return true func _canonical_promotion_details( request_id: StringName, target_rank: int, evidence: Dictionary ) -> Dictionary: var canonical_requirements: Array[Dictionary] = [] for raw_requirement: Variant in evidence.get(&"requirements", evidence.get("requirements", [])): var requirement := raw_requirement as Dictionary var canonical := { &"id": StringName(requirement.get(&"id", requirement.get("id", ""))), &"comparison": StringName(requirement.get(&"comparison", requirement.get("comparison", ""))), &"current": int(requirement.get(&"current", requirement.get("current", 0))), &"required": int(requirement.get(&"required", requirement.get("required", 0))), &"met": bool(requirement.get(&"met", requirement.get("met", false))), } var raw_values: Variant = requirement.get(&"values", requirement.get("values", null)) if raw_values is Dictionary: canonical[&"values"] = { "reputation": int(raw_values.get(&"reputation", raw_values.get("reputation", 0))), "underground_influence": int(raw_values.get(&"underground_influence", raw_values.get("underground_influence", 0))), } canonical_requirements.append(canonical) return { &"rank": target_rank, &"bounds": get_rank_bounds(target_rank), &"eligibility": { &"eligible": true, &"current_rank": target_rank - 1, &"target_rank": target_rank, &"requirements": canonical_requirements, &"reason": &"OK", &"bounds": get_rank_bounds(target_rank), }, &"request_id": request_id, } func _historical_evidence_matches_monotonic_facts() -> bool: for raw_record: Variant in _processed_requests.values(): var record := raw_record as Dictionary var details := record[&"details"] as Dictionary var evidence := details[&"eligibility"] as Dictionary for raw_requirement: Variant in evidence[&"requirements"]: var requirement := raw_requirement as Dictionary var comparison := StringName(requirement[&"comparison"]) var requirement_id := StringName(requirement[&"id"]) if comparison == &"QUEST_COMPLETED" and not completed_quest_ids.has(requirement_id): return _fail("Progression snapshot lost a quest used by promotion history.") if LIFETIME_METRICS.has(requirement_id) \ and int(metrics[requirement_id]) < int(requirement[&"current"]): return _fail("Progression snapshot lifetime metric is below promotion history.") last_error = "" return true func _expected_requirement_descriptors(target_rank: int) -> Array[Dictionary]: var result: Array[Dictionary] = [] match target_rank: 2: result.append(_metric_descriptor(&"reputation", 25)) result.append(_metric_descriptor(&"asset_gold", 10_000)) 3: result.append(_metric_descriptor(&"faith_lifetime", 100)) result.append(_metric_descriptor(&"guest_lifetime", 300)) 4: result.append(_metric_descriptor(&"reputation", 45)) result.append(_metric_descriptor(&"asset_gold", 50_000)) 5: result.append(_metric_descriptor(&"faith_lifetime", 500)) result.append({ &"id": &"important_rank_5", &"comparison": &"QUEST_COMPLETED", &"required": 1, &"kind": &"quest", }) 6: result.append({ &"id": &"reputation_or_underground_influence", &"comparison": &"EITHER_AT_LEAST", &"required": 65, &"kind": &"either", }) 7: result.append(_metric_descriptor(&"faith_lifetime", 1_500)) result.append(_metric_descriptor(&"reputation", 60)) result.append(_metric_descriptor(&"underground_influence", 40)) result.append({ &"id": &"main_chapter_complete", &"comparison": &"QUEST_COMPLETED", &"required": 1, &"kind": &"quest", }) if target_rank >= 4: result.append(_metric_descriptor(&"active_above_ground_facilities", 1)) result.append(_metric_descriptor(&"active_underground_facilities", 1)) return result static func _metric_descriptor(metric_id: StringName, required: int) -> Dictionary: return { &"id": metric_id, &"comparison": &"AT_LEAST", &"required": required, &"kind": &"metric", } func _require_metric(requirements: Array[Dictionary], metric_id: StringName, required: int) -> void: var current := int(metrics[metric_id]) requirements.append({ &"id": metric_id, &"comparison": &"AT_LEAST", &"current": current, &"required": required, &"met": current >= required, }) func _require_quest(requirements: Array[Dictionary], quest_id: StringName) -> void: requirements.append({ &"id": quest_id, &"comparison": &"QUEST_COMPLETED", &"current": int(completed_quest_ids.has(quest_id)), &"required": 1, &"met": completed_quest_ids.has(quest_id), }) func _require_either_metric( requirements: Array[Dictionary], requirement_id: StringName, first_metric_id: StringName, second_metric_id: StringName, required: int ) -> void: var first := int(metrics[first_metric_id]) var second := int(metrics[second_metric_id]) requirements.append({ &"id": requirement_id, &"comparison": &"EITHER_AT_LEAST", &"current": maxi(first, second), &"required": required, &"met": first >= required or second >= required, &"values": {String(first_metric_id): first, String(second_metric_id): second}, }) func _copy_from(source: ProgressionService) -> void: current_rank = source.current_rank metrics = source.metrics.duplicate(true) completed_quest_ids.assign(source.completed_quest_ids) _processed_requests = source._processed_requests.duplicate(true) quarantined = source.quarantined quarantine_reason = source.quarantine_reason func _failure(code: CommandResult.Code, message: String, details: Dictionary = {}) -> CommandResult: last_error = message return CommandResult.failed(code, 0, message, details) func _fail(message: String) -> bool: last_error = message return false static func _callback_succeeded(value: Variant) -> bool: if value is CommandResult: return (value as CommandResult).is_success() if value is Dictionary: var ok_value: Variant = value.get(&"ok", value.get("ok", null)) return ok_value is bool and bool(ok_value) return value is bool and bool(value) static func _is_integer_variant(value: Variant) -> bool: return value is int or (value is float and is_finite(value) and value == floor(value)) static func _is_string_variant(value: Variant) -> bool: return value is String or value is StringName static func _is_stable_id(value: String) -> bool: if value.is_empty() or value.length() > 128 or value[0] < "a" or value[0] > "z": return false for index in value.length(): var character := value[index] if not ((character >= "a" and character <= "z") or character == "_" or (character >= "0" and character <= "9")): return false return not value.ends_with("_") and not "__" in value static func _name_less(left: StringName, right: StringName) -> bool: return String(left) < String(right) static func _quarantine_state_is_valid(value: bool, reason: StringName) -> bool: return (value and QUARANTINE_REASONS.has(reason)) or (not value and reason.is_empty()) static func _keys_match(source: Dictionary, expected: Array) -> bool: if source.size() != expected.size(): return false var seen: Dictionary = {} for raw_key: Variant in source: if not _is_string_variant(raw_key): return false var key := StringName(raw_key) if seen.has(key) or not expected.has(key): return false seen[key] = true return seen.size() == expected.size() static func _bounds_shape_is_strict(bounds: Dictionary) -> bool: if not _keys_match(bounds, [&"max_x", &"max_y", &"min_x", &"min_y"]): return false for field_id: StringName in [&"max_x", &"max_y", &"min_x", &"min_y"]: if not _is_integer_variant(bounds.get(field_id, bounds.get(String(field_id), null))): return false return true static func _bounds_equal_expected(bounds: Dictionary, expected: Dictionary) -> bool: if not _bounds_shape_is_strict(bounds) or not _bounds_shape_is_strict(expected): return false for field_id: StringName in [&"max_x", &"max_y", &"min_x", &"min_y"]: var actual: Variant = bounds.get(field_id, bounds.get(String(field_id), null)) var wanted: Variant = expected.get(field_id, expected.get(String(field_id), null)) if int(actual) != int(wanted): return false return true static func _names_to_strings(source: Array[StringName]) -> Array[String]: var result: Array[String] = [] for value: StringName in source: result.append(String(value)) return result static func _string_key_dictionary(source: Dictionary) -> Dictionary: var result: Dictionary = {} for key: StringName in METRIC_IDS: result[String(key)] = int(source[key]) return result