diff --git a/assets/effects/flame.png b/assets/effects/flame.png new file mode 100644 index 0000000..f33b5af Binary files /dev/null and b/assets/effects/flame.png differ diff --git a/assets/effects/flame.png.import b/assets/effects/flame.png.import new file mode 100644 index 0000000..9ffbc70 --- /dev/null +++ b/assets/effects/flame.png.import @@ -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 diff --git a/docs/OFFICE-ATMOSPHERE.md b/docs/OFFICE-ATMOSPHERE.md new file mode 100644 index 0000000..c44cb9b --- /dev/null +++ b/docs/OFFICE-ATMOSPHERE.md @@ -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. diff --git a/docs/WEB-DEPLOY.md b/docs/WEB-DEPLOY.md index 936e6c2..ec8aceb 100644 --- a/docs/WEB-DEPLOY.md +++ b/docs/WEB-DEPLOY.md @@ -1,5 +1,13 @@ # 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 See `docs/HEALTH-SKY.md`: +1 HP/second while alive, capped at 100; Half-Life diff --git a/scripts/main.gd b/scripts/main.gd index 69a8bc4..8623ad5 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -39,16 +39,13 @@ func _ready() -> void: # Solo sfondo: illuminazione e riflessi non dipendono dal cielo scuro. env.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR - env.ambient_light_color = Color(0.65, 0.73, 0.85) - env.ambient_light_energy = 0.65 + env.ambient_light_color = Color(0.28, 0.36, 0.48) + env.ambient_light_energy = 0.12 envnode.environment = env add_child(envnode) - var light := OmniLight3D.new() - light.position = Vector3(0, 4.5, 0) - light.omni_range = 24 - light.light_energy = 3 - light.shadow_enabled = true - add_child(light) + var office := Node3D.new() + office.set_script(preload("res://scripts/office_atmosphere.gd")) + add_child(office) var canvas := CanvasLayer.new() add_child(canvas) hud = Label.new() diff --git a/scripts/office_atmosphere.gd b/scripts/office_atmosphere.gd new file mode 100644 index 0000000..1cb1fe7 --- /dev/null +++ b/scripts/office_atmosphere.gd @@ -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) diff --git a/scripts/office_atmosphere.gd.uid b/scripts/office_atmosphere.gd.uid new file mode 100644 index 0000000..606a51f --- /dev/null +++ b/scripts/office_atmosphere.gd.uid @@ -0,0 +1 @@ +uid://cx02g4bsbwanh diff --git a/tools/test_health_sky.gd.uid b/tools/test_health_sky.gd.uid new file mode 100644 index 0000000..656844f --- /dev/null +++ b/tools/test_health_sky.gd.uid @@ -0,0 +1 @@ +uid://dvv3ey14k862x diff --git a/tools/test_office_atmosphere.gd b/tools/test_office_atmosphere.gd new file mode 100644 index 0000000..f78ea8e --- /dev/null +++ b/tools/test_office_atmosphere.gd @@ -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() diff --git a/tools/test_office_atmosphere.gd.uid b/tools/test_office_atmosphere.gd.uid new file mode 100644 index 0000000..5a0db61 --- /dev/null +++ b/tools/test_office_atmosphere.gd.uid @@ -0,0 +1 @@ +uid://dbyackx6d67ns