- Venue-correct access artifacts (AccessLoadout) and screened-aware verify - Cone focus on NPC heads with billboarded ring + display name (ADR-0008) - Action wheel with de-escalation-first slices (ADR-0009); Esc/RMB cancel - Procedural NPC face/hair/neck/shoes (ADR-0010); named input actions - Fix GameManager parse error (radial typed as ActionRadial) - Fix first-person camera height and guard spawning inside the building - Move building volume back so the door/awning/marquee are visible - Pull the event scheduler once per simulated second; cap live NPCs - Regenerate README screenshots; update TODO/DONE/MEMORY/README; 77 tests Adds src/test/smoke_focus_radial.gd for the windowed focus/radial smoke.
124 lines
5.1 KiB
GDScript
124 lines
5.1 KiB
GDScript
class_name AccessVerifier
|
|
extends RefCounted
|
|
|
|
const Rules := preload("res://src/core/rules.gd")
|
|
const AccessPolicy := preload("res://src/core/access_policy.gd")
|
|
const AccessArtifact := preload("res://src/core/access_artifact.gd")
|
|
## Evaluates access artifacts against a venue AccessPolicy.
|
|
## Pure logic: no UI, no Node dependency, fully headless-testable.
|
|
|
|
## Result dictionary contract:
|
|
## { granted: bool, reasons: Array[String], issues: Array[String],
|
|
## decisive_artifacts: Array[String], checked_artifacts: Array[String] }
|
|
|
|
func _init() -> void:
|
|
pass
|
|
|
|
## Verify a single person's set of artifacts (person_artifacts: Array[AccessArtifact]).
|
|
func verify(person_artifacts: Array, policy: AccessPolicy, already_screened: bool = false) -> Dictionary:
|
|
var reasons: Array[String] = []
|
|
var issues: Array[String] = []
|
|
var checked: Array[String] = []
|
|
|
|
for artifact in person_artifacts:
|
|
if not (artifact is AccessArtifact):
|
|
continue
|
|
checked.append(artifact.summary())
|
|
if artifact.kind == Rules.AccessKind.RECOGNITION and artifact.status == Rules.ArtifactStatus.VALID:
|
|
reasons.append("Recognized by guard")
|
|
elif artifact.status == Rules.ArtifactStatus.VALID:
|
|
reasons.append("Valid %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
|
|
elif artifact.status == Rules.ArtifactStatus.EXPIRED:
|
|
issues.append("Expired %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
|
|
elif artifact.status == Rules.ArtifactStatus.COUNTERFEIT:
|
|
issues.append("Suspected counterfeit %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
|
|
elif artifact.status == Rules.ArtifactStatus.MISMATCHED:
|
|
issues.append("%s does not match person" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
|
|
elif artifact.status == Rules.ArtifactStatus.AMBIGUOUS:
|
|
issues.append("Ambiguous %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
|
|
elif artifact.status == Rules.ArtifactStatus.MISSING:
|
|
issues.append("Missing %s" % Rules.ACCESS_KIND_NAMES.get(artifact.kind, "artifact"))
|
|
|
|
# Age check.
|
|
if policy.minimum_age > 0 and require_age_info(person_artifacts, policy):
|
|
var age := person_age(person_artifacts)
|
|
if age < policy.minimum_age:
|
|
issues.append("Underage (%d < %d)" % [age, policy.minimum_age])
|
|
return _result(false, reasons, issues, checked)
|
|
|
|
# Wand screening is processed by the interaction layer (false alarms), but a
|
|
# policy can hard-require it; a refused screen blocks entry.
|
|
if policy.wand_screening_required and not already_screened and not person_screened(person_artifacts):
|
|
issues.append("Screening required")
|
|
return _result(false, reasons, issues, checked)
|
|
|
|
# Recognition short-circuits if present and valid.
|
|
var recognized := false
|
|
for artifact in person_artifacts:
|
|
if artifact is AccessArtifact:
|
|
if artifact.kind == Rules.AccessKind.RECOGNITION and artifact.status == Rules.ArtifactStatus.VALID:
|
|
recognized = true
|
|
if recognized:
|
|
return _result(true, reasons, issues, checked)
|
|
|
|
# Every required artifact kind must have a valid match (AND admission),
|
|
# unless the policy grants on any single valid artifact (OR admission).
|
|
var any_satisfied := false
|
|
for kind in policy.required_artifacts.keys():
|
|
var satisfied := false
|
|
for artifact in person_artifacts:
|
|
if artifact is AccessArtifact and artifact.kind == kind:
|
|
if artifact.status == Rules.ArtifactStatus.VALID:
|
|
satisfied = true
|
|
any_satisfied = any_satisfied or satisfied
|
|
if not satisfied:
|
|
issues.append("Missing valid %s" % Rules.ACCESS_KIND_NAMES.get(kind, "credential"))
|
|
if not policy.grant_if_any_required:
|
|
return _result(false, reasons, issues, checked)
|
|
if policy.grant_if_any_required and not any_satisfied:
|
|
return _result(false, reasons, issues, checked)
|
|
|
|
if policy.require_id:
|
|
var id_ok := false
|
|
for artifact in person_artifacts:
|
|
if artifact is AccessArtifact and artifact.kind == Rules.AccessKind.ID and artifact.status == Rules.ArtifactStatus.VALID:
|
|
id_ok = true
|
|
if not id_ok:
|
|
issues.append("Valid ID required")
|
|
return _result(false, reasons, issues, checked)
|
|
|
|
return _result(true, reasons, issues, checked)
|
|
|
|
func _result(granted: bool, reasons: Array, issues: Array, checked: Array) -> Dictionary:
|
|
return {
|
|
"granted": granted,
|
|
"reasons": reasons,
|
|
"issues": issues,
|
|
"decisive_artifacts": reasons.duplicate(),
|
|
"checked_artifacts": checked,
|
|
}
|
|
|
|
func require_age_info(person_artifacts: Array, policy: AccessPolicy) -> bool:
|
|
if policy.minimum_age <= 0:
|
|
return false
|
|
var id_present := false
|
|
for artifact in person_artifacts:
|
|
if artifact is AccessArtifact and artifact.kind == Rules.AccessKind.ID:
|
|
id_present = true
|
|
return id_present
|
|
|
|
func person_age(person_artifacts: Array) -> int:
|
|
for artifact in person_artifacts:
|
|
if artifact is AccessArtifact and artifact.kind == Rules.AccessKind.ID:
|
|
var age: Variant = artifact.attributes.get("age", -1)
|
|
if age is int and age > 0:
|
|
return age
|
|
return -1
|
|
|
|
func person_screened(person_artifacts: Array) -> bool:
|
|
for artifact in person_artifacts:
|
|
if artifact is AccessArtifact:
|
|
if artifact.kind == Rules.AccessKind.CREDENTIAL and artifact.attributes.get("screened", false) == true:
|
|
return true
|
|
return false
|