Visual pass: grid materials, corridor lights, emissive props, target marker, objectives

Portal-style triplanar grid on all structure, omni lighting per segment,
emissive glow on every interactable, floating look-target marker, socket
gets a visible empty-ring slot, HUD gains an OBJECTIVE tracker line.
Integration playthrough still 19/19.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
type-two 2026-08-03 19:39:14 +10:00
parent d414ccd6e4
commit 5f67b3c6b0
2 changed files with 166 additions and 34 deletions

View File

@ -19,6 +19,9 @@ class Door extends StaticBody3D:
mi.position.y = height / 2.0
var mat := StandardMaterial3D.new()
mat.albedo_color = tint
mat.emission_enabled = true
mat.emission = tint
mat.emission_energy_multiplier = 0.3
mi.material_override = mat
add_child(mi)
var col := CollisionShape3D.new()
@ -61,6 +64,9 @@ class Pickup extends Node3D:
mi.mesh = m
var mat := StandardMaterial3D.new()
mat.albedo_color = tint
mat.emission_enabled = true
mat.emission = tint
mat.emission_energy_multiplier = 0.6
mi.material_override = mat
add_child(mi)
@ -93,6 +99,9 @@ class Bucket extends Node3D:
mi.position.y = 0.21
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.35, 0.4, 0.5)
mat.emission_enabled = true
mat.emission = Color(0.35, 0.5, 0.8)
mat.emission_energy_multiplier = 0.5
mi.material_override = mat
add_child(mi)
# the leaky bucket IS just a decaying exposable state (GDD)
@ -115,12 +124,26 @@ class Socket extends Node3D:
add_to_group("interactable")
var mi := MeshInstance3D.new()
var m := BoxMesh.new()
m.size = Vector3(0.5, 0.5, 0.25)
m.size = Vector3(0.8, 0.8, 0.15)
mi.mesh = m
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.1, 0.1, 0.12)
mat.albedo_color = Color(0.2, 0.2, 0.24)
mi.material_override = mat
add_child(mi)
var ring := MeshInstance3D.new()
var t := TorusMesh.new()
t.inner_radius = 0.16
t.outer_radius = 0.26
ring.mesh = t
ring.rotation.x = PI / 2.0
ring.position.z = -0.1
var rmat := StandardMaterial3D.new()
rmat.albedo_color = Color(0.95, 0.55, 0.1)
rmat.emission_enabled = true
rmat.emission = Color(0.95, 0.55, 0.1)
rmat.emission_energy_multiplier = 1.4
ring.material_override = rmat
add_child(ring)
func interact(droid: Node3D) -> void:
if filled:
@ -137,7 +160,7 @@ class Socket extends Node3D:
m.height = 0.12
mi.mesh = m
mi.rotation.x = PI / 2.0
mi.position.z = 0.18
mi.position.z = -0.18
add_child(mi)
door.open()
droid.toast("WHEEL DONATED. DOOR BUS COMPLETE. MOBILITY REDUCED.")
@ -161,6 +184,9 @@ class Tank extends Node3D:
mi.position.y = 0.6
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.2, 0.3, 0.45)
mat.emission_enabled = true
mat.emission = Color(0.2, 0.4, 0.7)
mat.emission_energy_multiplier = 0.5
mi.material_override = mat
add_child(mi)
@ -192,6 +218,9 @@ class Plate extends Node3D:
mi.mesh = m
var mat := StandardMaterial3D.new()
mat.albedo_color = Color(0.6, 0.55, 0.2)
mat.emission_enabled = true
mat.emission = Color(0.8, 0.7, 0.2)
mat.emission_energy_multiplier = 0.6
mi.material_override = mat
add_child(mi)

View File

