Eight windows per room and random zombie entry from different windows

This commit is contained in:
enne2
2026-10-13 21:21:17 +02:00
parent 70c82df450
commit fcf298c692
9 changed files with 400 additions and 70 deletions
+93
View File
@@ -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.
+32 -10
View File
@@ -20,6 +20,7 @@ var wave_pending := false
var smoke := false var smoke := false
var awaiting_web_start := false var awaiting_web_start := false
var start_label: Label var start_label: Label
var last_window := -1
func _ready() -> void: func _ready() -> void:
smoke = "--smoke" in OS.get_cmdline_user_args() smoke = "--smoke" in OS.get_cmdline_user_args()
@@ -169,19 +170,40 @@ func _next_wave() -> void:
for i in zombies_for_wave(wave): for i in zombies_for_wave(wave):
if player.hp <= 0: if player.hp <= 0:
break break
_spawn(i % 4) _spawn()
await get_tree().create_timer(0.8).timeout await get_tree().create_timer(0.8).timeout
spawning = false spawning = false
_check_wave() _check_wave()
func _spawn(index: int) -> void: ## Sceglie una finestra a caso fra quelle disponibili, evitando di ripetere
var centers := [Vector3(0, 0, -9), Vector3(0, 0, 9), Vector3(-9, 0, 0), Vector3(9, 0, 0)] ## l'ultima usata: gli zombie entrano da aperture diverse invece che sempre dalle
var inward: Vector3 = -centers[index].normalized() ## 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() var zombie := CharacterBody3D.new()
zombie.set_script(ZombieScript) zombie.set_script(ZombieScript)
zombie.player = player zombie.player = player
zombie.navigation = navigation zombie.navigation = navigation
zombie.position = centers[index] - inward * 1.2 zombie.position = start
add_child(zombie) add_child(zombie)
alive += 1 alive += 1
zombie.killed.connect(func(headshot: bool): zombie.killed.connect(func(headshot: bool):
@@ -190,12 +212,12 @@ func _spawn(index: int) -> void:
_check_wave()) _check_wave())
var tween := zombie.create_tween() var tween := zombie.create_tween()
zombie.entry_tween = 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(): tween.tween_callback(func():
for glass in room.glass_panes.duplicate(): var glass = window.glass
if is_instance_valid(glass) and Vector2(glass.position.x - centers[index].x, glass.position.z - centers[index].z).length() < 2: if is_instance_valid(glass):
room.shatter_glass(glass)) room.shatter_glass(glass)) # solo la finestra usata
tween.tween_property(zombie, "position", centers[index] + inward * 1.4 + Vector3(0, 0.1, 0), 0.8) tween.tween_property(zombie, "position", face + inward * 1.4 + Vector3(0, 0.1, 0), 0.8)
tween.tween_callback(func(): zombie.entering = false) tween.tween_callback(func(): zombie.entering = false)
func _check_wave() -> void: func _check_wave() -> void:
+81 -54
View File
@@ -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. ## 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 class_name Room
extends Node3D 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_W: float = 4.0
const WIN_H: float = 2.0 const WIN_H: float = 2.0
const WIN_Y0: float = 1.5 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] = [] 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_spawn_points: Array[Vector3] = []
var window_normals: Array[Vector3] = [] # direzione "dentro" la stanza per ogni finestra var window_normals: Array[Vector3] = [] # direzione "dentro" la stanza per ogni finestra
@@ -30,69 +43,82 @@ func _ready() -> void:
# Soffitto # Soffitto
_box(Vector3(W, 0.2, D), Vector3(0, H + 0.1, 0), ceil_mat, false) _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) # nord (z = -D/2), sud (z = +D/2), ovest (x = -W/2), est (x = +W/2)
var offsets := window_offsets(W)
var walls := [ var walls := [
{"center": Vector3(0, H / 2, -D / 2 - WALL_T / 2), "size": Vector3(W, H, WALL_T), "axis": "x"}, {"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)},
{"center": Vector3(0, H / 2, D / 2 + WALL_T / 2), "size": Vector3(W, H, WALL_T), "axis": "x"}, {"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)},
{"center": Vector3(-W / 2 - WALL_T / 2, H / 2, 0), "size": Vector3(WALL_T, H, D), "axis": "z"}, {"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)},
{"center": Vector3(W / 2 + WALL_T / 2, H / 2, 0), "size": Vector3(WALL_T, H, D), "axis": "z"}, {"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: for wall in walls:
_build_wall_with_window(w.center, w.size, w.axis, wall_mat) _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 ## Posizioni laterali delle aperture su un muro lungo `length`, simmetriche rispetto
_add_glass(Vector3(0, WIN_Y0 + WIN_H / 2, -D / 2), Vector3(WIN_W, WIN_H, 0.05), Vector3(1, 0, 0)) ## al centro e distribuite uniformemente, lasciando WIN_EDGE_GAP a ogni angolo.
_add_glass(Vector3(0, WIN_Y0 + WIN_H / 2, D / 2), Vector3(WIN_W, WIN_H, 0.05), Vector3(1, 0, 0)) static func window_offsets(length: float) -> Array[float]:
_add_glass(Vector3(-W / 2, WIN_Y0 + WIN_H / 2, 0), Vector3(0.05, WIN_H, WIN_W), Vector3(0, 0, 1)) var offsets: Array[float] = []
_add_glass(Vector3(W / 2, WIN_Y0 + WIN_H / 2, 0), Vector3(0.05, WIN_H, WIN_W), Vector3(0, 0, 1)) 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 func _window_size(axis: String, width: float, height: float) -> Vector3:
var win_centers := [ return Vector3(width, height, 0.05) if axis == "x" else Vector3(0.05, height, width)
{"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 _build_wall_with_window(center: Vector3, size: Vector3, axis: String, mat: StandardMaterial3D) -> void: ## Muro con un numero qualsiasi di aperture: pezzi pieni fra le aperture e agli
# Segments around the window opening (window is centered on the wall face) ## 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() var wall := StaticBody3D.new()
wall.name = "Wall" wall.name = "Wall"
add_child(wall) add_child(wall)
var half_win_x := WIN_W / 2.0 var length := size.x if axis == "x" else size.z
var y0 := WIN_Y0 var sorted_offsets := offsets.duplicate()
var y1 := WIN_Y0 + WIN_H 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 func _wall_segment(wall: StaticBody3D, center: Vector3, size: Vector3, axis: String,
var L := size.x lateral: float, y_offset: float, span: float, height: float, mat: StandardMaterial3D) -> void:
var segs := [ var box := Vector3(span, height, size.z) if axis == "x" else Vector3(size.x, height, span)
{"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)}, var shift := Vector3(lateral, y_offset, 0) if axis == "x" else Vector3(0, y_offset, lateral)
{"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)}, _add_box_to(wall, box, center + shift, mat)
{"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 _add_glass(center: Vector3, size: Vector3, rot_axis: Vector3) -> void: func _add_glass(center: Vector3, size: Vector3) -> Area3D:
var glass := Area3D.new() var glass := Area3D.new()
glass.name = "Glass" glass.name = "Glass"
glass.collision_layer = 8 glass.collision_layer = 8
@@ -118,6 +144,7 @@ func _add_glass(center: Vector3, size: Vector3, rot_axis: Vector3) -> void:
mesh.mesh = bm mesh.mesh = bm
glass.add_child(mesh) glass.add_child(mesh)
glass_panes.append(glass) glass_panes.append(glass)
return glass
func shatter_glass(glass: Area3D) -> void: func shatter_glass(glass: Area3D) -> void:
var sound_origin := Node3D.new() var sound_origin := Node3D.new()
+44
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://bptxvotrlu7n4
+15 -4
View File
@@ -41,14 +41,25 @@ func run_test() -> void:
await physics_frame await physics_frame
await physics_frame await physics_frame
var space = world.get_world_3d().direct_space_state var space = world.get_world_3d().direct_space_state
# Cardinal routes from windows to center remain free at zombie torso height. # Ogni finestra deve essere un'APERTURA e deve essere COLLEGATA al centro: gli zombie
for edge in [Vector3(0, 0.8, -8), Vector3(0, 0.8, 8), Vector3(-8, 0.8, 0), Vector3(8, 0.8, 0)]: # 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) var q := PhysicsRayQueryParameters3D.create(edge, Vector3(0, 0.8, 0), 1)
q.exclude = [world.player.get_rid()] 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) var q := PhysicsRayQueryParameters3D.create(Vector3(-5, 0.5, -2), Vector3(-5, 0.5, -4), 1)
assert(not space.intersect_ray(q).is_empty()) 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(): if "--screenshot" in OS.get_cmdline_user_args():
world.player.set_physics_process(false) world.player.set_physics_process(false)
world.player.position = Vector3(0, 0.1, 0) world.player.position = Vector3(0, 0.1, 0)
+5 -2
View File
@@ -8,7 +8,10 @@ func run_test() -> void:
var world = load("res://scenes/main.tscn").instantiate() var world = load("res://scenes/main.tscn").instantiate()
root.add_child(world) root.add_child(world)
var surfaces = get_nodes_in_group("office_surfaces") 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: for surface in surfaces:
assert(surface.mesh is ArrayMesh) assert(surface.mesh is ArrayMesh)
var mat = surface.mesh.surface_get_material(0) var mat = surface.mesh.surface_get_material(0)
@@ -16,7 +19,7 @@ func run_test() -> void:
assert(mat.albedo_texture.resource_path.contains("/office/")) assert(mat.albedo_texture.resource_path.contains("/office/"))
var arrays = surface.mesh.surface_get_arrays(0) var arrays = surface.mesh.surface_get_arrays(0)
assert(arrays[Mesh.ARRAY_TEX_UV].size() == arrays[Mesh.ARRAY_VERTEX].size()) 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 north := Vector3(0, 2.5, -9.15)
var low := Vector3(0, 0.75, -9.15) var low := Vector3(0, 0.75, -9.15)
var high := Vector3(0, 4.25, -9.15) var high := Vector3(0, 4.25, -9.15)
+128
View File
@@ -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()
+1
View File
@@ -0,0 +1 @@
uid://bre5y2exwoc1l