Preserve approved pixel tower prototype as development baseline

This commit is contained in:
Codex
2026-09-09 07:51:16 +09:00
commit 423fd6dd2e
915 changed files with 95193 additions and 0 deletions
+822
View File
@@ -0,0 +1,822 @@
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
@@ -0,0 +1 @@
uid://minvgfo8gkdj