Eight windows per room and random zombie entry from different windows
This commit is contained in:
@@ -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
@@ -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:
|
||||
|
||||
+81
-54
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bptxvotrlu7n4
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -0,0 +1 @@
|
||||
uid://bre5y2exwoc1l
|
||||
Reference in New Issue
Block a user