extends Node3D const PlayerScript = preload("res://scripts/player.gd") const ZombieScript = preload("res://scripts/zombie.gd") const RoomScript = preload("res://scripts/room.gd") const LeaderboardScript = preload("res://scripts/leaderboard.gd") const StartScreenScript = preload("res://scripts/start_screen.gd") const Profile = preload("res://scripts/player_profile.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 last_window := -1 var leaderboard: Node var start_screen: CanvasLayer var profile := {} var score_submitted := false func _ready() -> void: smoke = "--smoke" in OS.get_cmdline_user_args() profile = Profile.load_profile() 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 # Glow compensates for the missing volumetric fog (unsupported in Compatibility): # a high threshold so only the fire core blooms, keeping the amber HUD readable. env.glow_enabled = true env.glow_intensity = 0.45 env.glow_strength = 1.0 env.glow_bloom = 0.05 env.glow_mix = 0.05 env.glow_hdr_threshold = 1.3 env.glow_blend_mode = Environment.GLOW_BLEND_MODE_ADDITIVE for level in [2, 3, 4]: env.set("glow_levels/%d" % level, 1.0 if level == 3 else 0.4) 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() leaderboard = Node.new() leaderboard.set_script(LeaderboardScript) leaderboard.player_id = profile["player_id"] add_child(leaderboard) leaderboard.leaderboard_ready.connect(_on_leaderboard_ready) leaderboard.leaderboard_failed.connect(_on_leaderboard_failed) leaderboard.score_submitted.connect(_on_score_submitted) leaderboard.submit_failed.connect(_on_submit_failed) player.died.connect(_on_player_died) start_screen = CanvasLayer.new() start_screen.set_script(StartScreenScript) start_screen.player = player start_screen.leaderboard = leaderboard start_screen.profile = profile add_child(start_screen) start_screen.started.connect(_on_start_pressed) if OS.has_feature("web"): if touch != null: touch.enabled = false # i tocchi servono alla schermata iniziale if smoke: get_tree().create_timer(12).timeout.connect(func(): print("SMOKE: wave=", wave, " alive=", alive, " player_hp=", player.hp) get_tree().quit()) ## Il pulsante GIOCA della schermata iniziale delega qui: sblocca l'audio (policy dei ## browser: serve un gesto dell'utente) e avvia la prima ondata. I test headless ## chiamano la stessa funzione con save_profile=false per non toccare il profilo reale. func _on_start_pressed(player_name: String) -> void: start_game(player_name, true) func start_game(player_name: String, save_profile := true) -> void: profile["name"] = player_name if save_profile: Profile.save_profile(profile) if not player.touch_mode: Input.mouse_mode = Input.MOUSE_MODE_CAPTURED if touch != null: touch.enabled = true if start_screen != null: start_screen.queue_free() start_screen = null _start_music() _next_wave() ## Alla morte si invia UNA volta il punteggio finale con nome e id del giocatore. func _on_player_died() -> void: if score_submitted: return score_submitted = true if int(score) > int(profile.get("best", 0)): profile["best"] = score Profile.save_profile(profile) player.set_death_note("invio del punteggio in corso...") leaderboard.submit(profile["name"], score) func _on_score_submitted(info: Dictionary) -> void: var total := int(info.get("total", 0)) player.set_death_note("PUNTEGGIO %d — %d° su %d partite" % [score, int(info.get("rank", 0)), total]) leaderboard.fetch() func _on_submit_failed(reason: String) -> void: player.set_death_note("punteggio non inviato (%s)" % reason) func _on_leaderboard_ready(rows: Array, total: int, me: Dictionary) -> void: if start_screen != null: start_screen.show_leaderboard(rows, total, me) func _on_leaderboard_failed(reason: String) -> void: if start_screen != null: start_screen.show_leaderboard_error(reason) ## Nota: qui non c'e' piu' nessuna rete di sicurezza sull'input. La partita parte ## solo dal pulsante GIOCA della schermata iniziale (segnale `started`): un click o un ## tocco sullo sfondo del menu non deve avviare nulla. ## 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() await get_tree().create_timer(0.8).timeout spawning = false _check_wave() ## 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 = start 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", face + Vector3(0, 1.55, 0), 1.2) tween.tween_callback(func(): 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: 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)