extends Node3D const PlayerScript = preload("res://scripts/player.gd") const ZombieScript = preload("res://scripts/zombie.gd") const RoomScript = preload("res://scripts/room.gd") const MUSIC_PATH := "res://assets/audio/music/zombie_industrial_loop.ogg" const MUSIC_VOLUME_DB := -11.0 const TouchControls = preload("res://scripts/touch_controls.gd") var player: CharacterBody3D var room: Node3D var music: AudioStreamPlayer var music_started := false var touch: Node var hud: CanvasLayer var navigation: Node var score := 0 var wave := 0 var alive := 0 var spawning := false var wave_pending := false var smoke := false var awaiting_web_start := false var start_label: Label func _ready() -> void: smoke = "--smoke" in OS.get_cmdline_user_args() room = Node3D.new() room.set_script(RoomScript) add_child(room) player = CharacterBody3D.new() player.set_script(PlayerScript) player.position = Vector3(0, 0.1, 0) add_child(player) var envnode := WorldEnvironment.new() var env := Environment.new() env.background_mode = Environment.BG_SKY var sky_material := PanoramaSkyMaterial.new() sky_material.panorama = load("res://assets/textures/sky/space_panorama.png") env.sky = Sky.new() env.sky.sky_material = sky_material # Solo sfondo: illuminazione e riflessi non dipendono dal cielo scuro. env.reflected_light_source = Environment.REFLECTION_SOURCE_DISABLED env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR env.ambient_light_color = Color(0.28, 0.36, 0.48) env.ambient_light_energy = 0.12 envnode.environment = env add_child(envnode) var office := Node3D.new() office.set_script(preload("res://scripts/office_atmosphere.gd")) add_child(office) navigation = Node.new() navigation.set_script(preload("res://scripts/room_navigation.gd")) add_child(navigation) var canvas := CanvasLayer.new() add_child(canvas) hud = CanvasLayer.new() hud.set_script(preload("res://scripts/game_hud.gd")) add_child(hud) var cross := Label.new() cross.text = "+" cross.add_theme_font_size_override("font_size", 30) canvas.add_child(cross) cross.set_anchors_and_offsets_preset(Control.PRESET_CENTER) _setup_music() _setup_touch() if OS.has_feature("web"): awaiting_web_start = true if touch != null: touch.enabled = false # il primo tocco/click avvia la partita start_label = Label.new() var howto := "trascina a sinistra: muovi · destra: guarda · pulsanti: fuoco/granata/ricarica/salto" if player.touch_mode else "WASD: movimento · clic sinistro: spara · destro: granata · R: ricarica" start_label.text = ("TOCCA PER INIZIARE" if player.touch_mode else "CLICCA PER INIZIARE") + "\n" + howto start_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER start_label.add_theme_font_size_override("font_size", 24) start_label.mouse_filter = Control.MOUSE_FILTER_IGNORE canvas.add_child(start_label) start_label.set_anchors_and_offsets_preset(Control.PRESET_CENTER) else: _start_music() _next_wave() if smoke: get_tree().create_timer(12).timeout.connect(func(): print("SMOKE: wave=", wave, " alive=", alive, " player_hp=", player.hp) get_tree().quit()) func _unhandled_input(event: InputEvent) -> void: if not awaiting_web_start: return var started := false if event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT: started = true elif event is InputEventScreenTouch and event.pressed: started = true # primo tocco su mobile if started: awaiting_web_start = false start_label.queue_free() if not player.touch_mode: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if touch != null: touch.enabled = true # Il browser concede l'audio solo dopo un gesto dell'utente: qui il click/tocco. _start_music() _next_wave() ## Controlli on-screen: attivi su browser mobile o touchscreen, forzabili con ## --touch / --no-touch sulla riga di comando e con ?touch=1 / ?touch=0 sul Web. func _setup_touch() -> void: var forced := TouchControls.force_mode() if not (forced > 0 or (forced == 0 and TouchControls.should_auto_enable())): return player.touch_mode = true # Su un dispositivo touch il pointer lock non serve e su desktop forzato # ruberebbe il cursore: nessuna cattura del mouse in modalita' touch. Input.mouse_mode = Input.MOUSE_MODE_VISIBLE touch = CanvasLayer.new() touch.set_script(TouchControls) touch.player = player # Su desktop forzato con --touch i drag del mouse valgono come tocchi, # cosi' la UI e' provabile anche senza dispositivo touch. touch.mouse_fallback = forced > 0 and not DisplayServer.is_touchscreen_available() add_child(touch) ## Colonna sonora: industrial metal strumentale su loop senza giunzione udibile. func _setup_music() -> void: music = AudioStreamPlayer.new() music.name = "Music" music.stream = load(MUSIC_PATH) music.volume_db = MUSIC_VOLUME_DB if music.stream is AudioStreamOggVorbis: music.stream.loop = true add_child(music) music.add_to_group("music") # Rete di sicurezza: se il flag di loop mancasse, il brano riparte a fine coda. music.finished.connect(_restart_music) func _start_music() -> void: if music_started or music == null: return music_started = true music.play() func _restart_music() -> void: if music_started and is_instance_valid(music): music.play() func _process(_dt: float) -> void: if hud: hud.update_status(player, wave, score) func zombies_for_wave(number: int) -> int: return 4 + number * 2 func _next_wave() -> void: wave_pending = false if player.hp <= 0 or spawning: return wave += 1 spawning = true for i in zombies_for_wave(wave): if player.hp <= 0: break _spawn(i % 4) 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() var zombie := CharacterBody3D.new() zombie.set_script(ZombieScript) zombie.player = player zombie.navigation = navigation zombie.position = centers[index] - inward * 1.2 add_child(zombie) alive += 1 zombie.killed.connect(func(headshot: bool): score += 150 if headshot else 100 alive -= 1 _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_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) tween.tween_callback(func(): zombie.entering = false) func _check_wave() -> void: if alive == 0 and not spawning and not wave_pending and player.hp > 0: wave_pending = true get_tree().create_timer(3).timeout.connect(_next_wave)