Files
zombie-fps/scripts/room.gd
T

266 lines
11 KiB
GDScript

## 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
const W: float = 18.0 # larghezza (x)
const D: float = 18.0 # profondita (z)
const H: float = 5.0 # altezza (y)
const WALL_T: float = 0.3
const WALL_REPEAT_M := 4.0 # 128x160 texture spans 4x5m: equal texel density
const CEILING_REPEAT_M := 1.2 # CEIL5 contains 2x2 panels: each panel = 0.6m
# Finestra: 4 m x 2 m, bordo basso a 1.5 m
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] = []
## 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
func _ready() -> void:
var wall_mat := _surface_material("wall", 0.9)
var floor_mat := _surface_material("floor", 0.95)
var ceil_mat := _surface_material("ceiling", 0.85)
# Pavimento
_box(Vector3(W, 0.2, D), Vector3(0, -0.1, 0), floor_mat, true)
# Soffitto
_box(Vector3(W, 0.2, D), Vector3(0, H + 0.1, 0), ceil_mat, false)
# 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 := [
{"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 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)
## 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
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)
## 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 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
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) -> Area3D:
var glass := Area3D.new()
glass.name = "Glass"
glass.collision_layer = 8
glass.collision_mask = 0
add_child(glass)
var shape := CollisionShape3D.new()
var box := BoxShape3D.new()
box.size = size
shape.shape = box
glass.add_child(shape)
glass.global_position = center
var mesh := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = size
var gm := StandardMaterial3D.new()
gm.albedo_color = Color(0.7, 0.85, 0.9, 0.16)
gm.cull_mode = BaseMaterial3D.CULL_DISABLED
gm.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
gm.shading_mode = BaseMaterial3D.SHADING_MODE_PER_PIXEL
gm.roughness = 0.05
gm.metallic = 0.1
bm.material = gm
mesh.mesh = bm
glass.add_child(mesh)
glass_panes.append(glass)
return glass
func shatter_glass(glass: Area3D) -> void:
var sound_origin := Node3D.new()
add_child(sound_origin)
sound_origin.global_position = glass.global_position
preload("res://scripts/hl_audio.gd").play(sound_origin, "debris/bustglass%d.wav" % randi_range(1, 3), true, -12)
get_tree().create_timer(4).timeout.connect(sound_origin.queue_free)
for i in glass_panes.size():
if glass_panes[i] == glass:
glass_panes.remove_at(i)
break
# Schegge di vetro (particelle semplici)
var sh := CPUParticles3D.new()
sh.direction = Vector3(0, -0.3, 0)
sh.spread = 180
sh.initial_velocity_min = 1.5
sh.initial_velocity_max = 4
sh.gravity = Vector3(0, -9.8, 0)
sh.scale_amount_min = 0.4
sh.scale_amount_max = 1.2
sh.color = Color(0.7, 0.85, 1.0, 0.8)
sh.emission_shape = CPUParticles3D.EMISSION_SHAPE_SPHERE
sh.emission_sphere_radius = 1.2
var shard := BoxMesh.new()
shard.size = Vector3(0.05, 0.02, 0.08)
var shard_mat := StandardMaterial3D.new()
shard_mat.albedo_color = Color(0.6, 0.8, 0.9)
shard.material = shard_mat
sh.mesh = shard
sh.amount = 40
sh.lifetime = 0.9
sh.one_shot = true
sh.explosiveness = 1.0
sh.position = glass.global_position
add_child(sh)
sh.emitting = true
get_tree().create_timer(2).timeout.connect(sh.queue_free)
glass.queue_free()
func _box(s: Vector3, c: Vector3, m: StandardMaterial3D, collide: bool) -> void:
var body := StaticBody3D.new()
add_child(body)
_add_box_to(body, s, c, m)
if not collide:
body.get_node("shape").set_deferred("disabled", true)
func _add_box_to(parent: Node3D, s: Vector3, c: Vector3, m: StandardMaterial3D) -> void:
var mi := MeshInstance3D.new()
var bm := BoxMesh.new()
bm.size = s
bm.material = m
if m.has_meta("office_surface"):
# BoxMesh's per-face UVs reset on every wall segment. Rebuild only UVs;
# geometry, normals, indices and collision boxes remain identical.
var arrays := bm.surface_get_arrays(0)
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
var uv := PackedVector2Array()
for vertex in vertices:
uv.append(office_uv(vertex + c, c, m.get_meta("office_surface")))
arrays[Mesh.ARRAY_TEX_UV] = uv
var mesh := ArrayMesh.new()
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
mesh.surface_set_material(0, m)
mi.mesh = mesh
mi.add_to_group("office_surfaces")
else:
mi.mesh = bm
parent.add_child(mi)
var cs := CollisionShape3D.new()
var bs := BoxShape3D.new()
bs.size = s
cs.shape = bs
cs.position = c - parent.position if parent is StaticBody3D else c
cs.name = "shape"
parent.add_child(cs)
# il mesh va centrato sullo stesso punto della collision
mi.position = c - parent.position if parent is StaticBody3D else c
func _surface_material(texture_name: String, rough: float) -> StandardMaterial3D:
var material := StandardMaterial3D.new()
var office := texture_name in ["wall", "ceiling"]
var directory := "res://assets/textures/office/" if office else "res://assets/textures/"
material.albedo_texture = load(directory + texture_name + ".png")
material.albedo_color = Color.WHITE
material.roughness = rough
material.texture_filter = BaseMaterial3D.TEXTURE_FILTER_LINEAR_WITH_MIPMAPS_ANISOTROPIC
material.texture_repeat = true
if office:
material.set_meta("office_surface", texture_name)
else:
# Existing floor projection intentionally unchanged.
material.uv1_triplanar = true
material.uv1_world_triplanar = true
material.uv1_scale = Vector3.ONE * 0.5
return material
## Shared room-space UVs, independent of each window/wall segment size.
static func office_uv(point: Vector3, center: Vector3, surface: String) -> Vector2:
if surface == "ceiling":
return Vector2(point.x + W * 0.5, point.z + D * 0.5) / CEILING_REPEAT_M
var along: float
if center.z < -D * 0.5:
along = point.x + W * 0.5 # north, clockwise
elif center.x > W * 0.5:
along = W + point.z + D * 0.5 # east
elif center.z > D * 0.5:
along = W + D + W * 0.5 - point.x # south
else:
along = W * 2 + D + D * 0.5 - point.z # west
# Top pixel at ceiling, bottom at floor: no vertical texture repeats.
# Half-texel inset avoids sampling the opposite edge at exact 0/1.
return Vector2(along / WALL_REPEAT_M, clampf(1.0 - point.y / H, 0.5 / 160.0, 1.0 - 0.5 / 160.0))
func _mat(color: Color, rough: float) -> StandardMaterial3D:
var m := StandardMaterial3D.new()
m.albedo_color = color
m.roughness = rough
return m