diff --git a/docs/WINDOWS-SPAWN.md b/docs/WINDOWS-SPAWN.md new file mode 100644 index 0000000..32bef27 --- /dev/null +++ b/docs/WINDOWS-SPAWN.md @@ -0,0 +1,93 @@ +# Eight windows and random entry + +## Layout + +`scripts/room.gd` now builds **eight windows, two per wall** (four walls), spaced +symmetrically about each wall centre. `WIN_EDGE_GAP = 2.4 m` is the wall left between a +window edge and the room corner, which for an 18 m wall with 4 m windows gives: openings +centred at ±4.6 m, 2.4 m of wall at each corner, and a **5.2 m pillar** between the two +openings. The number of windows follows from the constants: + +```gdscript +const WINDOWS_PER_WALL := 2 +const WIN_EDGE_GAP := 2.4 +static func window_offsets(length: float) -> Array[float] +``` + +`window_offsets()` works for any count: centres are spread evenly between +±(length/2 − WIN_W/2 − WIN_EDGE_GAP), so raising `WINDOWS_PER_WALL` widens the row +instead of pushing openings out of the wall, and a single window stays centred. + +The wall builder was generalised from "one opening in the middle" to any number of +openings: solid pieces along the wall between openings and at both ends, plus a sill and +a header per opening. The room-space UVs (see `OFFICE-SURFACES.md`) are unchanged: a +segment is mapped from its own world position, so the extra openings do not restart the +pattern. With two openings per wall there are 7 segments per wall, 29 surfaces in total +(counted from the constants by `test_office_uv.gd`). + +## Spawn data + +Every window publishes one record: + +```gdscript +windows.append({"center": center, "normal": wall.normal, "axis": wall.axis, + "slots": slots, "glass": glass}) +``` + +- `center`: a point on the room-side wall face at y = 0; +- `normal`: pointing into the room; +- `slots`: `SPAWN_SLOTS = 4` points spread across the opening, `SPAWN_OUTSIDE = 1.2 m` + outside the wall; +- `glass`: the pane of that specific window. + +The old `window_spawn_points` and `window_normals` arrays are still filled and are now +actually consumed — they were computed and ignored before, which was one of the known +anomalies of this prototype. + +## Entry logic + +`main.gd` no longer uses four hardcoded cardinal centres: + +- `_pick_window()` draws a random window and **never repeats the previous one**, so + successive zombies come from different openings instead of always the same wall; +- a random slot inside that window is picked, so even the same window is entered from a + different spot; +- the entry tween is unchanged (1.2 s rising to sill height, then 0.8 s into the room); +- only **that** window's glass is shattered (looked up by identity) instead of scanning + every pane and guessing by distance. + +## Notes and limits + +- With eight openings, three of the window-to-centre diagonals are crossed by furniture + (the rotated chair at (6.8, 0, −4) and the two desks). This is not a defect: the A* + grid routes zombies around it, and `test_office_atmosphere.gd` now asserts what + matters — every window is a real opening **and** every window is connected to the room + centre through the navigation grid. It prints a note for the obstructed diagonals. +- Zombies entering from eight directions spend visibly less time in a queue at one wall, + which changes the pacing a little; wave sizes are untouched (`4 + 2n`). + +## Verification + +```sh +"$G" --headless --path . --script res://tools/test_spawn_windows.gd +"$G" --display-driver x11 --rendering-method gl_compatibility --path . --script res://tools/shot_windows.gd +``` + +`test_spawn_windows.gd` checks: eight windows, two per wall, symmetric, with at least 1 m +of wall at the corners and the expected pillar; 32 spawn slots, all outside the room, +aligned with their opening and at the right distance; 400 random draws covering all eight +windows with no immediate repetition; a ray proving each opening is a real hole in the +wall while the pillar between the openings is solid; six real spawns, all starting +outside and ending inside with the correct pane shattered (counted by identity, so two +zombies using the same window break it once). + +Two traps met while writing it, both worth remembering: + +1. the **first wave starts automatically in `_ready`**, so a test that counts panes must + wait for that wave and its tweens, otherwise the counts are polluted; +2. physics ray queries need `await physics_frame` after building the room, otherwise + `direct_space_state` is still empty and the assertions pass or fail for the wrong + reason. + +Images `/tmp/zombie-windows-{nord,est,sud,ovest,angolo,ingresso}.png` show the two +openings per wall, the pillar between them, and a group of zombies that came in. diff --git a/scripts/main.gd b/scripts/main.gd index e9b9da0..eabe29b 100644 --- a/scripts/main.gd +++ b/scripts/main.gd @@ -20,6 +20,7 @@ var wave_pending := false var smoke := false var awaiting_web_start := false var start_label: Label +var last_window := -1 func _ready() -> void: smoke = "--smoke" in OS.get_cmdline_user_args() @@ -169,19 +170,40 @@ func _next_wave() -> void: for i in zombies_for_wave(wave): if player.hp <= 0: break - _spawn(i % 4) + _spawn() await get_tree().create_timer(0.8).timeout spawning = false _check_wave() -func _spawn(index: int) -> void: - var centers := [Vector3(0, 0, -9), Vector3(0, 0, 9), Vector3(-9, 0, 0), Vector3(9, 0, 0)] - var inward: Vector3 = -centers[index].normalized() +## Sceglie una finestra a caso fra quelle disponibili, evitando di ripetere +## l'ultima usata: gli zombie entrano da aperture diverse invece che sempre dalle +## stesse quattro posizioni fisse. +func _pick_window() -> int: + var count: int = room.windows.size() + if count == 0: + return -1 + if count == 1: + return 0 + var index: int = randi() % count + if index == last_window: + index = (last_window + 1 + randi() % (count - 1)) % count + return index + +func _spawn() -> void: + var chosen := _pick_window() + if chosen < 0: + return + last_window = chosen + var window: Dictionary = room.windows[chosen] + var slots: Array = window.slots + var start: Vector3 = slots[randi() % slots.size()] # slot casuale nella finestra + var face: Vector3 = window.center + var inward: Vector3 = window.normal var zombie := CharacterBody3D.new() zombie.set_script(ZombieScript) zombie.player = player zombie.navigation = navigation - zombie.position = centers[index] - inward * 1.2 + zombie.position = start add_child(zombie) alive += 1 zombie.killed.connect(func(headshot: bool): @@ -190,12 +212,12 @@ func _spawn(index: int) -> void: _check_wave()) var tween := zombie.create_tween() zombie.entry_tween = tween - tween.tween_property(zombie, "position", centers[index] + Vector3(0, 1.55, 0), 1.2) + tween.tween_property(zombie, "position", face + Vector3(0, 1.55, 0), 1.2) tween.tween_callback(func(): - for glass in room.glass_panes.duplicate(): - if is_instance_valid(glass) and Vector2(glass.position.x - centers[index].x, glass.position.z - centers[index].z).length() < 2: - room.shatter_glass(glass)) - tween.tween_property(zombie, "position", centers[index] + inward * 1.4 + Vector3(0, 0.1, 0), 0.8) + var glass = window.glass + if is_instance_valid(glass): + room.shatter_glass(glass)) # solo la finestra usata + tween.tween_property(zombie, "position", face + inward * 1.4 + Vector3(0, 0.1, 0), 0.8) tween.tween_callback(func(): zombie.entering = false) func _check_wave() -> void: diff --git a/scripts/room.gd b/scripts/room.gd index 18a4372..5f24fbd 100644 --- a/scripts/room.gd +++ b/scripts/room.gd @@ -1,5 +1,7 @@ -## Stanza 18x18x5 con 4 finestre (una per muro) e nessuna porta. +## Stanza 18x18x5 con 8 finestre (due per muro) e nessuna porta. ## Geometria costruita in codice per determinismo; vetro = Area3D rompibile. +## Le finestre sono DATI (windows): la logica di spawn degli zombie le legge da qui +## invece di usare centri scritti a mano. class_name Room extends Node3D @@ -14,9 +16,20 @@ const CEILING_REPEAT_M := 1.2 # CEIL5 contains 2x2 panels: each panel = 0.6m const WIN_W: float = 4.0 const WIN_H: float = 2.0 const WIN_Y0: float = 1.5 +# Due finestre per muro, distanziate simmetricamente. WIN_EDGE_GAP e' il muro che +# resta fra il bordo di una finestra e l'angolo della stanza: con W=18 e WIN_W=4 +# restano 2.4 m agli angoli e 5.2 m di pilastro fra le due aperture. +const WINDOWS_PER_WALL := 2 +const WIN_EDGE_GAP := 2.4 +# Slot di spawn fuori da ogni finestra, e quanto fuori dal muro. +const SPAWN_SLOTS := 4 +const SPAWN_OUTSIDE := 1.2 var glass_panes: Array[Area3D] = [] -## Punti di spawn esterni, uno per finestra (4 finestre x 4 slot) +## Una voce per finestra: {center (sul piano interno del muro, y=0), normal (verso +## l'interno), axis, slots: Array[Vector3], glass: Area3D}. +var windows: Array[Dictionary] = [] +## Punti di spawn esterni, appiattiti (finestre x slot) - usati dai test. var window_spawn_points: Array[Vector3] = [] var window_normals: Array[Vector3] = [] # direzione "dentro" la stanza per ogni finestra @@ -30,69 +43,82 @@ func _ready() -> void: # Soffitto _box(Vector3(W, 0.2, D), Vector3(0, H + 0.1, 0), ceil_mat, false) - # 4 muri con apertura finestra al centro + # 4 muri, ognuno con WINDOWS_PER_WALL aperture (due per muro = otto totali) # nord (z = -D/2), sud (z = +D/2), ovest (x = -W/2), est (x = +W/2) + var offsets := window_offsets(W) var walls := [ - {"center": Vector3(0, H / 2, -D / 2 - WALL_T / 2), "size": Vector3(W, H, WALL_T), "axis": "x"}, - {"center": Vector3(0, H / 2, D / 2 + WALL_T / 2), "size": Vector3(W, H, WALL_T), "axis": "x"}, - {"center": Vector3(-W / 2 - WALL_T / 2, H / 2, 0), "size": Vector3(WALL_T, H, D), "axis": "z"}, - {"center": Vector3(W / 2 + WALL_T / 2, H / 2, 0), "size": Vector3(WALL_T, H, D), "axis": "z"}, + {"axis": "x", "center": Vector3(0, H / 2, -D / 2 - WALL_T / 2), "size": Vector3(W, H, WALL_T), "normal": Vector3(0, 0, 1), "face": Vector3(0, 0, -D / 2)}, + {"axis": "x", "center": Vector3(0, H / 2, D / 2 + WALL_T / 2), "size": Vector3(W, H, WALL_T), "normal": Vector3(0, 0, -1), "face": Vector3(0, 0, D / 2)}, + {"axis": "z", "center": Vector3(-W / 2 - WALL_T / 2, H / 2, 0), "size": Vector3(WALL_T, H, D), "normal": Vector3(1, 0, 0), "face": Vector3(-W / 2, 0, 0)}, + {"axis": "z", "center": Vector3(W / 2 + WALL_T / 2, H / 2, 0), "size": Vector3(WALL_T, H, D), "normal": Vector3(-1, 0, 0), "face": Vector3(W / 2, 0, 0)}, ] - for w in walls: - _build_wall_with_window(w.center, w.size, w.axis, wall_mat) + for wall in walls: + _build_wall(wall.center, wall.size, wall.axis, wall_mat, offsets) + for offset in offsets: + var lateral := Vector3.RIGHT if wall.axis == "x" else Vector3.BACK + var center: Vector3 = wall.face + lateral * offset + var glass := _add_glass(center + Vector3(0, WIN_Y0 + WIN_H / 2, 0), _window_size(wall.axis, WIN_W, WIN_H)) + var slots: Array[Vector3] = [] + for i in SPAWN_SLOTS: + var spread := (i - (SPAWN_SLOTS - 1) * 0.5) * (WIN_W / SPAWN_SLOTS) + var point: Vector3 = center + lateral * spread + Vector3(wall.normal) * (-SPAWN_OUTSIDE) + slots.append(point) + window_spawn_points.append(point) + window_normals.append(wall.normal) + windows.append({"center": center, "normal": wall.normal, "axis": wall.axis, + "slots": slots, "glass": glass}) + assert(windows.size() == WINDOWS_PER_WALL * 4) - # Vetrata per ogni finestra - _add_glass(Vector3(0, WIN_Y0 + WIN_H / 2, -D / 2), Vector3(WIN_W, WIN_H, 0.05), Vector3(1, 0, 0)) - _add_glass(Vector3(0, WIN_Y0 + WIN_H / 2, D / 2), Vector3(WIN_W, WIN_H, 0.05), Vector3(1, 0, 0)) - _add_glass(Vector3(-W / 2, WIN_Y0 + WIN_H / 2, 0), Vector3(0.05, WIN_H, WIN_W), Vector3(0, 0, 1)) - _add_glass(Vector3(W / 2, WIN_Y0 + WIN_H / 2, 0), Vector3(0.05, WIN_H, WIN_W), Vector3(0, 0, 1)) +## Posizioni laterali delle aperture su un muro lungo `length`, simmetriche rispetto +## al centro e distribuite uniformemente, lasciando WIN_EDGE_GAP a ogni angolo. +static func window_offsets(length: float) -> Array[float]: + var offsets: Array[float] = [] + if WINDOWS_PER_WALL <= 0: + return offsets + if WINDOWS_PER_WALL == 1: + offsets.append(0.0) + return offsets + var limit := length * 0.5 - WIN_W * 0.5 - WIN_EDGE_GAP + var step := 2.0 * limit / float(WINDOWS_PER_WALL - 1) + for i in WINDOWS_PER_WALL: + offsets.append((i - (WINDOWS_PER_WALL - 1) * 0.5) * step) + return offsets - # Punti di spawn: 4 slot per finestra, fuori dal muro - var win_centers := [ - {"c": Vector3(0, 0, -D / 2), "n": Vector3(0, 0, 1)}, - {"c": Vector3(0, 0, D / 2), "n": Vector3(0, 0, -1)}, - {"c": Vector3(-W / 2, 0, 0), "n": Vector3(1, 0, 0)}, - {"c": Vector3(W / 2, 0, 0), "n": Vector3(-1, 0, 0)}, - ] - for wc in win_centers: - var side: Vector3 = wc.n.cross(Vector3(0, 1, 0)) # asse laterale della finestra - for i in 4: - var off := (i - 1.5) * (WIN_W / 3.0) - var p: Vector3 = wc.c + side * off + wc.n * (-1.2) # 1.2 m fuori dalla finestra - window_spawn_points.append(p) - window_normals.append(wc.n) +func _window_size(axis: String, width: float, height: float) -> Vector3: + return Vector3(width, height, 0.05) if axis == "x" else Vector3(0.05, height, width) -func _build_wall_with_window(center: Vector3, size: Vector3, axis: String, mat: StandardMaterial3D) -> void: - # Segments around the window opening (window is centered on the wall face) +## Muro con un numero qualsiasi di aperture: pezzi pieni fra le aperture e agli +## angoli, piu' davanzale e architrave per ciascuna apertura. +func _build_wall(center: Vector3, size: Vector3, axis: String, mat: StandardMaterial3D, offsets: Array[float]) -> void: var wall := StaticBody3D.new() wall.name = "Wall" add_child(wall) - var half_win_x := WIN_W / 2.0 - var y0 := WIN_Y0 - var y1 := WIN_Y0 + WIN_H + var length := size.x if axis == "x" else size.z + var sorted_offsets := offsets.duplicate() + sorted_offsets.sort() + var edges: Array[float] = [-length * 0.5] + for offset in sorted_offsets: + edges.append(offset - WIN_W * 0.5) + edges.append(offset + WIN_W * 0.5) + edges.append(length * 0.5) + for i in range(0, edges.size(), 2): + var span := edges[i + 1] - edges[i] + if span > 0.001: + _wall_segment(wall, center, size, axis, (edges[i] + edges[i + 1]) * 0.5, 0.0, span, size.y, mat) + var header := size.y - (WIN_Y0 + WIN_H) + for offset in sorted_offsets: + _wall_segment(wall, center, size, axis, offset, -size.y * 0.5 + WIN_Y0 * 0.5, + WIN_W, WIN_Y0, mat) # davanzale + _wall_segment(wall, center, size, axis, offset, size.y * 0.5 - header * 0.5, + WIN_W, header, mat) # architrave - if axis == "x": # muro lungo x - var L := size.x - var segs := [ - {"s": Vector3(L / 2 - half_win_x, size.y, size.z), "c": Vector3(-half_win_x - (L / 2 - half_win_x) / 2.0, 0, 0)}, - {"s": Vector3(L / 2 - half_win_x, size.y, size.z), "c": Vector3(half_win_x + (L / 2 - half_win_x) / 2.0, 0, 0)}, - {"s": Vector3(WIN_W, y0, size.z), "c": Vector3(0, -size.y / 2 + y0 / 2.0, 0)}, - {"s": Vector3(WIN_W, size.y - y1, size.z), "c": Vector3(0, size.y / 2 - (size.y - y1) / 2.0, 0)}, - ] - for seg in segs: - _add_box_to(wall, seg.s, center + seg.c, mat) - else: # muro lungo z - var L := size.z - var segs := [ - {"s": Vector3(size.x, size.y, L / 2 - half_win_x), "c": Vector3(0, 0, -half_win_x - (L / 2 - half_win_x) / 2.0)}, - {"s": Vector3(size.x, size.y, L / 2 - half_win_x), "c": Vector3(0, 0, half_win_x + (L / 2 - half_win_x) / 2.0)}, - {"s": Vector3(size.x, y0, WIN_W), "c": Vector3(0, -size.y / 2 + y0 / 2.0, 0)}, - {"s": Vector3(size.x, size.y - y1, WIN_W), "c": Vector3(0, size.y / 2 - (size.y - y1) / 2.0, 0)}, - ] - for seg in segs: - _add_box_to(wall, seg.s, center + seg.c, mat) +func _wall_segment(wall: StaticBody3D, center: Vector3, size: Vector3, axis: String, + lateral: float, y_offset: float, span: float, height: float, mat: StandardMaterial3D) -> void: + var box := Vector3(span, height, size.z) if axis == "x" else Vector3(size.x, height, span) + var shift := Vector3(lateral, y_offset, 0) if axis == "x" else Vector3(0, y_offset, lateral) + _add_box_to(wall, box, center + shift, mat) -func _add_glass(center: Vector3, size: Vector3, rot_axis: Vector3) -> void: +func _add_glass(center: Vector3, size: Vector3) -> Area3D: var glass := Area3D.new() glass.name = "Glass" glass.collision_layer = 8 @@ -118,6 +144,7 @@ func _add_glass(center: Vector3, size: Vector3, rot_axis: Vector3) -> void: mesh.mesh = bm glass.add_child(mesh) glass_panes.append(glass) + return glass func shatter_glass(glass: Area3D) -> void: var sound_origin := Node3D.new() diff --git a/tools/shot_windows.gd b/tools/shot_windows.gd new file mode 100644 index 0000000..a7b93b7 --- /dev/null +++ b/tools/shot_windows.gd @@ -0,0 +1,44 @@ +extends SceneTree +## Cattura la stanza con le otto finestre: le quattro pareti dal centro, una vista +## d'angolo e un ingresso in corso dalla finestra effettivamente scelta dallo spawn. +func _initialize() -> void: + call_deferred("run_test") + +func run_test() -> void: + var world = load("res://scenes/main.tscn").instantiate() + root.add_child(world) + world.player.hp = 100000 + world.player.set_physics_process(false) + await create_timer(0.35).timeout # la prima ondata parte in _ready + var chosen: int = world.last_window + var window: Dictionary = world.room.windows[chosen] + # Ingresso in corso: il tween dura 1,2 s salendo verso il davanzale. + world.player.position = Vector3(window.center) + Vector3(window.normal) * -0.5 + Vector3(0, 0.1, 0) + world.player.look_at(Vector3(window.center) + Vector3(0, 1.8, 0)) + await create_timer(0.7).timeout + await RenderingServer.frame_post_draw + root.get_texture().get_image().save_png("/tmp/zombie-windows-ingresso.png") + print("SHOT ingresso: finestra %d da %s" % [chosen, window.center]) + # Le quattro pareti dal centro. + var views := [ + ["nord", Vector3(0, 2.2, -9)], + ["est", Vector3(9, 2.2, 0)], + ["sud", Vector3(0, 2.2, 9)], + ["ovest", Vector3(-9, 2.2, 0)], + ] + for view in views: + world.player.position = Vector3(0, 0.1, 0) + world.player.look_at(view[1]) + await create_timer(0.4).timeout + await RenderingServer.frame_post_draw + root.get_texture().get_image().save_png("/tmp/zombie-windows-%s.png" % view[0]) + # Vista d'angolo: due pareti insieme. + world.player.position = Vector3(-7, 0.1, -7) + world.player.look_at(Vector3(6, 1.5, 6)) + await create_timer(0.4).timeout + await RenderingServer.frame_post_draw + root.get_texture().get_image().save_png("/tmp/zombie-windows-angolo.png") + print("SHOT: immagini in /tmp/zombie-windows-*.png") + world.queue_free() + await process_frame + quit() diff --git a/tools/shot_windows.gd.uid b/tools/shot_windows.gd.uid new file mode 100644 index 0000000..17b1021 --- /dev/null +++ b/tools/shot_windows.gd.uid @@ -0,0 +1 @@ +uid://bptxvotrlu7n4 diff --git a/tools/test_office_atmosphere.gd b/tools/test_office_atmosphere.gd index caf40c8..23aa82f 100644 --- a/tools/test_office_atmosphere.gd +++ b/tools/test_office_atmosphere.gd @@ -41,14 +41,25 @@ func run_test() -> void: 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)]: + # Ogni finestra deve essere un'APERTURA e deve essere COLLEGATA al centro: gli zombie + # entrano e devono poter arrivare. Il raggio prova l'apertura; il percorso A* prova + # la raggiungibilita', anche quando un arredo cade su una diagonale. + var inside_points: Array[Vector3] = [] + for window in world.room.windows: + var edge: Vector3 = window.center + Vector3(window.normal) * 0.8 + Vector3(0, 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()) + inside_points.append(edge) + if not space.intersect_ray(q).is_empty(): + print(" (nota: un arredo ostruisce la diagonale da ", edge, " al centro)") + assert(inside_points.size() >= 8) + for point in inside_points: + var path: PackedVector3Array = world.navigation.route(point, Vector3(0, 0, 0)) + assert(path.size() >= 2) # raggiungibile dal centro + assert(path[path.size() - 1].distance_to(Vector3.ZERO) < 1.5) 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") + print("PASS: otto aperture libere e tutte collegate al centro dalla navigazione, scrivanie solide") if "--screenshot" in OS.get_cmdline_user_args(): world.player.set_physics_process(false) world.player.position = Vector3(0, 0.1, 0) diff --git a/tools/test_office_uv.gd b/tools/test_office_uv.gd index 52f3087..dd4db4b 100644 --- a/tools/test_office_uv.gd +++ b/tools/test_office_uv.gd @@ -8,7 +8,10 @@ func run_test() -> void: var world = load("res://scenes/main.tscn").instantiate() root.add_child(world) var surfaces = get_nodes_in_group("office_surfaces") - assert(surfaces.size() == 17) # sixteen wall segments + ceiling + # Muri: per ogni muro (aperture+1) pezzi pieni, piu' davanzale e architrave per + # ogni apertura; poi il soffitto. Il numero si ricava dalle costanti della stanza. + var per_wall: int = (RoomScript.WINDOWS_PER_WALL + 1) + 2 * RoomScript.WINDOWS_PER_WALL + assert(surfaces.size() == 4 * per_wall + 1) # quattro muri + soffitto for surface in surfaces: assert(surface.mesh is ArrayMesh) var mat = surface.mesh.surface_get_material(0) @@ -16,7 +19,7 @@ func run_test() -> void: assert(mat.albedo_texture.resource_path.contains("/office/")) var arrays = surface.mesh.surface_get_arrays(0) assert(arrays[Mesh.ARRAY_TEX_UV].size() == arrays[Mesh.ARRAY_VERTEX].size()) - print("PASS: 17 superfici con UV esplicite e texture office originali") + print("PASS: %d superfici con UV esplicite e texture office originali (%d per muro)" % [surfaces.size(), per_wall]) var north := Vector3(0, 2.5, -9.15) var low := Vector3(0, 0.75, -9.15) var high := Vector3(0, 4.25, -9.15) diff --git a/tools/test_spawn_windows.gd b/tools/test_spawn_windows.gd new file mode 100644 index 0000000..bb61eef --- /dev/null +++ b/tools/test_spawn_windows.gd @@ -0,0 +1,128 @@ +extends SceneTree +## Verifica finestre multiple e spawn casuale da finestre diverse. +## +## Ordine voluto: prima i controlli sui DATI della stanza (immediati e indipendenti +## dal gioco), poi la parte con gli spawn, che richiede di aspettare la prima ondata +## automatica e i suoi tween. +func _initialize() -> void: + call_deferred("run_test") + +func run_test() -> void: + var world = load("res://scenes/main.tscn").instantiate() + root.add_child(world) + var room = world.room + var walls := 4 + + # --- finestre: almeno otto, due per muro, distribuite in modo simmetrico --- + assert(room.WINDOWS_PER_WALL == 2) + var expected: int = room.WINDOWS_PER_WALL * walls + assert(room.windows.size() == expected and expected >= 8) + assert(room.window_spawn_points.size() == expected * room.SPAWN_SLOTS) + assert(room.window_normals.size() == room.window_spawn_points.size()) + assert(room.glass_panes.size() == expected) # nessun vetro rotto prima delle ondate + var sides := {} + for window in room.windows: + var center: Vector3 = window.center + var on_x := absf(center.z) > absf(center.x) # muro nord/sud + sides[("x" if on_x else "z")] = int(sides.get("x" if on_x else "z", 0)) + 1 + var slots: Array = window.slots + assert(slots.size() == room.SPAWN_SLOTS) + assert(is_instance_valid(window.glass)) + for slot in slots: + assert(not (absf(slot.x) <= 9.0 and absf(slot.z) <= 9.0)) # fuori dalla stanza + assert(is_equal_approx(window.normal.dot(slot - center), -room.SPAWN_OUTSIDE)) + assert(absf((slot - center).dot(Vector3.UP)) < 0.001) + assert(sides.size() == 2 and sides["x"] == walls and sides["z"] == walls) # 4 finestre per asse + var north_centers: Array[float] = [] + for window in room.windows: + if absf(window.center.z + 9.0) < 0.01: + north_centers.append(window.center.x) + north_centers.sort() + # Geometria, non formula: simmetria, margine agli angoli e pilastro fra le aperture. + var limit: float = room.W * 0.5 - room.WIN_W * 0.5 + for i in north_centers.size(): + assert(is_equal_approx(north_centers[i], -north_centers[north_centers.size() - 1 - i])) + assert(limit - absf(north_centers[i]) >= 1.0) # almeno 1 m di muro all'angolo + var pillar: float = room.W - 2.0 * room.WIN_EDGE_GAP - 2.0 * room.WIN_W + assert(pillar >= 1.0) + assert(is_equal_approx(north_centers[1] - north_centers[0] - room.WIN_W, pillar)) + print("PASS: %d finestre, due per muro a %.1f m dal centro, pilastro %.1f m fra le aperture" % [room.windows.size(), north_centers[1], pillar]) + + # --- le finestre sono aperture vere: si vede il cielo attraverso --- + await physics_frame # lo spazio fisico va aggiornato, altrimenti le query non vedono nulla + await physics_frame + var space = world.get_world_3d().direct_space_state + for window in room.windows: + var center: Vector3 = window.center + Vector3(0, room.WIN_Y0 + room.WIN_H * 0.5, 0) # centro apertura + var query := PhysicsRayQueryParameters3D.create(center, center - Vector3(window.normal) * 3.0, 1) + assert(space.intersect_ray(query).is_empty()) # nessun muro davanti all'apertura + var pillar_ray := PhysicsRayQueryParameters3D.create(Vector3(0, 2.5, -6.0), Vector3(0, 2.5, -9.6), 1) # dall'interno verso il muro + assert(not space.intersect_ray(pillar_ray).is_empty()) # il pilastro fra le due finestre c'e' + print("PASS: le otto aperture sono libere e i pilastri fra le finestre sono solidi") + + # --- selezione casuale, mai la stessa finestra due volte di fila --- + var hits := {} + var repeats := 0 + world.last_window = -1 + for i in 400: + var picked: int = world._pick_window() + assert(picked >= 0 and picked < room.windows.size()) + if picked == world.last_window: + repeats += 1 + hits[picked] = int(hits.get(picked, 0)) + 1 + world.last_window = picked + assert(repeats == 0) + assert(hits.size() == expected) # tutte le finestre vengono usate + var minimum := 400 + for count in hits.values(): + minimum = mini(minimum, count) + assert(minimum > 10) # non sempre le stesse due + print("PASS: 400 estrazioni su %d finestre, nessuna ripetizione immediata, minimo %d per finestra" % [hits.size(), minimum]) + + # --- da qui servono gli spawn: prima si aspetta la prima ondata automatica --- + world.player.hp = 100000 + world.spawning = true + world.wave_pending = true # nessuna nuova ondata + var guard := 0 + while world.spawning and guard < 200: + await create_timer(0.1).timeout + guard += 1 + await create_timer(2.4).timeout # fine dei tween d'ingresso dell'ondata + for zombie in get_nodes_in_group("live_zombies"): + zombie.set_physics_process(false) + + # --- spawn reale: ogni zombie esce e rientra dalla propria finestra --- + var before: int = room.glass_panes.size() + var intact := {} + for i in room.windows.size(): + intact[i] = is_instance_valid(room.windows[i].glass) + var spawned: Array = [] + var windows_used := {} + for i in 6: + world._spawn() + spawned.append(world.get_child(world.get_child_count() - 1)) + windows_used[world.last_window] = true # _pick_window registra la scelta + assert(windows_used.size() >= 3) # non tutti dalla stessa apertura + var used := {} + for zombie in spawned: + assert(not (absf(zombie.position.x) <= 9.0 and absf(zombie.position.z) <= 9.0)) + used[Vector2i(roundi(zombie.position.x * 0.5), roundi(zombie.position.z * 0.5))] = true + assert(used.size() >= 3) + await create_timer(2.6).timeout # tween d'ingresso: 1,2 + 0,8 s + for zombie in spawned: + if is_instance_valid(zombie): + assert(absf(zombie.position.x) <= 9.0 and absf(zombie.position.z) <= 9.0) # dentro + assert(not zombie.entering) + # Identita', non solo conteggio: si rompe il vetro della finestra usata, e una volta + # sola anche se due zombie entrano dalla stessa apertura. + var broken_by_me := 0 + for index in windows_used.keys(): + assert(intact.has(index)) + if intact[index]: + broken_by_me += 1 + assert(not is_instance_valid(room.windows[index].glass)) + assert(room.glass_panes.size() == before - broken_by_me) + print("PASS: 6 zombie entrati dall'esterno usando %d finestre diverse, %d vetri rotti, tutti dentro" % [windows_used.size(), broken_by_me]) + world.queue_free() + await process_frame + quit() diff --git a/tools/test_spawn_windows.gd.uid b/tools/test_spawn_windows.gd.uid new file mode 100644 index 0000000..2c2d308 --- /dev/null +++ b/tools/test_spawn_windows.gd.uid @@ -0,0 +1 @@ +uid://bre5y2exwoc1l