Remove every hat except the firefighter helmet, drop the performer cone, rewrite the review page
This commit is contained in:
1 parent
98f618c501
commit
30d145079a
10 files changed
+125
-160
No files matched your search
+14
-50
@@ -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).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in new issue
Block a user