Rebuild the Joke Shop as the Sunset Strip block
- Replace the alley with the 27 m Comedy Store-inspired hall, signature canopy, circular blade sign, red curtain bays, west entrance, low-rise neighbours, dry hillside and opposite frontage. - Add day/night/across-street concept frames and real-site mapping notes. - Expose the Joke Shop day stage in the realism captures and refresh the gallery shots. - Render multiline HUD/wand strings correctly and list carried items in the wand panel. - Add the Joke Shop layout/budget tests and update docs, roadmap and changelog.
This commit is contained in:
1 parent
74dc1fdc1d
commit
37f925c7ad
39 files changed
+1015
-96
No files matched your search
@@ -53,7 +53,11 @@ const SHOTS_LATE := [
|
||||
{"name": "street_late", "pos": Vector3(0, 0.1, 10.5), "yaw": 180.0, "pitch": 2.0},
|
||||
]
|
||||
const SHOTS_JOKE := [
|
||||
{"name": "joke_shop_door", "pos": Vector3(2.2, 0.1, 7.0), "yaw": -20.0, "pitch": 2.0},
|
||||
{"name": "joke_shop_door", "pos": Vector3(-2.0, 0.1, 13.2), "yaw": 12.0, "pitch": 12.0},
|
||||
]
|
||||
const SHOTS_JOKE_DAY := [
|
||||
{"name": "joke_shop_door_day", "pos": Vector3(-2.0, 0.1, 13.2), "yaw": 12.0, "pitch": 12.0},
|
||||
{"name": "joke_shop_overview_day", "pos": Vector3(0.0, 22.0, 26.0), "yaw": 0.0, "pitch": -20.0},
|
||||
]
|
||||
const SHOTS_STUDIO := [
|
||||
{"name": "studio_door", "pos": Vector3(0, 0.1, 6.2), "yaw": 0.0, "pitch": 2.0},
|
||||
@@ -71,6 +75,7 @@ const STAGES := [
|
||||
"night": false},
|
||||
{"name": "evening", "elapsed": 620.0, "venue": "", "shots": SHOTS_EVENING, "night": true},
|
||||
{"name": "late", "elapsed": 760.0, "venue": "", "shots": SHOTS_LATE, "night": true},
|
||||
{"name": "joke_shop_day", "elapsed": 0.0, "venue": "joke_shop", "shots": SHOTS_JOKE_DAY, "night": false},
|
||||
{"name": "joke_shop", "elapsed": 700.0, "venue": "joke_shop", "shots": SHOTS_JOKE, "night": true},
|
||||
{"name": "studio", "elapsed": 700.0, "venue": "tv_studio", "shots": SHOTS_STUDIO, "night": true},
|
||||
]
|
||||
|
||||
@@ -116,8 +116,17 @@ func _advance() -> void:
|
||||
gm.guard.rotation.y = 0.0
|
||||
gm.guard.head.rotation.x = deg_to_rad(4.0)
|
||||
_reset_hud()
|
||||
if state_frames == 26 and gm != null:
|
||||
# Light the facade on the final frames only: turning it on early
|
||||
# made nearby guests log "notices you watching" over the shot.
|
||||
gm.hud.clear_log()
|
||||
gm.call("_toggle_flashlight")
|
||||
if state_frames >= 30:
|
||||
if state_frames == 29 and gm != null:
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_target_label("")
|
||||
if state_frames >= 32:
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_target_label("")
|
||||
_shoot("06-night-flashlight.png")
|
||||
# Move to The Joke Shop for the wand/shift-review set, and keep
|
||||
# the late-night phase so the UI sits on the night scene.
|
||||
@@ -154,6 +163,12 @@ func _advance() -> void:
|
||||
else:
|
||||
gm.call("_cycle_nearby_target")
|
||||
_face_target()
|
||||
# Stage the documented false-alarm case (keys) so the capture
|
||||
# shows the interesting alert instead of a clear guest.
|
||||
var guest: Object = gm.call("_targeted_controller")
|
||||
if guest != null and guest.profile != null:
|
||||
var false_alarm: Array[String] = ["keys"]
|
||||
guest.profile.carried_items = false_alarm
|
||||
gm.call("_toggle_wand_screening")
|
||||
wand_attempts += 1
|
||||
elif state_frames > 600 and gm != null:
|
||||
@@ -172,6 +187,10 @@ func _advance() -> void:
|
||||
if gm != null:
|
||||
if gm.wand_open:
|
||||
gm.call("_toggle_wand_screening")
|
||||
# The fallback path can show the wand HUD without setting
|
||||
# `wand_open`; always hide it before the next screens.
|
||||
gm.hud.hide_wand()
|
||||
gm.wand_open = false
|
||||
# Real escalation path: formal warning arms the body camera
|
||||
# (a timeline row), then blocking raises the meter to L3.
|
||||
gm.call("_apply_interaction", 2, "formal warning")
|
||||
@@ -198,14 +217,33 @@ func _advance() -> void:
|
||||
_shoot("09-body-camera-timeline.png")
|
||||
if gm != null:
|
||||
gm.event_timeline.close()
|
||||
gm.guard.global_position = Vector3(0, 0.1, 6.2)
|
||||
gm.guard.rotation.y = 0.0
|
||||
# Precision walk at the Joke Shop entrance, not a blank wall:
|
||||
# face the red curtain door and keep the step line in frame.
|
||||
gm.guard.global_position = Vector3(-5.2, 0.1, 6.4)
|
||||
gm.guard.rotation.y = deg_to_rad(22.0)
|
||||
gm.guard.head.rotation.x = deg_to_rad(2.0)
|
||||
gm.hud.set_body_camera(false)
|
||||
gm.hud.set_escalation(0)
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_target_label("")
|
||||
Input.action_press("move_forward")
|
||||
Input.action_press("walk_slow")
|
||||
state = 11
|
||||
state_frames = 0
|
||||
11:
|
||||
if state_frames == 1 and gm != null:
|
||||
if gm.wand_open:
|
||||
gm.call("_toggle_wand_screening")
|
||||
gm.hud.hide_wand()
|
||||
gm.wand_open = false
|
||||
if gm.inspection_open:
|
||||
gm.call("_toggle_inspection")
|
||||
gm.hud.hide_inspection()
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_target_label("")
|
||||
if state_frames >= 50:
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_target_label("")
|
||||
_shoot("11-precision-walk.png")
|
||||
Input.action_release("move_forward")
|
||||
Input.action_release("walk_slow")
|
||||
@@ -216,9 +254,18 @@ func _advance() -> void:
|
||||
state_frames = 0
|
||||
12:
|
||||
if state_frames == 1 and gm != null:
|
||||
gm.guard.global_position = Vector3(-1.5, 0.1, 8.5)
|
||||
gm.guard.rotation.y = deg_to_rad(-5.0)
|
||||
gm.guard.head.rotation.x = deg_to_rad(2.0)
|
||||
# The wand fallback can leave the panel open; close every modal
|
||||
# before the venue beauty shot.
|
||||
if gm.wand_open:
|
||||
gm.call("_toggle_wand_screening")
|
||||
gm.hud.hide_wand()
|
||||
gm.wand_open = false
|
||||
if gm.inspection_open:
|
||||
gm.call("_toggle_inspection")
|
||||
gm.hud.hide_inspection()
|
||||
gm.guard.global_position = Vector3(-2.0, 0.1, 13.2)
|
||||
gm.guard.rotation.y = deg_to_rad(12.0)
|
||||
gm.guard.head.rotation.x = deg_to_rad(12.0)
|
||||
gm.hud.clear_log()
|
||||
gm.hud.set_escalation(0)
|
||||
gm.hud.set_body_camera(false)
|
||||
|
||||
@@ -12,7 +12,14 @@ func test_strings_lookup() -> bool:
|
||||
func test_strings_format() -> bool:
|
||||
var a := TestUtils.check_eq(Strings.fmt("react.comply", ["Tourist"]), "Tourist complies and steps back.", "reaction formats")
|
||||
var b := TestUtils.check_eq(Strings.fmt("hud.venue_switched", ["The Joke Shop"]), "Venue: The Joke Shop", "venue switch formats")
|
||||
return a and b
|
||||
# JSON needs one escaped newline; two backslashes used to reach the HUD as a
|
||||
# literal "\n" in the shift status and wand panel.
|
||||
var status := Strings.lookup("hud.shift_status")
|
||||
var c := TestUtils.check(status.contains("\n") and not status.contains("\\n"),
|
||||
"multiline HUD strings use real newlines")
|
||||
var d := TestUtils.check_eq(Strings.fmt("hud.wand_items", ["Keys, Phone"]),
|
||||
"Carried items: Keys, Phone", "wand item list formats")
|
||||
return a and b and c and d
|
||||
|
||||
func test_strings_json_backed_all_resolve() -> bool:
|
||||
## Every key in the JSON-loaded table must resolve to a value other than
|
||||
@@ -92,6 +99,7 @@ func test_strings_cover_ui_keys() -> bool:
|
||||
var required := [
|
||||
"hud.controls", "hud.escalation_prefix", "hud.cooldown", "hud.venue_switched",
|
||||
"hud.aim_first", "hud.no_one_nearby", "hud.targeting", "hud.no_wand",
|
||||
"hud.wand_items", "hud.shift_status",
|
||||
"hud.screen_first", "access.granted", "access.denied", "access.policy_satisfied",
|
||||
"access.wrong_grant", "access.wrong_deny", "wand.admitted_prohibited",
|
||||
"wand.denied_false_alarm", "wand.cleared", "wand.refused", "wand.recharge", "radio.address_first",
|
||||
|
||||
@@ -27,7 +27,7 @@ func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool:
|
||||
var a := TestUtils.check(hollywood.venue_id == "hollywood_building",
|
||||
"the default venue is Hollywood")
|
||||
var b := TestUtils.check(float(hollywood.spec["facade_width"]) > float(joke.spec["facade_width"]),
|
||||
"the Joke Shop alley is narrower than the boulevard")
|
||||
"the Joke Shop frontage is slightly narrower than the boulevard")
|
||||
var c := TestUtils.check(not bool(joke.spec["features"]["skyline"])
|
||||
and not bool(joke.spec["features"]["marquee"]),
|
||||
"the Joke Shop disables the skyline and marquee")
|
||||
@@ -41,8 +41,11 @@ func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool:
|
||||
builder.build()
|
||||
var body := root.find_child("Building", true, false)
|
||||
var facade := body.get_child(0) as MeshInstance3D if body != null else null
|
||||
var f := TestUtils.check(facade != null and (facade.mesh as BoxMesh).size.x < 20.0,
|
||||
"the Joke Shop builds the narrow facade")
|
||||
var f := TestUtils.check(facade != null
|
||||
and str(builder.spec.get("architecture", "")) == "comedy_store"
|
||||
and (facade.mesh as BoxMesh).size.x > 24.0
|
||||
and (facade.mesh as BoxMesh).size.x < 30.0,
|
||||
"the Joke Shop builds its 27 m Sunset frontage")
|
||||
var g := TestUtils.check(root.find_child("SkylineBackdrop", true, false) == null,
|
||||
"the Joke Shop builds no skyline ring")
|
||||
hollywood.root.free()
|
||||
@@ -53,6 +56,47 @@ func test_venue_geometry_specs_are_distinct_and_defaulted() -> bool:
|
||||
return a and b and c and d and e and f and g
|
||||
|
||||
|
||||
func test_joke_shop_uses_the_real_sunset_strip_layout() -> bool:
|
||||
var root := Node3D.new()
|
||||
var builder := WorldBuilder.new(root, "joke_shop")
|
||||
builder.build()
|
||||
var a := TestUtils.check(root.find_child("SignatureCanopy", true, false) != null,
|
||||
"the signature-covered canopy is built")
|
||||
var sign := root.find_child("ComedySignLabel", true, false) as Label3D
|
||||
var b := TestUtils.check(sign != null and sign.text.contains("JOKE")
|
||||
and sign.text.contains("SHOP"), "the circular blade sign carries the club wordmark")
|
||||
var c := TestUtils.check(root.get_node_or_null("SunsetHillside/DryHillside") != null,
|
||||
"the dry hillside rises behind the club")
|
||||
var d := TestUtils.check(root.get_node_or_null("SunsetNeighbours/WestMediterraneanBlock") != null
|
||||
and root.get_node_or_null("SunsetNeighbours/EastCommercialBlock") != null,
|
||||
"the west and east Sunset neighbours are built")
|
||||
var e := TestUtils.check(root.find_child("SunsetAcrossStreet", true, false) != null,
|
||||
"the opposite side of Sunset is built as background context")
|
||||
var f := TestUtils.check(root.find_child("EntranceStep", true, false) != null,
|
||||
"the low entrance step is built")
|
||||
var g := TestUtils.check(root.find_child("AlleyWall", true, false) == null,
|
||||
"the old alley walls are gone")
|
||||
var placements: Array = builder.spec.get("prop_placements", [])
|
||||
var h := TestUtils.check(not placements.is_empty()
|
||||
and WorldBuilder.placement_problems(placements).is_empty(),
|
||||
"the Joke Shop prop placements stay on the sidewalk and clear the lane")
|
||||
root.free()
|
||||
return a and b and c and d and e and f and g and h
|
||||
|
||||
func test_joke_shop_geometry_stays_inside_the_map_budget() -> bool:
|
||||
var root := Node3D.new()
|
||||
var builder := WorldBuilder.new(root, "joke_shop")
|
||||
builder.build()
|
||||
var triangles := builder.world_triangle_count()
|
||||
var meshes := builder.world_mesh_count()
|
||||
print("joke shop map: %d triangles across %d meshes" % [triangles, meshes])
|
||||
var a := TestUtils.check(triangles < 150000,
|
||||
"Joke Shop triangle budget stays under 150k (got %d)" % triangles)
|
||||
var b := TestUtils.check(meshes < 700,
|
||||
"Joke Shop draw-call count stays under 700 meshes (got %d)" % meshes)
|
||||
root.free()
|
||||
return a and b
|
||||
|
||||
func test_hollywood_uses_the_real_landmark_layout() -> bool:
|
||||
var root := Node3D.new()
|
||||
var builder := WorldBuilder.new(root)
|
||||
|
||||
Reference in new issue
Block a user