232 lines
8.5 KiB
GDScript
232 lines
8.5 KiB
GDScript
extends SceneTree
|
|
|
|
## Requirement-level audit for the park/shrine/intersection follow-up pass.
|
|
|
|
const MAP := "res://scenes/maps/sakura_crossing/sakura_crossing.tscn"
|
|
const ROAD_HALF := 4.5
|
|
const PAVE_W := 2.6
|
|
const SHOP_ST_Z := -30.0
|
|
const SOUTH_ST_Z := 20.0
|
|
const EAST_ST_X := 64.0
|
|
const EAST_ST_Z := 54.0
|
|
const STREET_HALF := 4.5
|
|
|
|
var _level: Node
|
|
var _frames := 0
|
|
var _fails := 0
|
|
|
|
|
|
func _initialize() -> void:
|
|
_level = (load(MAP) as PackedScene).instantiate()
|
|
root.add_child(_level)
|
|
|
|
|
|
func _ok(condition: bool, message: String) -> void:
|
|
if condition:
|
|
print("FOLLOWUP_AUDIT OK ", message)
|
|
else:
|
|
print("FOLLOWUP_AUDIT FAIL ", message)
|
|
_fails += 1
|
|
|
|
|
|
func _all_nodes() -> Array[Node]:
|
|
var result: Array[Node] = []
|
|
var queue: Array[Node] = [_level]
|
|
while not queue.is_empty():
|
|
var node: Node = queue.pop_front()
|
|
result.append(node)
|
|
for child in node.get_children():
|
|
queue.append(child)
|
|
return result
|
|
|
|
|
|
func _base_name(node: Node) -> String:
|
|
var value := String(node.name)
|
|
while value.length() > 0 and value[value.length() - 1].is_valid_int():
|
|
value = value.substr(0, value.length() - 1)
|
|
return value
|
|
|
|
|
|
func _named(name: String) -> Array[Node]:
|
|
var result: Array[Node] = []
|
|
for node in _all_nodes():
|
|
if _base_name(node) == name:
|
|
result.append(node)
|
|
return result
|
|
|
|
|
|
func _box_size(node: Node) -> Vector3:
|
|
var collisions := node.find_children("*", "CollisionShape3D", true, false)
|
|
if collisions.is_empty():
|
|
return Vector3.ZERO
|
|
var collision := collisions[0] as CollisionShape3D
|
|
if collision == null or not collision.shape is BoxShape3D:
|
|
return Vector3.ZERO
|
|
return (collision.shape as BoxShape3D).size
|
|
|
|
|
|
func _on_road_or_pavement(p: Vector2) -> bool:
|
|
var paved_half := ROAD_HALF + PAVE_W + 0.30
|
|
if absf(p.x) <= paved_half:
|
|
return true
|
|
if absf(p.y - SOUTH_ST_Z) <= paved_half:
|
|
return true
|
|
if p.x <= 52.0 and absf(p.y - SHOP_ST_Z) <= paved_half:
|
|
return true
|
|
if p.x >= ROAD_HALF - 0.2 and p.x <= 108.0 \
|
|
and absf(p.y - EAST_ST_Z) <= paved_half:
|
|
return true
|
|
if p.y >= SOUTH_ST_Z and p.y <= 80.0 \
|
|
and absf(p.x - EAST_ST_X) <= paved_half:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _audit_signs(nodes: Array[Node]) -> void:
|
|
var signs := 0
|
|
for node in nodes:
|
|
if not node.has_meta("ground_sign"):
|
|
continue
|
|
signs += 1
|
|
var g := (node as Node3D).global_position
|
|
_ok(not _on_road_or_pavement(Vector2(g.x, g.z)),
|
|
"ground sign %s is clear of roads, crosswalks and pavements" % node.name)
|
|
_ok(signs == 6, "all six freestanding sign posts are tagged and checked")
|
|
|
|
|
|
func _audit_sidewalk_connectors(nodes: Array[Node]) -> void:
|
|
var connectors := 0
|
|
var full_size := true
|
|
for node in nodes:
|
|
if not node.has_meta("sidewalk_connector"):
|
|
continue
|
|
connectors += 1
|
|
var collisions := node.find_children("*", "CollisionShape3D", true, false)
|
|
var collision: CollisionShape3D = collisions[0] if not collisions.is_empty() else null
|
|
if collision == null or not collision.shape is BoxShape3D:
|
|
full_size = false
|
|
continue
|
|
var size := (collision.shape as BoxShape3D).size
|
|
if size.x < PAVE_W + 0.59 or size.z < PAVE_W + 0.59:
|
|
full_size = false
|
|
_ok(connectors == 10, "eight main-junction and two shrine-side corner connectors exist")
|
|
_ok(full_size, "every tactile apron reaches the adjoining pavement on both axes")
|
|
|
|
|
|
func _audit_east_link(nodes: Array[Node]) -> void:
|
|
var links: Array[Node] = []
|
|
for node in nodes:
|
|
if node.has_meta("east_street_link"):
|
|
links.append(node)
|
|
_ok(links.size() == 1, "the shrine-side neighbourhood road has one continuous link slab")
|
|
if links.size() == 1:
|
|
var body := links[0] as StaticBody3D
|
|
var collisions := body.find_children("*", "CollisionShape3D", true, false)
|
|
var collision: CollisionShape3D = collisions[0] if not collisions.is_empty() else null
|
|
var size := (collision.shape as BoxShape3D).size
|
|
var left_edge := body.global_position.x - size.x * 0.5
|
|
_ok(left_edge <= ROAD_HALF, "the neighbourhood branch physically reaches the main road")
|
|
var query := PhysicsRayQueryParameters3D.create(
|
|
Vector3(6.0, 2.0, EAST_ST_Z), Vector3(6.0, -1.0, EAST_ST_Z))
|
|
var hit: Dictionary = _level.get_world_3d().direct_space_state.intersect_ray(query)
|
|
var collider: Node = hit.get("collider") as Node
|
|
_ok(not hit.is_empty() and collider != null and _base_name(collider) == "EastStreetLink",
|
|
"the former gap is road surface, not the old sidewalk barrier")
|
|
|
|
|
|
func _audit_park_and_playground() -> void:
|
|
_ok(_named("PondBridge").size() == 1, "north park has a connected pond-loop bridge")
|
|
_ok(_named("PicnicTable").size() >= 3, "park system has multiple social/picnic nodes")
|
|
_ok(_named("ParkLamp").size() >= 8, "park paths have a consistent lighting rhythm")
|
|
_ok(_named("ParkBin").size() >= 4, "park entrances and activity nodes have service assets")
|
|
_ok(_named("SwingBeam").size() == 1 and _named("SwingSeat").size() == 2 \
|
|
and _named("SwingChain").size() == 4,
|
|
"two swing seats align to four chains and one overhead beam")
|
|
var beams := _named("SwingBeam")
|
|
var seats := _named("SwingSeat")
|
|
var chains := _named("SwingChain")
|
|
if beams.size() == 1 and seats.size() == 2 and chains.size() == 4:
|
|
var beam := beams[0] as Node3D
|
|
var beam_size := _box_size(beam)
|
|
var beam_left := beam.global_position.x - beam_size.x * 0.5
|
|
var beam_right := beam.global_position.x + beam_size.x * 0.5
|
|
var chains_under_beam := true
|
|
for chain in chains:
|
|
var cp := (chain as Node3D).global_position
|
|
chains_under_beam = chains_under_beam \
|
|
and cp.x >= beam_left and cp.x <= beam_right \
|
|
and absf(cp.z - beam.global_position.z) < 0.01 \
|
|
and absf((cp.y + 1.55 * 0.5) - (beam.global_position.y - beam_size.y * 0.5)) < 0.08
|
|
_ok(chains_under_beam, "every swing chain lands under and touches the common top beam")
|
|
var seats_have_pairs := true
|
|
for seat in seats:
|
|
var sp := (seat as Node3D).global_position
|
|
var matching := 0
|
|
for chain in chains:
|
|
var cp := (chain as Node3D).global_position
|
|
if absf(cp.x - sp.x) <= 0.55 and absf(cp.z - sp.z) < 0.01:
|
|
matching += 1
|
|
seats_have_pairs = seats_have_pairs and matching == 2
|
|
_ok(seats_have_pairs, "each swing seat is centered beneath exactly two chains")
|
|
_ok(_named("SlidePlatform").size() == 1 and _named("SlideSlope").size() == 1,
|
|
"slide platform and slope are both present")
|
|
var platforms := _named("SlidePlatform")
|
|
var slopes := _named("SlideSlope")
|
|
if platforms.size() == 1 and slopes.size() == 1:
|
|
var platform := platforms[0] as Node3D
|
|
var slope := slopes[0] as Node3D
|
|
var platform_size := _box_size(platform)
|
|
var slope_size := _box_size(slope)
|
|
var high_end := slope.global_transform * Vector3(0.0, 0.0, slope_size.z * 0.5)
|
|
var platform_edge := Vector3(platform.global_position.x,
|
|
platform.global_position.y, platform.global_position.z - platform_size.z * 0.5)
|
|
_ok(Vector2(high_end.y, high_end.z).distance_to(
|
|
Vector2(platform_edge.y, platform_edge.z)) < 0.16,
|
|
"slide incline meets the platform edge at the same height and z datum")
|
|
_ok(_named("Sandpit").size() == 1 and _named("Seesaw").size() == 1 \
|
|
and _named("ClimbingFrame").size() == 1,
|
|
"playground contains distinct sand, seesaw and climbing zones")
|
|
|
|
var cherries: Array[Node] = []
|
|
var cedars: Array[Node] = []
|
|
for node in _named("Cherry"):
|
|
var p := (node as Node3D).global_position
|
|
if p.x >= -96.0 and p.x <= 92.0 and p.z < -70.0:
|
|
cherries.append(node)
|
|
for node in _named("Cedar"):
|
|
var p := (node as Node3D).global_position
|
|
if p.x >= -96.0 and p.x <= 92.0 and p.z <= -78.0 and p.z >= -89.0:
|
|
cedars.append(node)
|
|
var min_gap := INF
|
|
for cherry in cherries:
|
|
var cp := (cherry as Node3D).global_position
|
|
for cedar in cedars:
|
|
var ep := (cedar as Node3D).global_position
|
|
min_gap = minf(min_gap, Vector2(cp.x, cp.z).distance_to(Vector2(ep.x, ep.z)))
|
|
_ok(not cherries.is_empty() and not cedars.is_empty() and min_gap > 8.0,
|
|
"north-park cherry and evergreen planting bands are spatially separated")
|
|
|
|
|
|
func _audit_shrine() -> void:
|
|
_ok(_named("Komainu").size() >= 4, "outer and inner shrine thresholds have guardian pairs")
|
|
_ok(_named("ShrineBanner").size() >= 4, "shrine approach and precinct have banner rhythm")
|
|
_ok(_named("OmikujiRack").size() == 1, "precinct has a dedicated fortune-tying rack")
|
|
_ok(_named("SacredTree").size() == 1 and _named("Hokora").size() == 1,
|
|
"shrine lawn has a sacred tree and auxiliary shrine destination")
|
|
|
|
|
|
func _process(_delta: float) -> bool:
|
|
_frames += 1
|
|
if _frames < 120:
|
|
return false
|
|
var nodes := _all_nodes()
|
|
_audit_signs(nodes)
|
|
_audit_sidewalk_connectors(nodes)
|
|
_audit_east_link(nodes)
|
|
_audit_park_and_playground()
|
|
_audit_shrine()
|
|
print("=== SAKURA FOLLOW-UP AUDIT: %s ===" %
|
|
("ALL CLEAR" if _fails == 0 else "%d FAILED" % _fails))
|
|
quit(0 if _fails == 0 else 1)
|
|
return true
|