Add office furniture and dim fire-lit horror atmosphere

This commit is contained in:
enne2
2026-10-11 00:42:20 +02:00
parent d74af7b550
commit d6d25fabaa
10 changed files with 326 additions and 8 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

+40
View File
@@ -0,0 +1,40 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bvxhc5epqpwms"
path="res://.godot/imported/flame.png-5c6c1591c52492c60f7d263f90723222.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://assets/effects/flame.png"
dest_files=["res://.godot/imported/flame.png-5c6c1591c52492c60f7d263f90723222.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
+43
View File
@@ -0,0 +1,43 @@
# Office horror atmosphere
`scripts/office_atmosphere.gd` adds two desks, three chairs and a metal fire barrel.
Props use low-poly primitives with simple static collisions on world layer 1.
Positions avoid the four cardinal entry corridors and the central combat space.
The zombies still use their existing direct pursuit, not a new pathfinding system;
props can obstruct arbitrary paths when the player hides behind them.
The central ceiling OmniLight was removed. Ambient energy is 0.12 with a cool blue
color, preserving silhouette readability rather than total darkness. Two warm desk
lamps use short-range unshadowed OmniLights. The barrel is the only persistent
shadow-casting light (range10, energy1.9–2.8), with smooth bounded sine-based flicker.
The existing muzzle flash is unchanged: four omni lights total.
Fire uses 28 CPUParticles3D and small unshaded additive billboard quads. A generated
64x96 alpha flame mask is in assets/effects/flame.png. No glow, volumetric fog, GI,
GPU particles or Forward+-only effects are required. No fire damage was added.
The flame mesh does not cast shadows. The barrel and furniture do.
References consulted (Perplexity unavailable; official docs via Exa):
- https://docs.godotengine.org/en/stable/tutorials/3d/lights_and_shadows.html
- https://docs.godotengine.org/en/stable/classes/class_omnilight3d.html
- https://docs.godotengine.org/en/stable/classes/class_cpuparticles3d.html
Compatibility defaults to eight omni lights per mesh. Shadows are expensive and
can change light brightness because Compatibility uses a different shadowed-light
rendering path; visual checks were therefore done with gl_compatibility.
Verification (installed Godot4.7.1):
```sh
"$G" --headless --path . --script res://tools/test_office_atmosphere.gd
"$G" --display-driver x11 --rendering-method gl_compatibility --path . \
--script res://tools/test_office_atmosphere.gd -- --screenshot
```
Checks: prop counts/collisions, bounded flicker, CPU emitter, light count, absent
ceiling light, low ambient, free cardinal ray corridors and desk collision.
Three screenshots `/tmp/zombie-office-{0,1,2}.png` inspected, including the barrel.
All seven prior suites pass: health/sky, music, grenade, waves, death, damage, touch.
Existing resource-in-use warnings at test shutdown remain. Actual smartphone
performance has not been measured; this is a small light/particle budget, not an
FPS guarantee. Keep testing arbitrary zombie routes around the new obstacles.
+8
View File
@@ -1,5 +1,13 @@
# Web build and compressed deployment # Web build and compressed deployment
## Office atmosphere release: fd65770c62e811fe
See `docs/OFFICE-ATMOSPHERE.md`: two desks, three chairs, two desk lamps and
CPU-particle fire barrel; ceiling light removed, low cool ambient.
PCK 3,946,516 bytes; gzip payload total 12,402,612 bytes. Previous releases retained.
Root backup: `index-before-office.html`. Serial import workaround applied to staging.
## Health + starry sky release: 31564f79b9f5e299 ## Health + starry sky release: 31564f79b9f5e299
See `docs/HEALTH-SKY.md`: +1 HP/second while alive, capped at 100; Half-Life See `docs/HEALTH-SKY.md`: +1 HP/second while alive, capped at 100; Half-Life
+5 -8
View File
@@ -39,16 +39,13 @@ func _ready() -> void:
# Solo sfondo: illuminazione e riflessi non dipendono dal cielo scuro. # Solo sfondo: illuminazione e riflessi non dipendono dal cielo scuro.
env.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED env.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.ambient_light_color = Color(0.65, 0.73, 0.85) env.ambient_light_color = Color(0.28, 0.36, 0.48)
env.ambient_light_energy = 0.65 env.ambient_light_energy = 0.12
envnode.environment = env envnode.environment = env
add_child(envnode) add_child(envnode)
var light := OmniLight3D.new() var office := Node3D.new()
light.position = Vector3(0, 4.5, 0) office.set_script(preload("res://scripts/office_atmosphere.gd"))
light.omni_range = 24 add_child(office)
light.light_energy = 3
light.shadow_enabled = true
add_child(light)
var canvas := CanvasLayer.new() var canvas := CanvasLayer.new()
add_child(canvas) add_child(canvas)
hud = Label.new() hud = Label.new()
+166
View File
@@ -0,0 +1,166 @@
extends Node3D
## Sparse office props and bounded fire animation; Web/Compatibility safe.
const FIRE_MIN := 1.9
const FIRE_MAX := 2.8
var fire_light: OmniLight3D
var _time := 0.0
func _ready() -> void:
name = "OfficeAtmosphere"
_desk(Vector3(-5, 0, -4))
_desk(Vector3(5, 0, 4))
_chair(Vector3(-5, 0, -2.6), 0)
_chair(Vector3(5, 0, 5.4), PI)
_chair(Vector3(6.8, 0, -4), -0.35)
_barrel(Vector3(-4, 0, 4))
func _material(color: Color, metallic: float = 0.0) -> StandardMaterial3D:
var mat := StandardMaterial3D.new()
mat.albedo_color = color
mat.roughness = 0.85
mat.metallic = metallic
return mat
func _box(parent: Node3D, pos: Vector3, size: Vector3, mat: Material) -> MeshInstance3D:
var mesh := BoxMesh.new()
mesh.size = size
mesh.material = mat
var node := MeshInstance3D.new()
node.mesh = mesh
node.position = pos
parent.add_child(node)
return node
func _body(pos: Vector3, label: String) -> StaticBody3D:
var body := StaticBody3D.new()
body.name = label
body.position = pos
body.collision_layer = 1
body.collision_mask = 0
body.add_to_group("office_props")
add_child(body, true)
return body
func _collision(body: StaticBody3D, center: Vector3, size: Vector3) -> void:
var shape := BoxShape3D.new()
shape.size = size
var collider := CollisionShape3D.new()
collider.shape = shape
collider.position = center
body.add_child(collider)
func _desk(pos: Vector3) -> void:
var desk := _body(pos, "OfficeDesk")
var wood := _material(Color(0.19, 0.12, 0.07))
var metal := _material(Color(0.12, 0.15, 0.14), 0.5)
_box(desk, Vector3(0, 0.84, 0), Vector3(2.5, 0.12, 1.15), wood)
for x in [-1.05, 1.05]:
for z in [-0.43, 0.43]:
_box(desk, Vector3(x, 0.4, z), Vector3(0.08, 0.8, 0.08), metal)
_box(desk, Vector3(-0.82, 0.53, 0), Vector3(0.48, 0.5, 0.94), metal)
for y in [0.4, 0.62]:
_box(desk, Vector3(-0.82, y, 0.49), Vector3(0.23, 0.025, 0.035), wood)
_box(desk, Vector3(0.3, 0.91, 0.05), Vector3(0.48, 0.015, 0.35), _material(Color(0.45, 0.43, 0.32)))
_collision(desk, Vector3(0, 0.45, 0), Vector3(2.5, 0.9, 1.15))
_lamp(desk, Vector3(0.84, 0.91, -0.23))
func _chair(pos: Vector3, yaw: float) -> void:
var chair := _body(pos, "OfficeChair")
chair.rotation.y = yaw
var metal := _material(Color(0.12, 0.12, 0.13), 0.6)
var fabric := _material(Color(0.1, 0.14, 0.12))
_box(chair, Vector3(0, 0.48, 0), Vector3(0.58, 0.12, 0.56), fabric)
_box(chair, Vector3(0, 0.84, 0.25), Vector3(0.58, 0.64, 0.09), fabric)
for x in [-0.23, 0.23]:
for z in [-0.22, 0.22]:
_box(chair, Vector3(x, 0.21, z), Vector3(0.045, 0.42, 0.045), metal)
_collision(chair, Vector3(0, 0.53, 0), Vector3(0.6, 1.06, 0.6))
func _cylinder(parent: Node3D, pos: Vector3, radius: float, height: float, mat: Material) -> void:
var mesh := CylinderMesh.new()
mesh.top_radius = radius
mesh.bottom_radius = radius
mesh.height = height
mesh.radial_segments = 24
mesh.material = mat
var node := MeshInstance3D.new()
node.mesh = mesh
node.position = pos
parent.add_child(node)
func _lamp(parent: Node3D, pos: Vector3) -> void:
var lamp := Node3D.new()
lamp.name = "DeskLamp"
lamp.position = pos
parent.add_child(lamp)
var metal := _material(Color(0.13, 0.19, 0.15), 0.5)
_cylinder(lamp, Vector3(0, 0.035, 0), 0.16, 0.07, metal)
_box(lamp, Vector3(0, 0.24, 0), Vector3(0.04, 0.4, 0.04), metal)
_box(lamp, Vector3(0, 0.44, 0), Vector3(0.4, 0.12, 0.24), metal)
var bulb := _material(Color(0.8, 0.63, 0.3))
bulb.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
_box(lamp, Vector3(0, 0.375, 0), Vector3(0.32, 0.012, 0.18), bulb)
var light := OmniLight3D.new()
light.name = "DeskLampLight"
light.position = Vector3(0, 0.31, 0)
light.light_color = Color(1.0, 0.7, 0.35)
light.light_energy = 1.2
light.omni_range = 5.5
light.shadow_enabled = false
lamp.add_child(light)
func _barrel(pos: Vector3) -> void:
var barrel := _body(pos, "FireBarrel")
var metal := _material(Color(0.15, 0.1, 0.075), 0.65)
_cylinder(barrel, Vector3(0, 0.5, 0), 0.43, 1.0, metal)
for y in [0.12, 0.78, 1.01]:
_cylinder(barrel, Vector3(0, y, 0), 0.46, 0.055, metal)
_cylinder(barrel, Vector3(0, 1.045, 0), 0.39, 0.015, _material(Color(0.025, 0.012, 0.005)))
_collision(barrel, Vector3(0, 0.52, 0), Vector3(0.92, 1.04, 0.92))
var fire := CPUParticles3D.new()
fire.name = "BarrelFlames"
fire.position = Vector3(0, 1.08, 0)
fire.amount = 28
fire.lifetime = 0.85
fire.preprocess = 1.0
fire.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE
fire.emission_sphere_radius = 0.23
fire.direction = Vector3.UP
fire.spread = 12
fire.gravity = Vector3(0, 0.6, 0)
fire.initial_velocity_min = 0.5
fire.initial_velocity_max = 1.15
fire.scale_amount_min = 0.5
fire.scale_amount_max = 1.0
var gradient := Gradient.new()
gradient.set_color(0, Color(1, 0.65, 0.12, 0.8))
gradient.set_color(1, Color(0.6, 0.04, 0.005, 0))
fire.color_ramp = gradient
var quad := QuadMesh.new()
quad.size = Vector2(0.48, 0.8)
var flame := StandardMaterial3D.new()
flame.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
flame.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
flame.blend_mode = BaseMaterial3D.BLEND_MODE_ADD
flame.billboard_mode = BaseMaterial3D.BILLBOARD_PARTICLES
flame.albedo_texture = load("res://assets/effects/flame.png")
flame.vertex_color_use_as_albedo = true
flame.cull_mode = BaseMaterial3D.CULL_DISABLED
quad.material = flame
fire.mesh = quad
fire.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
barrel.add_child(fire)
fire_light = OmniLight3D.new()
fire_light.name = "BarrelFireLight"
fire_light.position = Vector3(0, 1.5, 0)
fire_light.light_color = Color(1, 0.36, 0.07)
fire_light.light_energy = (FIRE_MIN + FIRE_MAX) * 0.5
fire_light.omni_range = 10
fire_light.shadow_enabled = true
barrel.add_child(fire_light)
func _process(dt: float) -> void:
_time += dt
# Smooth, deterministic, bounded flicker: no per-frame random flashes.
var wave := 0.5 + 0.3 * sin(_time * 7.1) + 0.2 * sin(_time * 11.7 + 1.2)
fire_light.light_energy = lerpf(FIRE_MIN, FIRE_MAX, wave)
+1
View File
@@ -0,0 +1 @@
uid://cx02g4bsbwanh
+1
View File
@@ -0,0 +1 @@
uid://dvv3ey14k862x
+61
View File
@@ -0,0 +1,61 @@
extends SceneTree
func _initialize() -> void:
call_deferred("run_test")
func run_test() -> void:
var world = load("res://scenes/main.tscn").instantiate()
root.add_child(world)
var office = world.get_node("OfficeAtmosphere")
assert(get_nodes_in_group("office_props").size() == 6)
var desks := 0
var chairs := 0
for prop in get_nodes_in_group("office_props"):
if prop.name.begins_with("OfficeDesk"):
desks += 1
if prop.name.begins_with("OfficeChair"):
chairs += 1
assert(prop.collision_layer == 1)
assert(desks == 2 and chairs == 3)
print("PASS: due scrivanie, tre sedie, bidone con collisioni")
var flames = office.get_node("FireBarrel/BarrelFlames")
assert(flames is CPUParticles3D and flames.amount == 28 and flames.emitting)
assert(office.fire_light.shadow_enabled)
for i in 100:
office._process(0.016)
assert(office.fire_light.light_energy >= office.FIRE_MIN and office.fire_light.light_energy <= office.FIRE_MAX)
print("PASS: fiamme CPU leggere e flicker continuo entro limiti")
var lights: Array[Node] = world.find_children("*", "OmniLight3D", true, false)
assert(lights.size() == 4) # due lampade, bidone, muzzle flash
assert(world.find_children("DeskLampLight", "OmniLight3D", true, false).size() == 2)
for node in world.get_children():
assert(not node is OmniLight3D) # nessuna plafoniera
if node is WorldEnvironment:
assert(node.environment.ambient_light_energy <= 0.15)
print("PASS: plafoniera rimossa, ambient basso e quattro luci totali")
await physics_frame
await physics_frame
var space = world.get_world_3d().direct_space_state
# Cardinal routes from windows to center remain free at zombie torso height.
for edge in [Vector3(0, 0.8, -8), Vector3(0, 0.8, 8), Vector3(-8, 0.8, 0), Vector3(8, 0.8, 0)]:
var q := PhysicsRayQueryParameters3D.create(edge, Vector3(0, 0.8, 0), 1)
q.exclude = [world.player.get_rid()]
assert(space.intersect_ray(q).is_empty())
var q := PhysicsRayQueryParameters3D.create(Vector3(-5, 0.5, -2), Vector3(-5, 0.5, -4), 1)
assert(not space.intersect_ray(q).is_empty())
print("PASS: corridoi cardinali liberi, scrivanie bloccano fisicamente")
if "--screenshot" in OS.get_cmdline_user_args():
world.player.set_physics_process(false)
world.player.position = Vector3(0, 0.1, 0)
world.player.camera.rotation.x = -0.08
for i in 3:
if i == 2:
world.player.look_at(Vector3(-4, 0.1, 4))
else:
world.player.rotation.y = -PI * 0.25 if i == 0 else PI * 0.25 + PI
await create_timer(0.4).timeout
await RenderingServer.frame_post_draw
root.get_texture().get_image().save_png("/tmp/zombie-office-%d.png" % i)
world.queue_free()
await process_frame
quit()
+1
View File
@@ -0,0 +1 @@
uid://dbyackx6d67ns