diff --git a/assets/audio/fvox/flatline.wav b/assets/audio/fvox/flatline.wav new file mode 100755 index 0000000..24a2499 Binary files /dev/null and b/assets/audio/fvox/flatline.wav differ diff --git a/assets/audio/fvox/flatline.wav.import b/assets/audio/fvox/flatline.wav.import new file mode 100644 index 0000000..499b4b5 --- /dev/null +++ b/assets/audio/fvox/flatline.wav.import @@ -0,0 +1,24 @@ +[remap] + +importer="wav" +type="AudioStreamWAV" +uid="uid://dpd2tbv3u062y" +path="res://.godot/imported/flatline.wav-f115126661c17b8f714387eac4171e94.sample" + +[deps] + +source_file="res://assets/audio/fvox/flatline.wav" +dest_files=["res://.godot/imported/flatline.wav-f115126661c17b8f714387eac4171e94.sample"] + +[params] + +force/8_bit=false +force/mono=false +force/max_rate=false +force/max_rate_hz=44100 +edit/trim=false +edit/normalize=false +edit/loop_mode=0 +edit/loop_begin=0 +edit/loop_end=-1 +compress/mode=2 diff --git a/docs/GAMEPLAY-IMPROVEMENTS.md b/docs/GAMEPLAY-IMPROVEMENTS.md new file mode 100644 index 0000000..17c58d2 --- /dev/null +++ b/docs/GAMEPLAY-IMPROVEMENTS.md @@ -0,0 +1,61 @@ +# Classic HUD, automatic reload, furniture navigation, death audio + +## HUD + +`scripts/game_hud.gd` replaces the debug-style text block with translucent black +panels and amber classic-FPS typography. Health and magazine numbers are large, +low health (<=25) and low ammo (<=5) turn red. Reload status is explicit; wave, +score and grenade cooldown are compact at top center. Panels are above touch +controls; every HUD Control ignores mouse events so the Web start gesture works. +Desktop instructions are hidden in touch mode. `30 / 30` means loaded/max magazine, +not a fictional reserve-ammo counter (reloads remain unlimited). + +## Automatic reload + +The last shot triggers `start_reload()` immediately. An empty magazine detected in +physics also triggers it. Guarded against duplicate requests, full magazines and +death. Existing duration1.57s and reload sounds remain; no refill after death. +Manual R and touch reload are unchanged. + +## Shared grid navigation on the room's XZ floor + +`scripts/room_navigation.gd` builds a shared35x35 AStarGrid2D with0.5m cells. +Obstacles come from `office_props` box collisions transformed into world-space +XZ rectangles (including rotated chairs). Rectangles are expanded0.60m to protect +the zombie capsule radius0.35m plus grid/edge margin. Diagonals are allowed only +when both adjacent cells are free. Grid setup calls update BEFORE solid flags, +since update clears them. + +Targets/start cells inside expanded obstacles are snapped to the nearest free +cell. Each zombie caches direct visibility or an A* route, refreshing approximately +every0.55–0.67s with staggered intervals rather than every physics frame. Clear +routes use direct pursuit; blocked routes follow waypoints. Entry window tween, +animations, gravity, attack range and corpse behavior remain unchanged. +Melee also checks a world-geometry ray below desk-top height: chest-high rays can +pass over tables even when the actors cannot physically approach each other. + +This map is for static furniture: moved/new props require rebuilding it. Existing +body collisions still separate zombies, but this is not a full crowd avoidance +solver. No mobile FPS guarantee is made. The algorithm uses the existing simple +flat room and does not require runtime navmesh baking or native extensions. + +Reference: https://docs.godotengine.org/en/stable/classes/class_astargrid2d.html + +## Death sound + +Copied the ORIGINAL local Half-Life `valve/sound/fvox/flatline.wav` to +`assets/audio/fvox/flatline.wav`, no source modification. SHA256: +`6fc00ca83a1310b8ff373a77ff895c5396c35416c00a974a0fa55b3fb14bd772`. +Duration3.994s, mono11025Hz. Played once at-9dB on death alongside the existing +player vocal; nonpositional so it remains audible through the camera fall. + +## Verification + +`tools/test_gameplay_improvements.gd`: HUD values/colors/touch hints, immediate +last-shot reload, duplicate-request guard, physical pursuits around both desks, +chairs (including rotated chair) and barrel, blocked melee through desk, +idempotent flatline and no refill after death. Passed headless and with real +PulseAudio output. All eight prior test suites pass. Touch screenshot of new HUD +inspected via test_office_atmosphere --screenshot --touch. +Existing test-shutdown resource-in-use warnings remain. Device touch feel and +smartphone performance still need real-device validation. diff --git a/docs/OFFICE-ATMOSPHERE.md b/docs/OFFICE-ATMOSPHERE.md index c44cb9b..f904bee 100644 --- a/docs/OFFICE-ATMOSPHERE.md +++ b/docs/OFFICE-ATMOSPHERE.md @@ -3,8 +3,8 @@ `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. +Zombies now route around furniture using the shared XZ grid described in +`GAMEPLAY-IMPROVEMENTS.md`; static collision bounds feed the navigation map. 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 @@ -40,4 +40,4 @@ Three screenshots `/tmp/zombie-office-{0,1,2}.png` inspected, including the barr 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. +FPS guarantee. Physical furniture-pursuit tests are in `test_gameplay_improvements.gd`. diff --git a/docs/WEB-DEPLOY.md b/docs/WEB-DEPLOY.md index ec8aceb..f631d8d 100644 --- a/docs/WEB-DEPLOY.md +++ b/docs/WEB-DEPLOY.md @@ -1,5 +1,13 @@ # Web build and compressed deployment +## Gameplay improvements release: 7ebf03fe3c88be15 + +See `docs/GAMEPLAY-IMPROVEMENTS.md`: amber HUD, automatic reload, shared furniture +pathfinding and original Half-Life flatline. PCK 3,972,620 bytes; gzip payload total +12,424,176 bytes. Previous releases retained; root backup `index-before-gameplay.html`. +Serial staging import workaround retained. + + ## Office atmosphere release: fd65770c62e811fe See `docs/OFFICE-ATMOSPHERE.md`: two desks, three chairs, two desk lamps and diff --git a/scripts/game_hud.gd b/scripts/game_hud.gd new file mode 100644 index 0000000..51452d4 --- /dev/null +++ b/scripts/game_hud.gd @@ -0,0 +1,87 @@ +extends CanvasLayer +## Classic amber HUD, kept above touch sticks/buttons rather than over them. +const AMBER := Color(1.0, 0.66, 0.16) +const DANGER := Color(1.0, 0.22, 0.12) +var health_value: Label +var ammo_value: Label +var ammo_state: Label +var wave_value: Label +var grenade_value: Label +var help: Label +var root: Control + +func _ready() -> void: + layer = 2 + root = Control.new() + root.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT) + root.mouse_filter = Control.MOUSE_FILTER_IGNORE + add_child(root) + var health := _panel("Health", false) + _label(health, "+ VITA", 16, Vector2(16, 10)) + health_value = _label(health, "100", 42, Vector2(16, 27)) + var ammo := _panel("Ammo", true) + _label(ammo, "CARICATORE", 16, Vector2(16, 10)) + ammo_value = _label(ammo, "30 / 30", 36, Vector2(16, 30)) + ammo_state = _label(ammo, "", 14, Vector2(16, 77)) + wave_value = _label(root, "", 18, Vector2.ZERO) + wave_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + wave_value.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) + wave_value.offset_left = -160 + wave_value.offset_right = 160 + wave_value.offset_top = 15 + wave_value.offset_bottom = 45 + grenade_value = _label(root, "", 15, Vector2.ZERO) + grenade_value.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + grenade_value.set_anchors_and_offsets_preset(Control.PRESET_CENTER_TOP) + grenade_value.offset_left = -160 + grenade_value.offset_right = 160 + grenade_value.offset_top = 44 + grenade_value.offset_bottom = 66 + help = _label(root, "WASD: muovi | mouse: mira | R: ricarica | destro: granata", 15, Vector2.ZERO) + help.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER + help.set_anchors_and_offsets_preset(Control.PRESET_CENTER_BOTTOM) + help.offset_left = -420 + help.offset_right = 420 + help.offset_top = -30 + help.offset_bottom = -8 + +func _panel(label: String, right: bool) -> Panel: + var panel := Panel.new() + panel.name = label + panel.mouse_filter = Control.MOUSE_FILTER_IGNORE + var style := StyleBoxFlat.new() + style.bg_color = Color(0.025, 0.02, 0.008, 0.8) + style.border_color = Color(1.0, 0.66, 0.16, 0.45) + style.set_border_width_all(1) + style.set_corner_radius_all(4) + panel.add_theme_stylebox_override("panel", style) + root.add_child(panel) + panel.set_anchors_and_offsets_preset(Control.PRESET_TOP_RIGHT if right else Control.PRESET_TOP_LEFT) + panel.offset_left = -224 if right else 16 + panel.offset_right = -16 if right else 224 + panel.offset_top = 16 + panel.offset_bottom = 116 + return panel + +func _label(parent: Control, text: String, size: int, pos: Vector2) -> Label: + var label := Label.new() + label.text = text + label.position = pos + label.mouse_filter = Control.MOUSE_FILTER_IGNORE + label.add_theme_color_override("font_color", AMBER) + label.add_theme_color_override("font_shadow_color", Color.BLACK) + label.add_theme_constant_override("shadow_offset_x", 1) + label.add_theme_constant_override("shadow_offset_y", 1) + label.add_theme_font_size_override("font_size", size) + parent.add_child(label) + return label + +func update_status(player: Node, wave: int, score: int) -> void: + health_value.text = str(maxi(0, ceili(player.hp))) + health_value.add_theme_color_override("font_color", DANGER if player.hp <= 25 else AMBER) + ammo_value.text = "%02d / 30" % player.ammo + ammo_value.add_theme_color_override("font_color", DANGER if player.ammo <= 5 else AMBER) + ammo_state.text = "RICARICA..." if player.reloading else "" + wave_value.text = "ONDATA %d | PUNTI %d" % [wave, score] + grenade_value.text = "GRANATA: " + ("PRONTA" if player.grenade_cooldown <= 0 else "%ds" % ceili(player.grenade_cooldown)) + help.visible = not player.touch_mode diff --git a/scripts/game_hud.gd.uid b/scripts/game_hud.gd.uid new file mode 100644 index 0000000..3bc1e41 --- /dev/null +++ b/scripts/game_hud.gd.uid @@ -0,0 +1 @@ +uid://cxxu6fixvsjbs diff --git a/scripts/main.gd b/scripts/main.gd index 8623ad5..da330c9 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -10,7 +10,8 @@ var room: Node3D var music: AudioStreamPlayer var music_started := false var touch: Node -var hud: Label +var hud: CanvasLayer +var navigation: Node var score := 0 var wave := 0 var alive := 0 @@ -46,12 +47,14 @@ func _ready() -> void: var office := Node3D.new() office.set_script(preload("res://scripts/office_atmosphere.gd")) add_child(office) + navigation = Node.new() + navigation.set_script(preload("res://scripts/room_navigation.gd")) + add_child(navigation) var canvas := CanvasLayer.new() add_child(canvas) - hud = Label.new() - hud.position = Vector2(24, 20) - hud.add_theme_font_size_override("font_size", 24) - canvas.add_child(hud) + hud = CanvasLayer.new() + hud.set_script(preload("res://scripts/game_hud.gd")) + add_child(hud) var cross := Label.new() cross.text = "+" cross.add_theme_font_size_override("font_size", 30) @@ -141,14 +144,7 @@ func _restart_music() -> void: func _process(_dt: float) -> void: if hud: - hud.text = "VITA %d MUNIZIONI %d/30 ONDATA %d PUNTI %d\nWASD · mouse · sinistro: spara · destro: granata · R: ricarica · spazio: salto · Esc: mouse" % [player.hp, player.ammo, wave, score] - hud.text += "\nGRANATA: " + ("PRONTA" if player.grenade_cooldown <= 0 else "%d s" % ceili(player.grenade_cooldown)) - if player.reloading: - hud.text += "\nRICARICA..." - if player.touch_mode: - hud.text += "\ntrascina a sinistra: muovi · a destra: guarda" - if player.hp <= 0: - hud.text += "\nSEI MORTO — tocca RICOMINCIA" if player.touch_mode else "\nSEI MORTO — premi R per ricominciare" + hud.update_status(player, wave, score) func zombies_for_wave(number: int) -> int: return 4 + number * 2 @@ -173,6 +169,7 @@ func _spawn(index: int) -> void: var zombie := CharacterBody3D.new() zombie.set_script(ZombieScript) zombie.player = player + zombie.navigation = navigation zombie.position = centers[index] - inward * 1.2 add_child(zombie) alive += 1 diff --git a/scripts/player.gd b/scripts/player.gd index f2ec150..2541a24 100644 --- a/scripts/player.gd +++ b/scripts/player.gd @@ -100,6 +100,8 @@ func _unhandled_input(event: InputEvent) -> void: ## Ricarica: tempi invariati, riusata da tastiera (R) e dal pulsante on-screen. func start_reload() -> void: + if reloading or death_started or hp <= 0 or ammo >= 30: + return reloading = true weapon_anim.play("reload") Audio.play(self, "items/cliprelease1.wav") @@ -140,6 +142,8 @@ func _physics_process(dt: float) -> void: get_tree().reload_current_scene() # R o pulsante Ricarica a partita finita return _regenerate_health(dt) + if ammo <= 0 and not reloading: + start_reload() _handle_actions() # Deadzone esplicita piccola (0.2): con la 0.5 delle azioni la corsa utile del # joystick touch si dimezzerebbe, mentre per tastiera/joypad nulla cambia. @@ -176,6 +180,8 @@ func shoot() -> void: if hit and hit.collider.has_method("take_damage"): Blood.impact(get_parent(), hit.position, -camera.global_basis.z) hit.collider.take_damage(35, hit.position.y) + if ammo <= 0: + start_reload() func throw_grenade() -> RigidBody3D: if death_started or hp <= 0 or grenade_cooldown > 0: @@ -219,6 +225,7 @@ func _die() -> void: Input.mouse_mode = Input.MOUSE_MODE_VISIBLE # Original Half-Life player pain sample used as the death event vocal. Audio.play(self, "player/pl_pain7.wav", false, -8) + Audio.play(self, "fvox/flatline.wav", false, -9) var layer := CanvasLayer.new() layer.layer = 5 add_child(layer) diff --git a/scripts/room_navigation.gd b/scripts/room_navigation.gd new file mode 100644 index 0000000..46c6422 --- /dev/null +++ b/scripts/room_navigation.gd @@ -0,0 +1,73 @@ +extends Node +## Shared planar A* map for static office collisions; no runtime navmesh bake. +const CELL := 0.5 +const CLEARANCE := 0.60 # capsule radius .35 + conservative cell/edge margin +var grid := AStarGrid2D.new() +var obstacles: Array[Rect2] = [] + +func _ready() -> void: + name = "RoomNavigation" + grid.region = Rect2i(-17, -17, 35, 35) + grid.cell_size = Vector2(CELL, CELL) + grid.diagonal_mode = AStarGrid2D.DIAGONAL_MODE_ONLY_IF_NO_OBSTACLES + grid.default_compute_heuristic = AStarGrid2D.HEURISTIC_OCTILE + grid.default_estimate_heuristic = AStarGrid2D.HEURISTIC_OCTILE + grid.update() # Must precede solid flags; update clears them. + for prop in get_tree().get_nodes_in_group("office_props"): + for collider in prop.get_children(): + if not collider is CollisionShape3D or not collider.shape is BoxShape3D: + continue + var half: Vector3 = collider.shape.size * 0.5 + var minimum := Vector2(INF, INF) + var maximum := Vector2(-INF, -INF) + for x in [-half.x, half.x]: + for z in [-half.z, half.z]: + var p: Vector3 = collider.global_transform * Vector3(x, 0, z) + minimum = minimum.min(Vector2(p.x, p.z)) + maximum = maximum.max(Vector2(p.x, p.z)) + obstacles.append(Rect2(minimum, maximum - minimum).grow(CLEARANCE)) + for x in range(-17, 18): + for y in range(-17, 18): + var id := Vector2i(x, y) + var p := Vector2(id) * CELL + for rect in obstacles: + if rect.has_point(p): + grid.set_point_solid(id) + break + +func _free_id(point: Vector3) -> Vector2i: + var id := Vector2i(roundi(point.x / CELL), roundi(point.z / CELL)) + id.x = clampi(id.x, -17, 17) + id.y = clampi(id.y, -17, 17) + if not grid.is_point_solid(id): + return id + var best := id + var distance := INF + for x in range(-17, 18): + for y in range(-17, 18): + var candidate := Vector2i(x, y) + if not grid.is_point_solid(candidate): + var d := Vector2(candidate).distance_squared_to(Vector2(point.x, point.z) / CELL) + if d < distance: + best = candidate + distance = d + return best + +func route(from: Vector3, to: Vector3) -> PackedVector3Array: + var result := PackedVector3Array() + for p in grid.get_point_path(_free_id(from), _free_id(to)): + result.append(Vector3(p.x, 0, p.y)) + return result + +func direct_clear(from: Vector3, to: Vector3) -> bool: + var a := Vector2(from.x, from.z) + var b := Vector2(to.x, to.z) + var count := maxi(1, ceili(a.distance_to(b) / 0.15)) + for i in range(count + 1): + var p := a.lerp(b, float(i) / count) + if absf(p.x) > 8.6 or absf(p.y) > 8.6: + return false + for obstacle in obstacles: + if obstacle.has_point(p): + return false + return true diff --git a/scripts/room_navigation.gd.uid b/scripts/room_navigation.gd.uid new file mode 100644 index 0000000..50aee2f --- /dev/null +++ b/scripts/room_navigation.gd.uid @@ -0,0 +1 @@ +uid://dkb8hopidwlrs diff --git a/scripts/zombie.gd b/scripts/zombie.gd index d20b14f..869709f 100644 --- a/scripts/zombie.gd +++ b/scripts/zombie.gd @@ -1,6 +1,12 @@ extends CharacterBody3D signal killed(headshot: bool) var player: CharacterBody3D +var navigation: Node +var _path := PackedVector3Array() +var _path_index := 0 +var _repath := 0.0 +var _direct := false +var _nav_started := false var hp := 100.0 var speed := 2.0 var cooldown := 0.0 @@ -30,6 +36,7 @@ func _ready() -> void: for area in model.find_children("*", "Area3D", true, false): area.collision_layer = 0 speed = randf_range(1.6, 2.8) + _repath = randf_range(0, 0.5) anim.play("walk") Audio.play(self, "zombie/zo_alert%d0.wav" % randi_range(1, 3), true, -15) @@ -46,8 +53,10 @@ func _physics_process(dt: float) -> void: var distance := dir.length() if distance > 0.01: rotation.y = atan2(-dir.x, -dir.z) - var walking := distance > 1.7 - var motion := dir.normalized() * speed if walking else Vector3.ZERO + var walking := distance > 1.7 or not _attack_clear() + var motion := _pursuit_direction(dt) * speed if walking else Vector3.ZERO + if motion.length_squared() > 0.01: + rotation.y = atan2(-motion.x, -motion.z) velocity.x = motion.x velocity.z = motion.z velocity.y = -1.0 if is_on_floor() else velocity.y - 20.0 * dt @@ -60,8 +69,41 @@ func _physics_process(dt: float) -> void: Audio.play(self, "zombie/zo_attack%d.wav" % randi_range(1, 2), true, -15) get_tree().create_timer(0.5).timeout.connect(_attack) +func _pursuit_direction(dt: float) -> Vector3: + var target := player.global_position + _repath -= dt + if navigation != null and (_repath <= 0 or not _nav_started): + _nav_started = true + _repath = 0.55 + float(get_instance_id() % 7) * 0.02 + _direct = navigation.direct_clear(global_position, target) + if not _direct: + _path = navigation.route(global_position, target) + _path_index = 0 + if _path.size() > 1 and navigation.direct_clear(global_position, _path[1]): + _path_index = 1 + if navigation == null or _direct: + var dir := target - global_position + dir.y = 0 + return dir.normalized() + while _path_index < _path.size(): + var dir := _path[_path_index] - global_position + dir.y = 0 + if dir.length() > 0.22: + return dir.normalized() + _path_index += 1 + return Vector3.ZERO + +func _attack_clear() -> bool: + # Check below desk-top height: a chest-high ray can pass over a desk + # even though the bodies cannot approach each other through it. + var start := global_position + Vector3.UP * 0.6 + var end := player.global_position + Vector3.UP * 0.6 + var query := PhysicsRayQueryParameters3D.create(start, end, 1) + query.exclude = [get_rid(), player.get_rid()] + return get_world_3d().direct_space_state.intersect_ray(query).is_empty() + func _attack() -> void: - if not dead and is_instance_valid(player) and player.hp > 0 and global_position.distance_to(player.global_position) < 2.3: + if not dead and is_instance_valid(player) and player.hp > 0 and global_position.distance_to(player.global_position) < 2.3 and _attack_clear(): player.take_damage(12) Audio.play(self, "zombie/claw_strike%d.wav" % randi_range(1, 3), true, -12) diff --git a/tools/test_gameplay_improvements.gd b/tools/test_gameplay_improvements.gd new file mode 100644 index 0000000..2f2fade --- /dev/null +++ b/tools/test_gameplay_improvements.gd @@ -0,0 +1,117 @@ +extends SceneTree +const Zombie = preload("res://scripts/zombie.gd") +var world: Node3D +var player: Node3D + +func _initialize() -> void: + call_deferred("run_test") + +func _freeze_others(actor: Node = null) -> void: + for zombie in get_nodes_in_group("live_zombies"): + if zombie != actor: + zombie.set_physics_process(false) + +func run_test() -> void: + world = load("res://scenes/main.tscn").instantiate() + root.add_child(world) + player = world.player + player.set_physics_process(false) + player.touch_mode = true + _freeze_others() + player.hp = 20 + player.ammo = 3 + world.hud.update_status(player, 4, 1200) + assert(world.hud.health_value.text == "20") + assert(world.hud.ammo_value.text == "03 / 30") + assert(world.hud.health_value.get_theme_color("font_color") == world.hud.DANGER) + assert(not world.hud.help.visible) + print("PASS: HUD numeri, vita bassa, caricatore e modalita' touch") + player.hp = 100 + player.ammo = 1 + player.shoot() + assert(player.ammo == 0 and player.reloading) + var release_count := 0 + for speaker in player.get_children(): + if speaker is AudioStreamPlayer and speaker.stream.resource_path.ends_with("cliprelease1.wav"): + release_count += 1 + player.start_reload() # must not add another reload + var repeated_count := 0 + for speaker in player.get_children(): + if speaker is AudioStreamPlayer and speaker.stream.resource_path.ends_with("cliprelease1.wav"): + repeated_count += 1 + assert(repeated_count == release_count) + await create_timer(1.7).timeout + assert(player.ammo == 30 and not player.reloading) + print("PASS: ultimo colpo avvia auto-reload e completa a 30") + # Shared grid route: actual physics movement, not just returned waypoints. + Engine.time_scale = 4 + var cases := [ + [Vector3(-5, 0.1, -6.5), Vector3(-5, 0.1, 0)], + [Vector3(5, 0.1, 1.5), Vector3(5, 0.1, 7.5)], + [Vector3(-6, 0.1, 4), Vector3(-2, 0.1, 4)], + [Vector3(6.8, 0.1, -6), Vector3(6.8, 0.1, -2)], + ] + player.hp = 10000 + for positions in cases: + player.position = positions[1] + var actor := CharacterBody3D.new() + actor.set_script(Zombie) + actor.player = player + actor.navigation = world.navigation + actor.position = positions[0] + world.add_child(actor) + actor.entering = false + actor.speed = 3 + var reached := false + for i in 420: + _freeze_others(actor) + await physics_frame + var delta: Vector3 = actor.position - player.position + delta.y = 0 + if delta.length() < 1.8: + reached = true + break + assert(reached, "Zombie bloccato: %s -> %s, finale %s" % [positions[0], positions[1], actor.position]) + actor.queue_free() + await process_frame + print("PASS: inseguimento fisico attorno a entrambe le scrivanie, sedie e bidone") + Engine.time_scale = 1 + # A melee ray across the left desk must not hit through it. + player.position = Vector3(-5, 0.1, -3.2) + var actor := CharacterBody3D.new() + actor.set_script(Zombie) + actor.player = player + actor.position = Vector3(-5, 0.1, -4.8) + world.add_child(actor) + actor.entering = false + actor.set_physics_process(false) + await physics_frame + await physics_frame + assert(not actor._attack_clear()) + var hp_before: float = player.hp + actor._attack() + assert(player.hp == hp_before) + print("PASS: nessun attacco attraverso la scrivania") + _freeze_others() + player.hp = 100 + player.ammo = 1 + player.shoot() + assert(player.reloading) + player.take_damage(1000) + var flatlines := 0 + for speaker in player.get_children(): + if speaker is AudioStreamPlayer and speaker.stream.resource_path.ends_with("flatline.wav"): + flatlines += 1 + assert(flatlines == 1) + player.take_damage(10) + flatlines = 0 + for speaker in player.get_children(): + if speaker is AudioStreamPlayer and speaker.stream.resource_path.ends_with("flatline.wav"): + flatlines += 1 + assert(flatlines == 1) + await create_timer(1.8).timeout + assert(player.hp == 0 and player.ammo == 0 and not player.reloading) + print("PASS: flatline una volta e auto-reload annullato dalla morte") + world.queue_free() + await process_frame + quit() diff --git a/tools/test_gameplay_improvements.gd.uid b/tools/test_gameplay_improvements.gd.uid new file mode 100644 index 0000000..34a2832 --- /dev/null +++ b/tools/test_gameplay_improvements.gd.uid @@ -0,0 +1 @@ +uid://dttbumb5iq0ti