@ -29,6 +29,7 @@ var sun_light: DirectionalLight3D
var drum: Node3D
var drum_turned := false
var _marker: Label3D
var t_sol := 10.0 # start mid-day
var is_day := true
@ -45,6 +46,28 @@ func _ready() -> void:
_build_objects()
_build_droid()
_build_hud()
if "--shot" in OS.get_cmdline_user_args():
_photo_tour()
func _photo_tour() -> void:
var spots := [
[Vector3(0, 0.2, 2), PI], # spawn, facing door1
[Vector3(1.4, 0.2, 6.2), PI], # socket close-up
[Vector3(0, 0.2, 9), PI], # segment B: post + bus gap
[Vector3(-0.5, 0.2, 17.5), PI], # segment C: drum bay + puddle
[Vector3(-1.5, 0.2, 27), PI], # segment D: valve + tank
[Vector3(0, 0.2, 35), PI], # segment E: skylight + solar
]
for i in spots.size():
droid.global_position = spots[i][0]
droid.rotation.y = spots[i][1]
for f in 20:
await get_tree().process_frame
await RenderingServer.frame_post_draw
var img := get_viewport().get_texture().get_image()
img.save_png("/tmp/exposure_shot_%d.png" % i)
get_tree().quit()
# ---------- input ----------
@ -65,6 +88,20 @@ func _init_input() -> void:
# ---------- construction helpers ----------
var _grid_tex: ImageTexture
func grid_tex() -> ImageTexture:
if not _grid_tex:
var img := Image.create(64, 64, false, Image.FORMAT_RGB8)
for y in 64:
for x in 64:
var line := x % 16 == 0 or y % 16 == 0
img.set_pixel(x, y, Color(0.5, 0.5, 0.5) if line else Color(0.92, 0.92, 0.92))
_grid_tex = ImageTexture.create_from_image(img)
return _grid_tex
func mat(col: Color) -> StandardMaterial3D:
if not _mats.has(col):
var m := StandardMaterial3D.new()
@ -73,12 +110,36 @@ func mat(col: Color) -> StandardMaterial3D:
return _mats[col]
func box(pos: Vector3, size: Vector3, col: Color, parent: Node3D = null) -> CSGBox3D:
func struct_mat(col: Color) -> StandardMaterial3D:
var key := "g" + str(col)
if not _mats.has(key):
var m := StandardMaterial3D.new()
m.albedo_color = col
m.albedo_texture = grid_tex()
m.uv1_triplanar = true
m.uv1_scale = Vector3(0.25, 0.25, 0.25) # 1m grid cells
_mats[key] = m
return _mats[key]
func glow(col: Color, energy := 0.7) -> StandardMaterial3D:
var key := "e" + str(col) + str(energy)
if not _mats.has(key):
var m := StandardMaterial3D.new()
m.albedo_color = col
m.emission_enabled = true
m.emission = col
m.emission_energy_multiplier = energy
_mats[key] = m
return _mats[key]
func box(pos: Vector3, size: Vector3, col: Color, parent: Node3D = null, grid := false) -> CSGBox3D:
var b := CSGBox3D.new()
b.size = size
b.position = pos
b.use_collision = true
b.material = mat(col)
b.material = struct_mat(col) if grid else mat(col)
(parent if parent else self).add_child(b)
return b
@ -95,10 +156,10 @@ func sign3d(text: String, pos: Vector3, rot_y := PI, size := 30, col := Color(0.
func divider(z: float) -> void:
var c := Color(0.3, 0.3, 0.34)
box(Vector3(-2.6, 2, z), Vector3(2.8, 4, 0.3), c)
box(Vector3(2.6, 2, z), Vector3(2.8, 4, 0.3), c)
box(Vector3(0, 3.5, z), Vector3(2.4, 1, 0.3), c)
var c := Color(0.45, 0.46, 0.52)
box(Vector3(-2.6, 2, z), Vector3(2.8, 4, 0.3), c, null, true)
box(Vector3(2.6, 2, z), Vector3(2.8, 4, 0.3), c, null, true)
box(Vector3(0, 3.5, z), Vector3(2.4, 1, 0.3), c, null, true)
func make_door(z: float) -> Node3D:
@ -133,35 +194,50 @@ func _build_env() -> void:
sun_light.light_energy = 1.2
add_child(sun_light)
# corridor lighting — segments feel lit, not fog-grey
for z in [4.0, 12.0, 18.5, 24.0, 30.0, 37.0, 42.5]:
var o := OmniLight3D.new()
o.position = Vector3(0, 3.4, z)
o.omni_range = 10.0
o.light_energy = 1.1
o.light_color = Color(1.0, 0.96, 0.88)
add_child(o)
for p in [Vector3(8, 6.5, 22), Vector3(6.4, 3.2, 25.8), Vector3(-5.0, 3.0, 24)]:
var o := OmniLight3D.new()
o.position = p
o.omni_range = 8.0
o.light_energy = 1.0
add_child(o)
func _build_geometry() -> void:
var wall := Color(0.28, 0.29, 0.33)
var wall := Color(0.5, 0.52, 0.58)
# floor
box(Vector3(3, -0.15, 22.5), Vector3(18.6, 0.3, 46), Color(0.36, 0.36, 0.38))
box(Vector3(3, -0.15, 22.5), Vector3(18.6, 0.3, 46), Color(0.62, 0.62, 0.65), null, true)
# left wall x=-4.15, niche gap z 23.4..24.6
box(Vector3(-4.15, 2, 11.45), Vector3(0.3, 4, 23.9), wall)
box(Vector3(-4.15, 2, 33.55), Vector3(0.3, 4, 17.9), wall)
box(Vector3(-4.15, 2, 11.45), Vector3(0.3, 4, 23.9), wall, null, true)
box(Vector3(-4.15, 2, 33.55), Vector3(0.3, 4, 17.9), wall, null, true)
# relay niche (behind left wall) + bars
box(Vector3(-5.8, 2, 24), Vector3(0.3, 4, 1.6), wall)
box(Vector3(-5.0, 2, 23.4), Vector3(1.6, 4, 0.3), wall)
box(Vector3(-5.0, 2, 24.6), Vector3(1.6, 4, 0.3), wall)
box(Vector3(-5.8, 2, 24), Vector3(0.3, 4, 1.6), wall, null, true)
box(Vector3(-5.0, 2, 23.4), Vector3(1.6, 4, 0.3), wall, null, true)
box(Vector3(-5.0, 2, 24.6), Vector3(1.6, 4, 0.3), wall, null, true)
box(Vector3(-4.15, 1, 23.8), Vector3(0.1, 2, 0.1), Color(0.15, 0.15, 0.15))
box(Vector3(-4.15, 1, 24.2), Vector3(0.1, 2, 0.1), Color(0.15, 0.15, 0.15))
# right wall x=4.15: gaps for drum bay (z 20..24) and bonus bay (z 25..26.6)
box(Vector3(4.15, 2, 9.75), Vector3(0.3, 4, 20.5), wall)
box(Vector3(4.15, 2, 24.5), Vector3(0.3, 4, 1.0), wall)
box(Vector3(4.15, 2, 34.55), Vector3(0.3, 4, 15.9), wall)
box(Vector3(4.15, 2, 9.75), Vector3(0.3, 4, 20.5), wall, null, true)
box(Vector3(4.15, 2, 24.5), Vector3(0.3, 4, 1.0), wall, null, true)
box(Vector3(4.15, 2, 34.55), Vector3(0.3, 4, 15.9), wall, null, true)
# bonus bay shell
box(Vector3(8.65, 2, 25.8), Vector3(0.3, 4, 3.0), wall)
box(Vector3(6.4, 2, 24.9), Vector3(4.4, 4, 0.3), wall)
box(Vector3(6.4, 2, 26.7), Vector3(4.4, 4, 0.3), wall)
box(Vector3(8.65, 2, 25.8), Vector3(0.3, 4, 3.0), wall, null, true)
box(Vector3(6.4, 2, 24.9), Vector3(4.4, 4, 0.3), wall, null, true)
box(Vector3(6.4, 2, 26.7), Vector3(4.4, 4, 0.3), wall, null, true)
# drum bay outer shell
box(Vector3(12.15, 3.5, 22), Vector3(0.3, 8, 6), wall)
box(Vector3(8, 3.5, 19.3), Vector3(8, 8, 0.3), wall)
box(Vector3(8, 3.5, 24.7), Vector3(8, 8, 0.3), wall)
box(Vector3(12.15, 3.5, 22), Vector3(0.3, 8, 6), wall, null, true)
box(Vector3(8, 3.5, 19.3), Vector3(8, 8, 0.3), wall, null, true)
box(Vector3(8, 3.5, 24.7), Vector3(8, 8, 0.3), wall, null, true)
# end walls
box(Vector3(0, 2, -0.5), Vector3(8.6, 4, 0.3), wall)
box(Vector3(0, 2, 44.5), Vector3(8.6, 4, 0.3), wall)
box(Vector3(0, 2, -0.5), Vector3(8.6, 4, 0.3), wall, null, true)
box(Vector3(0, 2, 44.5), Vector3(8.6, 4, 0.3), wall, null, true)
# dividers + doors
for z in [8.0, 16.0, 26.0, 34.0, 40.0]:
divider(z)
@ -189,14 +265,14 @@ func _build_objects() -> void:
Vector3(-2.0, 2.2, 4.0), PI, 22, Color(1.0, 0.75, 0.3))
# --- segment B: charge post + be-the-wire gap ---
box(Vector3(-2.5, 0.9, 10.5), Vector3(0.5, 1.8, 0.5), Color(0.2, 0.5, 0.6))
box(Vector3(-2.5, 0.9, 10.5), Vector3(0.5, 1.8, 0.5), Color(0.2, 0.5, 0.6)).material = glow(Color(0.15, 0.7, 0.85))
var post1 := ElementField.make("electricity", 1.3, Color(0.3, 1.0, 1.0, 0.12))
post1.position = Vector3(-2.5, 0.9, 10.5)
add_child(post1)
sign3d("CHARGE POST", Vector3(-2.5, 2.3, 10.2))
for x in [-1.0, 1.0]:
box(Vector3(x, 1.0, 15.3), Vector3(0.25, 2.0, 0.25), Color(0.55, 0.55, 0.6))
box(Vector3(x, 1.0, 15.3), Vector3(0.25, 2.0, 0.25), Color(0.55, 0.55, 0.6)).material = glow(Color(0.8, 0.8, 0.5), 0.5)
var contact_trans := [{"from": "idle", "to": "hot", "element": "electricity",
"duration": 0.15, "decay_to": "idle", "decay_s": 0.6}]
contact_a = make_exposable("BUS CONTACT A", "idle", contact_trans.duplicate(true), Vector3(-1.0, 1.0, 15.3))
@ -211,7 +287,7 @@ func _build_objects() -> void:
sign3d("SPARE MOBILITY UNIT — GC CAT. 6601-W", Vector3(2.5, 1.4, 17.8))
# --- segment C: post 2, drum bay, puddle + relay ---
box(Vector3(-2.8, 0.9, 20.5), Vector3(0.5, 1.8, 0.5), Color(0.2, 0.5, 0.6))
box(Vector3(-2.8, 0.9, 20.5), Vector3(0.5, 1.8, 0.5), Color(0.2, 0.5, 0.6)).material = glow(Color(0.15, 0.7, 0.85))
var post2 := ElementField.make("electricity", 1.3, Color(0.3, 1.0, 1.0, 0.12))
post2.position = Vector3(-2.8, 0.9, 20.5)
add_child(post2)
@ -236,7 +312,7 @@ func _build_objects() -> void:
crate.position = Vector3(-2.6, 2.65, 0)
drum.add_child(crate)
box(Vector3(3.7, 0.7, 18.6), Vector3(0.3, 1.4, 0.3), Color(0.5, 0.4, 0.2))
box(Vector3(3.7, 0.7, 18.6), Vector3(0.3, 1.4, 0.3), Color(0.5, 0.4, 0.2)).material = glow(Color(0.9, 0.6, 0.15), 0.5)
drum_switch = make_exposable("DRUM MOTOR BUS", "idle",
[{"from": "idle", "to": "active", "element": "electricity",
"duration": 0.8, "permanent": true}],
@ -261,7 +337,7 @@ func _build_objects() -> void:
puddle_live.enabled = false
add_child(puddle_live)
box(Vector3(-5.0, 1.0, 24), Vector3(0.4, 0.4, 0.4), Color(0.6, 0.3, 0.3))
box(Vector3(-5.0, 1.0, 24), Vector3(0.4, 0.4, 0.4), Color(0.6, 0.3, 0.3)).material = glow(Color(0.9, 0.25, 0.2), 0.6)
relay = make_exposable("SLUICE RELAY", "open",
[{"from": "open", "to": "tripped", "element": "electricity",
"duration": 1.0, "permanent": true}],
@ -287,8 +363,8 @@ func _build_objects() -> void:
Vector3(7.2, 2.0, 25.8), PI / 2.0, 24, Color(1.0, 0.75, 0.3))
# --- segment D: valve, bucket, tank ---
box(Vector3(-3.8, 1.4, 28), Vector3(0.5, 0.5, 0.5), Color(0.3, 0.5, 0.7))
box(Vector3(-3.4, 0.9, 28), Vector3(0.12, 1.6, 0.12), Color(0.3, 0.5, 0.9)) # stream
box(Vector3(-3.8, 1.4, 28), Vector3(0.5, 0.5, 0.5), Color(0.3, 0.5, 0.7)).material = glow(Color(0.25, 0.5, 0.9), 0.5)
box(Vector3(-3.4, 0.9, 28), Vector3(0.12, 1.6, 0.12), Color(0.3, 0.5, 0.9)).material = glow(Color(0.3, 0.55, 1.0), 0.9) # stream
var valve := ElementField.make("water", 1.3, Color(0.2, 0.4, 1.0, 0.10))
valve.position = Vector3(-3.3, 0.8, 28)
add_child(valve)
@ -310,6 +386,7 @@ func _build_objects() -> void:
sun_field.position = Vector3(0, 2.2, 37)
add_child(sun_field)
var panel := box(Vector3(1.5, 1.5, 37.3), Vector3(1.2, 0.8, 0.1), Color(0.1, 0.15, 0.4))
panel.material = glow(Color(0.15, 0.25, 0.7), 0.5)
panel.rotation.x = -0.5
solar = make_exposable("SOLAR CELL", "unpowered",
[{"from": "unpowered", "to": "powered", "element": "light",
@ -354,6 +431,15 @@ func _build_hud() -> void:
cross.add_theme_color_override("font_color", Color(0.55, 1.0, 0.6, 0.7))
cl.add_child(cross)
_marker = Label3D.new()
_marker.text = ""
_marker.font_size = 72
_marker.modulate = Color(0.55, 1.0, 0.6)
_marker.billboard = BaseMaterial3D.BILLBOARD_ENABLED
_marker.no_depth_test = true
_marker.visible = false
add_child(_marker)
# ---------- rules ----------
@ -425,12 +511,25 @@ func _process(_delta: float) -> void:
holding += " [%s%s]" % [ex.state.to_upper(),
" %ds" % ceil(ex.decay_remaining()) if ex.decay_remaining() > 0 else ""]
var obj := "WALK OUT THE EXIT."
if not door1.is_open:
obj = "DOOR 1 BUS IS MISSING A COMPONENT. YOU ARE MADE OF COMPONENTS."
elif not door2.is_open:
obj = "BUS GAP AHEAD — GET CHARGED AT THE POST, STAND IN THE GAP."
elif not door3.is_open:
obj = "SEALED BAY — RELAY IS BEHIND THE BARS. READ THE CAUTION SIGN. DISOBEY IT."
elif not door4.is_open:
obj = "GRAV TANK NEEDS 2 POURS. THE BUCKET LEAKS. HUSTLE."
elif not won:
obj = "EXIT RUNS ON SUNLIGHT. CHECK THE SOL CLOCK."
var lines := [
"GENERAL COMPONENT™ — UNIT-77 — MISSION: RESTORE ASSET. RETURN ASSET.",
"SOL: %s%s" % [sol, " ★ ROOM RESTORED — PHASE 0 COMPLETE" if won else ""],
"SHELL: %s | CORE: %s | WHEELS: %d | SPARES: %d | PORT 00: NO COMPONENT SPECIFIED"
% [shell_s, core_s, droid.wheels, droid.spare_wheels],
"HOLDING: %s" % holding,
"OBJECTIVE: %s" % obj,
]
var scan := droid.best_target(["exposable"], 8.0, 0.93)
@ -445,6 +544,10 @@ func _process(_delta: float) -> void:
var target := droid.best_target(["interactable"], 3.0, 0.7)
if target:
lines.append("[E] %s" % target.get("prompt_text"))
_marker.visible = true
_marker.global_position = target.global_position + Vector3(0, 1.1 + 0.12 * sin(t_sol * 5.0), 0)
else:
_marker.visible = false
if droid.stun > 0:
lines.append(">>> REBOOTING <<<")