Files
security-game/src/core/access_verifier.gd
T
zwitschi f11b441c07
Full build / Import + headless tests (push) Successful in 22s
Full build / Full desktop build + optional release (push) Skipped
Implement control pass: focus cone, action wheel, named inputs, boot fixes
- 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.
2026-10-31 15:58:01 +01:00

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