Remove every hat except the firefighter helmet, drop the performer cone, rewrite the review page
Full build / Import + headless tests (push) Successful in 45s
Full build / Full desktop build + optional release (push) Skipped

This commit is contained in:
zwitschi committed 2026-11-06 17:52:46 +01:00
1 parent 98f618c501
commit 30d145079a
10 files changed
+125 -160

No files matched your search

+14 -50
View File
@@ -154,15 +154,17 @@ const HAIR_COLORS := [Color(0.12, 0.1, 0.08), Color(0.35, 0.22, 0.12), Color(0.5
## Which small accessory a role wears. This is look-only: the player must still
## read intent from behaviour, never from the archetype name (ADR-0010).
## Only the responder wears head gear: the firefighter helmet. Everything else
## is a body or hand item, so no role can grow a hat that hides the face.
const ACCESSORIES := {
Rules.NpcType.AUTHORIZED_STAFF: "cap",
Rules.NpcType.RESPONDER: "cap",
Rules.NpcType.DELIVERY: "cap",
Rules.NpcType.AUTHORIZED_STAFF: "none",
Rules.NpcType.RESPONDER: "none",
Rules.NpcType.DELIVERY: "none",
Rules.NpcType.TOURIST: "backpack",
Rules.NpcType.INFLUENCER: "sunglasses",
Rules.NpcType.TRESPASSER: "hood",
Rules.NpcType.VIOLENT_THREAT: "hood",
Rules.NpcType.MENTAL_CRISIS: "hood",
Rules.NpcType.TRESPASSER: "none",
Rules.NpcType.VIOLENT_THREAT: "none",
Rules.NpcType.MENTAL_CRISIS: "none",
Rules.NpcType.AUTHORIZED_GUEST: "lanyard",
Rules.NpcType.PERFORMER: "none",
Rules.NpcType.PROTESTER: "none",
@@ -178,22 +180,6 @@ const BLENDER_GEAR_DIR := "res://assets/models/blender/npc_gear/"
const GEAR_OFFSETS := {
"lanyard": Vector3(0, 0, 0.04),
"firefighter_helmet": Vector3(0, 0.03, 0),
# The Quaternius head wears a 0.24 m hair shell that stands ~3.5 cm above the
# cap crown, so the cap sinks into the hair and only its brim shows. Lift the
# cap and hood onto the hair; sunglasses stay at the eye line.
# Owner: every hat and head item moves up; the firefighter helmet keeps its fit.
"cap": Vector3(0, 0.07, 0),
"hood": Vector3(0, 0.07, 0),
"beanie": Vector3(0, 0.07, 0),
}
## The hat geometry is authored around the 0.315 m procedural head and scaled to
## 0.64 in the Blender build to match the 0.20 m Quaternius skull. That fits the
## skull but not the 0.228 m hair shell on top of it, so a cap sinks into the hair
## and only its brim shows. These scale the head-gear node instead of the mesh.
const GEAR_SCALE := {
"cap": 1.22,
"hood": 1.12,
"beanie": 1.22,
}
## Hand gear is authored around the wrist proxy. The palm sits ~4 cm in front
## of that origin, so without this offset the first phone/flashlight versions
@@ -824,8 +810,6 @@ func _attach_gear(gear_name: String) -> bool:
node.position += GEAR_OFFSETS[gear_name]
if str(GEAR_ATTACH[gear_name]) == "hand":
node.position += HAND_ITEM_FORWARD
if GEAR_SCALE.has(gear_name):
node.scale = Vector3.ONE * float(GEAR_SCALE[gear_name])
_detail_nodes.append(node)
var ao := Materials.load_ao(BLENDER_GEAR_DIR + gear_name + "_ao.png")
for child in node.find_children("*", "MeshInstance3D", true, false):
@@ -837,30 +821,20 @@ func _attach_gear(gear_name: String) -> bool:
func _add_character_gear() -> void:
match character_model:
"npc_firefighter":
# The only head gear in the roster.
_attach_gear("firefighter_helmet")
_attach_gear("scba_tank")
"npc_guard":
_attach_gear("cap")
"npc_vest":
_attach_gear("cap")
"npc_dress":
_attach_gear("shoulder_bag")
## Small role accessory: cap, hood, backpack, sunglasses or a staff lanyard.
## Small role accessory: backpack, sunglasses or a staff lanyard. No hats: the
## firefighter helmet is the only head gear in the roster.
func _add_accessory(kind: String, shirt: Color) -> void:
if character_model == "npc_firefighter":
return
if character_model == "npc_hoodie" and kind == "hood":
# The hoodie variant already models the hood up.
return
if _attach_gear(kind):
return
match kind:
"cap":
_detail(_add_sphere(_head, 0.175, Vector3(0, 0.12, 0), shirt.darkened(0.15), "cloth"))
_detail(_add_box_mesh(_head, Vector3(0.30, 0.03, 0.16), Vector3(0, 0.10, 0.16), shirt.darkened(0.22), "cloth"))
"hood":
_detail(_add_sphere(_head, 0.205, Vector3(0, 0.03, -0.03), Color(0.16, 0.16, 0.18), "cloth"))
"backpack":
_detail(_add_box_mesh(_rig, Vector3(0.34, 0.42, 0.16), Vector3(0, 1.06, -0.24), Color(0.24, 0.28, 0.36), "cloth"))
_detail(_add_box_mesh(_rig, Vector3(0.30, 0.06, 0.02), Vector3(0, 1.24, -0.33), Color(0.42, 0.44, 0.5), "cloth"))
@@ -980,19 +954,9 @@ func _add_props(npc_type: int) -> void:
Rules.NpcType.DELIVERY:
_add_box_prop(_rig, Vector3(0.34, 0.26, 0.28), Vector3(0.0, 1.02, 0.3), Color(0.55, 0.4, 0.25))
Rules.NpcType.PERFORMER:
# The cone is sized to the shipped head (0.20 m wide) and sits on the
# crown; at the old 0.44 m base it swallowed the whole head.
var hat := _material(Color(0.85, 0.2, 0.25))
var cone := MeshInstance3D.new()
var cm := CylinderMesh.new()
cm.top_radius = 0.012
cm.bottom_radius = 0.125
cm.height = 0.24
cone.mesh = cm
cone.material_override = hat
cone.position = Vector3(0, 0.15, 0)
_head.add_child(cone)
# Hat in one hand and a small board in the other: the performer plays
# No cone on the head: the performer plays the sidewalk with a board in
# one hand and a baton in the other, so the face stays readable.
# Baton in one hand and a small board in the other: the performer plays
# to the sidewalk once at the waypoint. Both ride the hand proxies, so
# they follow the arm instead of floating beside the body at a fixed
# rig offset (which is what the old -0.46 m placement did).
+4 -2
View File
@@ -326,7 +326,7 @@ func _gear_bounds(node: Node3D) -> AABB:
return box
func test_head_and_back_gear_clear_the_body_silhouette() -> bool:
const HEAD_WIDTH := 0.315
const HEAD_WIDTH := 0.201
const WIDEST_BODY_RADIUS := 0.26
var problems: Array[String] = []
var seen := {}
@@ -362,7 +362,9 @@ func test_head_and_back_gear_clear_the_body_silhouette() -> bool:
problems.append("%s only reaches %.3f m (body %.3f)" % [
name, maxf(outer_x, outer_z), WIDEST_BODY_RADIUS])
avatar.free()
for required in ["head:sunglasses", "head:firefighter_helmet", "head:cap",
# The firefighter helmet is the only head gear in the roster; caps and hoods
# were removed so no role can hide its face.
for required in ["head:sunglasses", "head:firefighter_helmet",
"back:shoulder_bag", "back:backpack", "back:scba_tank"]:
if not seen.has(required):
problems.append("missing %s" % required)
+6 -4
View File
@@ -65,12 +65,14 @@ func test_outfit_varies_across_seeds() -> bool:
return a and b
func test_accessories_follow_the_role() -> bool:
# No hats: the firefighter helmet is the only head gear, so responders and
# trespassers carry body accessories instead of a cap or hood.
var a := TestUtils.check_eq(
NpcAvatar.outfit_for(Rules.NpcType.RESPONDER, 5)["accessory"], "cap",
"responders wear a cap")
NpcAvatar.outfit_for(Rules.NpcType.RESPONDER, 5)["accessory"], "none",
"responders wear no hat")
var b := TestUtils.check_eq(
NpcAvatar.outfit_for(Rules.NpcType.TRESPASSER, 5)["accessory"], "hood",
"trespassers wear a hood")
NpcAvatar.outfit_for(Rules.NpcType.TRESPASSER, 5)["accessory"], "none",
"trespassers wear no hood")
var c := TestUtils.check_eq(
NpcAvatar.outfit_for(Rules.NpcType.AUTHORIZED_GUEST, 5)["accessory"], "lanyard",
"guests wear a lanyard")