Detailed graphics pass: authored textures, humanoid NPCs, HUD feedback, asset pipeline
- tools/generate_textures.py (deterministic PIL) produces all authored PNGs: brick facade, concrete, asphalt, awning, skyline, vignette, HUD icons, access-card art (all <=1024px, power-of-two) - WorldBuilder: textured facade/glass door/sidewalk/asphalt, striped awning with 'STEP LINE', generated skyline backdrop planes - GameManager: soft sun shadows (4-split parallel); guard in 'guard' group - NPC avatars: humanoid rig (torso/head/limbs) with archetype props (camera, delivery box, performer hat, protest sign, tourist backpack), procedural walk bob, head-turns-toward-guard attention cue - HUD: inspection artifact cards, mode icons (flashlight/door/wand), per-escalation vignette, body-camera overlay (corner brackets + REC + clock) - assets/ import convention documented; test_asset_textures_present_and_pow2 guards the pipeline (57 tests pass) - README tech stack updated for Gitea Actions + asset pipeline; screenshot gallery regenerated (day + night)
This commit is contained in:
1 parent
ac3bb7dcd8
commit
031503e161
31 files changed
+636
-57
No files matched your search
+52
-12
@@ -1,5 +1,11 @@
|
||||
class_name WorldBuilder
|
||||
## Procedurally assembles a minimal Hollywood Boulevard entrance prototype:
|
||||
|
||||
const T_BRICK := preload("res://assets/textures/brick_facade.png")
|
||||
const T_CONCRETE := preload("res://assets/textures/concrete.png")
|
||||
const T_ASPHALT := preload("res://assets/textures/asphalt.png")
|
||||
const T_AWNING := preload("res://assets/textures/awning.png")
|
||||
const T_SKYLINE := preload("res://assets/textures/skyline.png")
|
||||
## steps, building facade, restricted door, sidewalk, pavement, and street edge.
|
||||
## No art assets required; all geometry is BoxMesh with flat materials.
|
||||
|
||||
@@ -27,14 +33,19 @@ func build() -> void:
|
||||
_build_crosswalk()
|
||||
_build_street_furniture()
|
||||
_build_guard_spawn()
|
||||
_build_skyline_backdrop()
|
||||
_build_navmesh()
|
||||
set_night_mode(false)
|
||||
|
||||
func _add_box(parent: Node3D, size: Vector3, pos: Vector3, color: Color, cast_shadow: bool = false) -> MeshInstance3D:
|
||||
func _add_box(parent: Node3D, size: Vector3, pos: Vector3, color: Color, cast_shadow: bool = false,
|
||||
texture: Texture2D = null, uv_scale: Vector2 = Vector2(1, 1)) -> MeshInstance3D:
|
||||
var mesh := BoxMesh.new()
|
||||
mesh.size = size
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = color
|
||||
if texture:
|
||||
mat.albedo_texture = texture
|
||||
mat.uv1_scale = Vector3(uv_scale.x, uv_scale.y, 1.0)
|
||||
mesh.material = mat
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
@@ -56,19 +67,23 @@ func _build_ground() -> void:
|
||||
func _build_building() -> void:
|
||||
var building := StaticBody3D.new()
|
||||
building.name = "Building"
|
||||
# Main facade
|
||||
_add_box(building, Vector3(40, 18, 4), Vector3(0, 9, -2), Color(0.35, 0.32, 0.30))
|
||||
# Main facade (textured brick)
|
||||
_add_box(building, Vector3(40, 18, 4), Vector3(0, 9, -2), Color(0.85, 0.8, 0.78), false, T_BRICK, Vector2(10, 5))
|
||||
# Entrance recess + door recess frame
|
||||
_add_box(building, Vector3(5.4, 6.4, 0.2), Vector3(0, 3.2, -4.0), Color(0.10, 0.10, 0.10))
|
||||
# Door
|
||||
_add_box(building, Vector3(3.0, 5.2, 0.3), Vector3(0, 2.9, -4.15), Color(0.16, 0.15, 0.13))
|
||||
# Door window
|
||||
_add_box(building, Vector3(1.8, 1.4, 0.34), Vector3(0, 3.8, -4.0), Color(0.5, 0.7, 0.9, 1.0))
|
||||
_add_box(building, Vector3(5.4, 6.4, 0.2), Vector3(0, 3.2, -4.0), Color(0.08, 0.08, 0.09))
|
||||
# Door frame and dark door
|
||||
_add_box(building, Vector3(3.0, 5.2, 0.3), Vector3(0, 2.9, -4.15), Color(0.13, 0.12, 0.11))
|
||||
# Door glass (semi-transparent)
|
||||
var glass := _add_box(building, Vector3(1.8, 1.4, 0.34), Vector3(0, 3.8, -4.0), Color(0.6, 0.75, 0.9, 0.55))
|
||||
var glass_mat := StandardMaterial3D.new()
|
||||
glass_mat.albedo_color = Color(0.6, 0.75, 0.9, 0.45)
|
||||
glass_mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
glass_mat.roughness = 0.15
|
||||
glass_mat.metallic = 0.1
|
||||
glass.material_override = glass_mat
|
||||
# Door lock indicator (green = open, red = locked), placed beside the handle
|
||||
door_indicator = _add_box(building, Vector3(0.18, 0.18, 0.24), Vector3(-1.8, 1.7, -4.35), Color(0.1, 0.9, 0.3))
|
||||
door_indicator.material_override = _lock_indicator_material(Color(0.1, 0.9, 0.3))
|
||||
# Awning sign bar
|
||||
_add_box(building, Vector3(8, 0.5, 0.6), Vector3(0, 7.0, -3.6), Color(0.45, 0.44, 0.44))
|
||||
var col := BoxShape3D.new()
|
||||
col.size = Vector3(40, 18, 4)
|
||||
var so := building.create_shape_owner(building)
|
||||
@@ -96,7 +111,7 @@ func _build_steps() -> void:
|
||||
func _build_sidewalk() -> void:
|
||||
var sidewalk := StaticBody3D.new()
|
||||
sidewalk.name = "Sidewalk"
|
||||
_add_box(sidewalk, Vector3(30, 0.18, 8), Vector3(0, 0.09, 8), Color(0.60, 0.58, 0.56))
|
||||
_add_box(sidewalk, Vector3(30, 0.18, 8), Vector3(0, 0.09, 8), Color(1, 1, 1), false, T_CONCRETE, Vector2(9, 2.4))
|
||||
var col := BoxShape3D.new()
|
||||
col.size = Vector3(30, 0.36, 8)
|
||||
var so := sidewalk.create_shape_owner(sidewalk)
|
||||
@@ -107,7 +122,7 @@ func _build_sidewalk() -> void:
|
||||
func _build_street() -> void:
|
||||
var street := StaticBody3D.new()
|
||||
street.name = "Street"
|
||||
_add_box(street, Vector3(120, 0.1, 26), Vector3(0, 0.05, 22), Color(0.13, 0.13, 0.14))
|
||||
_add_box(street, Vector3(120, 0.1, 26), Vector3(0, 0.05, 22), Color(1, 1, 1), false, T_ASPHALT, Vector2(30, 7))
|
||||
# Center dashes
|
||||
for i in range(-40, 41, 4):
|
||||
_add_box(street, Vector3(0.2, 0.02, 1.2), Vector3(i * 0.0, 0.11, 22), Color(0.85, 0.85, 0.30))
|
||||
@@ -138,6 +153,9 @@ func _build_navmesh() -> void:
|
||||
root.add_child(region)
|
||||
|
||||
func _build_marquee() -> void:
|
||||
## Striped awning above the door, then the neon sign strip + label.
|
||||
var awning := _add_box(root, Vector3(10.5, 0.08, 1.8), Vector3(0, 6.55, -3.7), Color(1, 1, 1), false, T_AWNING)
|
||||
awning.name = "Awning"
|
||||
## Neon-style marquee strip above the door: emissive bars + a label.
|
||||
var strip_parent := Node3D.new()
|
||||
strip_parent.name = "Marquee"
|
||||
@@ -243,6 +261,28 @@ func _plain_material(color: Color) -> StandardMaterial3D:
|
||||
mat.albedo_color = color
|
||||
return mat
|
||||
|
||||
func _build_skyline_backdrop() -> void:
|
||||
## Distant procedural skyline on four unshaded planes around the block.
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_texture = T_SKYLINE
|
||||
mat.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
mat.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
mat.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
const DIST := 95.0
|
||||
const HEIGHT := 90.0
|
||||
for i in 4:
|
||||
var plane := MeshInstance3D.new()
|
||||
plane.name = "Skyline_%d" % i
|
||||
var pm := PlaneMesh.new()
|
||||
pm.size = Vector2(300, HEIGHT)
|
||||
plane.mesh = pm
|
||||
plane.material_override = mat
|
||||
var angle := i * 90.0
|
||||
plane.position = Vector3(sin(deg_to_rad(angle)) * DIST, HEIGHT / 2.0, cos(deg_to_rad(angle)) * DIST)
|
||||
plane.rotation_degrees = Vector3(0, -angle + 90.0, 0)
|
||||
plane.cast_shadow = (0 as GeometryInstance3D.ShadowCastingSetting)
|
||||
root.add_child(plane)
|
||||
|
||||
func _build_guard_spawn() -> void:
|
||||
var marker := Marker3D.new()
|
||||
marker.name = "GuardSpawn"
|
||||
|
||||
Reference in new issue
Block a user