Gate library prop collision coverage across venues
This commit is contained in:
1 parent
fe3ba3054e
commit
5dec59355a
4 files changed
+84
-1
No files matched your search
@@ -4,6 +4,14 @@ Session memory for durable decisions, state, and gotchas. Keep concise, remove o
|
|||||||
|
|
||||||
## Decisions
|
## Decisions
|
||||||
|
|
||||||
|
- **Collider coverage gate (2026-11-03):** `WorldBuilder` sets `library_prop` metadata on every
|
||||||
|
node in `library_nodes`, and `NON_BLOCKING_LIBRARY` (`marquee`, `wall_lantern`, `handrail`,
|
||||||
|
`double_door`) documents the mounted/decorative pieces that intentionally publish no
|
||||||
|
walk-blocking body. `test_world.gd` verifies placed props have `Collision` + `Shape` +
|
||||||
|
`Avoidance` in all three venues, and that library dressing has a body unless exempt. Both
|
||||||
|
were mutation-checked (removing `_add_prop_collision` fails 3 tests; dropping `handrail` from
|
||||||
|
the exemption list fails with the node name). Suite is 275/275. Add the metadata when adding a
|
||||||
|
new library placement, or the coverage test will report it.
|
||||||
- **3D review gates (2026-11-03):** `tools/test_blender_assets.py` now gates texel density
|
- **3D review gates (2026-11-03):** `tools/test_blender_assets.py` now gates texel density
|
||||||
(all five character slots present, 40–400 px/m authored, ≥250 px/m effective fabric), and
|
(all five character slots present, 40–400 px/m authored, ≥250 px/m effective fabric), and
|
||||||
`tools/test_model_review.py` gates scorecard coverage (every `CHARACTERS`/`GEAR`/`PROPS` entry
|
`tools/test_model_review.py` gates scorecard coverage (every `CHARACTERS`/`GEAR`/`PROPS` entry
|
||||||
|
|||||||
@@ -122,8 +122,14 @@ except the rebuilt assets themselves.
|
|||||||
and the 40–400 px/m authored band plus the 250 px/m fabric floor; the pose→bone contract is
|
and the 40–400 px/m authored band plus the 250 px/m fabric floor; the pose→bone contract is
|
||||||
covered by `test_pose_reaches_the_skeleton_bones`; LOD/impostor already has
|
covered by `test_pose_reaches_the_skeleton_bones`; LOD/impostor already has
|
||||||
`test_avatar_mesh_budget_and_lod_swap`. Collider coverage is still open — split below.)
|
`test_avatar_mesh_budget_and_lod_swap`. Collider coverage is still open — split below.)
|
||||||
- [ ] Add a collider/proxy coverage check: every library prop that blocks walking has a
|
- [x] Add a collider/proxy check: every library prop that blocks walking has a
|
||||||
`StaticBody3D` + `NavigationObstacle3D`, checked against the catalogue.
|
`StaticBody3D` + `NavigationObstacle3D`, checked against the catalogue.
|
||||||
|
(`world_builder.gd` tags every library node with a `library_prop` meta and declares the
|
||||||
|
non-blocking `marquee`/`handrail`/`lantern`/`door` set; two tests in `test_world.gd`
|
||||||
|
verify placed props (body + shape + avoidance radius, all three venues) and library
|
||||||
|
dressing (body unless documented exempt). Mutation-checked: removing `_add_prop_collision`
|
||||||
|
fails 3 tests, dropping `handrail` from the exemption list fails with the exact node.
|
||||||
|
Suite is 275/275.)
|
||||||
- [x] Add a coverage test so every catalogue asset has a scorecard row and verdict.
|
- [x] Add a coverage test so every catalogue asset has a scorecard row and verdict.
|
||||||
(`tools/test_model_review.py`: every `CHARACTERS`/`GEAR`/`PROPS` entry needs a row in
|
(`tools/test_model_review.py`: every `CHARACTERS`/`GEAR`/`PROPS` entry needs a row in
|
||||||
`docs/3d-model-review.md`, and character rows need a non-empty verdict.)
|
`docs/3d-model-review.md`, and character rows need a non-empty verdict.)
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ const LIBRARY_PROPS := ["bollard", "bench", "trash_bin", "planter_box",
|
|||||||
"newspaper_box", "fire_hydrant", "street_lamp", "street_tree", "traffic_cone",
|
"newspaper_box", "fire_hydrant", "street_lamp", "street_tree", "traffic_cone",
|
||||||
"palm_tree", "round_planter", "a_frame_sign", "crowd_barrier", "stanchion",
|
"palm_tree", "round_planter", "a_frame_sign", "crowd_barrier", "stanchion",
|
||||||
"security_podium"]
|
"security_podium"]
|
||||||
|
## Library pieces that are mounted above head height, part of the building shell or
|
||||||
|
## purely decorative, so they intentionally publish no walk-blocking body. Anything
|
||||||
|
## else in `library_nodes` must carry a `StaticBody3D` + `NavigationObstacle3D`
|
||||||
|
## (guarded by `test_library_dressing_collision_matches_the_documented_exemptions`).
|
||||||
|
const NON_BLOCKING_LIBRARY := ["marquee", "wall_lantern", "handrail", "double_door"]
|
||||||
|
|
||||||
var root: Node3D = null
|
var root: Node3D = null
|
||||||
## Door lock indicator mesh near the handle; toggled by GameManager.
|
## Door lock indicator mesh near the handle; toggled by GameManager.
|
||||||
@@ -336,6 +341,7 @@ func _build_building() -> void:
|
|||||||
door_model.position = Vector3(0, 0.0, front + 0.16)
|
door_model.position = Vector3(0, 0.0, front + 0.16)
|
||||||
building.add_child(door_model)
|
building.add_child(door_model)
|
||||||
library_nodes.append(door_model)
|
library_nodes.append(door_model)
|
||||||
|
door_model.set_meta("library_prop", "double_door")
|
||||||
_apply_library_ao(door_model, "double_door")
|
_apply_library_ao(door_model, "double_door")
|
||||||
for material in _emissive_materials(door_model):
|
for material in _emissive_materials(door_model):
|
||||||
if not _lamp_bulb_mats.has(material):
|
if not _lamp_bulb_mats.has(material):
|
||||||
@@ -1674,6 +1680,7 @@ func _build_marquee() -> void:
|
|||||||
library.position = Vector3(0, 5.62, -3.9)
|
library.position = Vector3(0, 5.62, -3.9)
|
||||||
root.add_child(library)
|
root.add_child(library)
|
||||||
library_nodes.append(library)
|
library_nodes.append(library)
|
||||||
|
library.set_meta("library_prop", "marquee")
|
||||||
_apply_library_ao(library, "marquee")
|
_apply_library_ao(library, "marquee")
|
||||||
for material in _emissive_materials(library):
|
for material in _emissive_materials(library):
|
||||||
if not _marquee_emissive.has(material):
|
if not _marquee_emissive.has(material):
|
||||||
@@ -1714,6 +1721,7 @@ func _build_library_dressing() -> void:
|
|||||||
rail.rotation_degrees.y = 90.0
|
rail.rotation_degrees.y = 90.0
|
||||||
root.add_child(rail)
|
root.add_child(rail)
|
||||||
library_nodes.append(rail)
|
library_nodes.append(rail)
|
||||||
|
rail.set_meta("library_prop", "handrail")
|
||||||
_apply_library_ao(rail, "handrail")
|
_apply_library_ao(rail, "handrail")
|
||||||
if _feature("sedan") and ResourceLoader.exists(BLENDER_PROP_DIR + "sedan.glb"):
|
if _feature("sedan") and ResourceLoader.exists(BLENDER_PROP_DIR + "sedan.glb"):
|
||||||
var sedan_pos: Vector3 = spec.get("sedan_position", Vector3(6.0, 0.1, 17.0))
|
var sedan_pos: Vector3 = spec.get("sedan_position", Vector3(6.0, 0.1, 17.0))
|
||||||
@@ -1724,6 +1732,7 @@ func _build_library_dressing() -> void:
|
|||||||
_apply_car_paint(sedan)
|
_apply_car_paint(sedan)
|
||||||
if not library_nodes.has(sedan):
|
if not library_nodes.has(sedan):
|
||||||
library_nodes.append(sedan)
|
library_nodes.append(sedan)
|
||||||
|
sedan.set_meta("library_prop", "sedan")
|
||||||
for material in _emissive_materials(sedan):
|
for material in _emissive_materials(sedan):
|
||||||
if not _lamp_bulb_mats.has(material):
|
if not _lamp_bulb_mats.has(material):
|
||||||
_lamp_bulb_mats.append(material)
|
_lamp_bulb_mats.append(material)
|
||||||
@@ -1795,6 +1804,7 @@ func _add_wall_lantern(pos: Vector3, with_light: bool) -> void:
|
|||||||
lantern.position = pos
|
lantern.position = pos
|
||||||
root.add_child(lantern)
|
root.add_child(lantern)
|
||||||
library_nodes.append(lantern)
|
library_nodes.append(lantern)
|
||||||
|
lantern.set_meta("library_prop", "wall_lantern")
|
||||||
_apply_library_ao(lantern, "wall_lantern")
|
_apply_library_ao(lantern, "wall_lantern")
|
||||||
for material in _emissive_materials(lantern):
|
for material in _emissive_materials(lantern):
|
||||||
if not _lamp_bulb_mats.has(material):
|
if not _lamp_bulb_mats.has(material):
|
||||||
@@ -2095,6 +2105,7 @@ func _add_prop_model(parent: Node3D, prop: String, pos: Vector3,
|
|||||||
if ResourceLoader.exists(BLENDER_PROP_DIR + prop + ".glb"):
|
if ResourceLoader.exists(BLENDER_PROP_DIR + prop + ".glb"):
|
||||||
library_props.append(prop)
|
library_props.append(prop)
|
||||||
library_nodes.append(holder)
|
library_nodes.append(holder)
|
||||||
|
holder.set_meta("library_prop", prop)
|
||||||
_apply_library_ao(model, prop)
|
_apply_library_ao(model, prop)
|
||||||
else:
|
else:
|
||||||
var fallback := _fallback_prop(prop)
|
var fallback := _fallback_prop(prop)
|
||||||
|
|||||||
@@ -296,3 +296,61 @@ func test_door_indicator_built() -> bool:
|
|||||||
return a and b and c
|
return a and b and c
|
||||||
root.free()
|
root.free()
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
## 3D review gate: a placed prop that people can walk into must publish both a
|
||||||
|
## static body and a navigation-avoidance footprint. A prop whose bounds are
|
||||||
|
## empty (broken GLB/fallback) used to lose its collider silently.
|
||||||
|
func test_every_placed_prop_has_a_collider_and_avoidance() -> bool:
|
||||||
|
var problems: Array[String] = []
|
||||||
|
for venue in ["hollywood_building", "joke_shop", "tv_studio"]:
|
||||||
|
var root := Node3D.new()
|
||||||
|
var builder := WorldBuilder.new(root, venue)
|
||||||
|
builder.build()
|
||||||
|
var holders := root.find_children("Prop_*", "Node3D", true, false)
|
||||||
|
if holders.size() < 8:
|
||||||
|
problems.append("%s places only %d props" % [venue, holders.size()])
|
||||||
|
for holder in holders:
|
||||||
|
var body := holder.get_node_or_null("Collision") as StaticBody3D
|
||||||
|
var shape := body.get_node_or_null("Shape") as CollisionShape3D if body != null else null
|
||||||
|
var obstacle := holder.get_node_or_null("Avoidance") as NavigationObstacle3D
|
||||||
|
if body == null or shape == null or shape.shape == null:
|
||||||
|
problems.append("%s/%s has no collision shape" % [venue, holder.name])
|
||||||
|
if obstacle == null or obstacle.radius <= 0.0:
|
||||||
|
problems.append("%s/%s has no avoidance radius" % [venue, holder.name])
|
||||||
|
root.free()
|
||||||
|
return TestUtils.check(problems.is_empty(),
|
||||||
|
"placed props are covered: %s" % ", ".join(problems.slice(0, 6)))
|
||||||
|
|
||||||
|
## Library dressing must either publish a walk-blocking body or be on the
|
||||||
|
## documented mounted/decorative list. The check also proves the metadata that
|
||||||
|
## ties a node to its catalogue asset is present.
|
||||||
|
func test_library_dressing_collision_matches_the_documented_exemptions() -> bool:
|
||||||
|
var problems: Array[String] = []
|
||||||
|
for venue in ["hollywood_building", "joke_shop", "tv_studio"]:
|
||||||
|
var root := Node3D.new()
|
||||||
|
var builder := WorldBuilder.new(root, venue)
|
||||||
|
builder.build()
|
||||||
|
if builder.library_nodes.is_empty():
|
||||||
|
problems.append("%s placed no library dressing" % venue)
|
||||||
|
for node in builder.library_nodes:
|
||||||
|
if not is_instance_valid(node):
|
||||||
|
continue
|
||||||
|
var prop := str(node.get_meta("library_prop", ""))
|
||||||
|
if prop == "":
|
||||||
|
problems.append("%s/%s has no library_prop metadata" % [venue, node.name])
|
||||||
|
continue
|
||||||
|
if WorldBuilder.NON_BLOCKING_LIBRARY.has(prop):
|
||||||
|
continue
|
||||||
|
if _walk_blocking_body(node) == null:
|
||||||
|
problems.append("%s/%s (%s) blocks walking without a body" % [venue, node.name, prop])
|
||||||
|
root.free()
|
||||||
|
return TestUtils.check(problems.is_empty(),
|
||||||
|
"library dressing is covered: %s" % ", ".join(problems.slice(0, 6)))
|
||||||
|
|
||||||
|
func _walk_blocking_body(node: Node) -> StaticBody3D:
|
||||||
|
for child in node.find_children("*", "StaticBody3D", true, false):
|
||||||
|
var body := child as StaticBody3D
|
||||||
|
for shape in body.find_children("*", "CollisionShape3D", true, false):
|
||||||
|
if (shape as CollisionShape3D).shape != null:
|
||||||
|
return body
|
||||||
|
return null
|
||||||
Reference in new issue
Block a user