extends Node3D @export_group("Riferimenti Base") @export var path_treno: Path3D @export_group("I 3 Biomi Principali") @export var bioma_campagna: Array[PackedScene] @export var bioma_foresta: Array[PackedScene] @export var bioma_altro: Array[PackedScene] @export_group("Impostazioni Griglia e Aree") @export var dimensione_chunk: float = 20.0 @export var raggio_visivo: int = 3 @export var scala_distretti: float = 0.05 var scacchiera: Dictionary = {} var ultima_pos_griglia_treno: Vector2i = Vector2i(999999, 999999) var generatore_rumore: FastNoiseLite var usa_terzo_bioma: bool = true # --- VARIABILI PER L'INTERFACCIA A SCHERMO --- var lbl_bioma: Label var lbl_distanza: Label func _ready() -> void: if bioma_campagna.is_empty() or path_treno == null: print("โŒ ERRORE: Assegna almeno il 'Bioma Campagna' e il 'Path Treno' nell'Inspector!") return generatore_rumore = FastNoiseLite.new() generatore_rumore.noise_type = FastNoiseLite.TYPE_PERLIN generatore_rumore.seed = randi() generatore_rumore.frequency = scala_distretti # Creiamo l'interfaccia a schermo! _crea_ui_gps() print("โœ… Generatore a Distretti Dinamici Pronto (Versione Ottimizzata).") print("Premere il tasto 'B' per abilitare/disabilitare il terzo bioma.") # --- CREA LE SCRITTE A SCHERMO --- func _crea_ui_gps() -> void: var canvas = CanvasLayer.new() canvas.layer = 10 add_child(canvas) var margin = MarginContainer.new() margin.set_anchors_preset(Control.PRESET_FULL_RECT) margin.add_theme_constant_override("margin_top", 20) margin.add_theme_constant_override("margin_right", 20) canvas.add_child(margin) var vbox = VBoxContainer.new() vbox.size_flags_horizontal = Control.SIZE_SHRINK_END margin.add_child(vbox) # --- SCRITTA BIOMA ATTUALE (Titolo) --- lbl_bioma = Label.new() lbl_bioma.add_theme_font_size_override("font_size", 10) lbl_bioma.add_theme_color_override("font_color", Color(1, 0.9, 0.4)) lbl_bioma.add_theme_color_override("font_outline_color", Color.BLACK) lbl_bioma.add_theme_constant_override("outline_size", 3) lbl_bioma.text = "๐Ÿ“ Avvio GPS..." vbox.add_child(lbl_bioma) # --- SCRITTA METRI MANCANTI (Sottotitolo) --- lbl_distanza = Label.new() lbl_distanza.add_theme_font_size_override("font_size", 8) lbl_distanza.add_theme_color_override("font_outline_color", Color.BLACK) lbl_distanza.add_theme_constant_override("outline_size", 3) lbl_distanza.text = "Calcolo tragitto..." vbox.add_child(lbl_distanza) _radar_bioma_treno() func _input(event: InputEvent) -> void: if event is InputEventKey and event.pressed and not event.echo: if event.keycode == KEY_B: usa_terzo_bioma = !usa_terzo_bioma # E forziamo un aggiornamento del radar per aggiornare le scritte subito _radar_bioma_treno() func _physics_process(_delta: float) -> void: if path_treno == null or path_treno.treno_istanziato == null: return var pos_treno = path_treno.treno_istanziato.global_position var grid_x = roundi(pos_treno.x / dimensione_chunk) var grid_z = roundi(pos_treno.z / dimensione_chunk) var pos_attuale = Vector2i(grid_x, grid_z) if pos_attuale != ultima_pos_griglia_treno: ultima_pos_griglia_treno = pos_attuale # 1. Genera i nuovi chunk davanti al treno _genera_attorno_al_treno(pos_attuale) # 2. Pulisce i chunk vecchi dietro al treno (OTTIMIZZAZIONE) _pulisci_chunk_lontani(pos_attuale) # 3. Aggiorna il GPS a schermo _radar_bioma_treno() func _genera_attorno_al_treno(centro: Vector2i) -> void: for x in range(-raggio_visivo, raggio_visivo + 1): for z in range(-raggio_visivo, raggio_visivo + 1): var pos_griglia = centro + Vector2i(x, z) if not scacchiera.has(pos_griglia): var ce_ostacolo = _registra_cella_con_laser(pos_griglia) if not ce_ostacolo: _piazza_bioma_compatibile(pos_griglia) func _scegli_catalogo_per_cella(pos_griglia: Vector2i) -> Array[PackedScene]: var valore = generatore_rumore.get_noise_2d(pos_griglia.x, pos_griglia.y) var nome_bioma_scelto = _get_nome_bioma(valore) var catalogo_scelto: Array[PackedScene] = [] if nome_bioma_scelto == "Campagna": catalogo_scelto = bioma_campagna elif nome_bioma_scelto == "Campagna-Foresta": catalogo_scelto = bioma_campagna if randf() > 0.5 else bioma_foresta elif nome_bioma_scelto == "Foresta": catalogo_scelto = bioma_foresta elif nome_bioma_scelto == "Foresta-Altro": catalogo_scelto = bioma_foresta if randf() > 0.5 else bioma_altro else: catalogo_scelto = bioma_altro if catalogo_scelto.is_empty(): catalogo_scelto = bioma_campagna return catalogo_scelto func _registra_cella_con_laser(pos_griglia: Vector2i) -> bool: if scacchiera.has(pos_griglia): return true var pos_mondo = Vector3(pos_griglia.x * dimensione_chunk, 0, pos_griglia.y * dimensione_chunk) var space_state = get_world_3d().direct_space_state var origine_raggio = Vector3(pos_mondo.x, 50.0, pos_mondo.z) var fine_raggio = Vector3(pos_mondo.x, -10.0, pos_mondo.z) var query = PhysicsRayQueryParameters3D.create(origine_raggio, fine_raggio) var result = space_state.intersect_ray(query) if result.size() > 0: var collider = result["collider"] var nodo_corrente = collider var uscite_trovate = {"nord": false, "est": false, "sud": false, "ovest": false} while nodo_corrente != null and nodo_corrente != get_tree().root: if nodo_corrente.has_method("get_uscite_ruotate"): var scatti_rotazione = roundi(rad_to_deg(nodo_corrente.global_rotation.y) / -90.0) uscite_trovate = nodo_corrente.get_uscite_ruotate(scatti_rotazione) break nodo_corrente = nodo_corrente.get_parent() scacchiera[pos_griglia] = {"tipo": "ostacolo", "uscite": uscite_trovate} return true return false func _piazza_bioma_compatibile(pos_griglia: Vector2i) -> void: var req_nord = _richiede_connessione(pos_griglia + Vector2i(0, -1), "sud") var req_est = _richiede_connessione(pos_griglia + Vector2i(1, 0), "ovest") var req_sud = _richiede_connessione(pos_griglia + Vector2i(0, 1), "nord") var req_ovest = _richiede_connessione(pos_griglia + Vector2i(-1, 0), "est") var catalogo_zona = _scegli_catalogo_per_cella(pos_griglia) var candidati = [] for scena in catalogo_zona: var test_chunk = scena.instantiate() if test_chunk.has_method("get_uscite_ruotate"): for rot in range(4): var u = test_chunk.get_uscite_ruotate(rot) var match_n = (req_nord == -1) or ((req_nord == 1) == u["nord"]) var match_e = (req_est == -1) or ((req_est == 1) == u["est"]) var match_s = (req_sud == -1) or ((req_sud == 1) == u["sud"]) var match_o = (req_ovest == -1) or ((req_ovest == 1) == u["ovest"]) if match_n and match_e and match_s and match_o: candidati.append({"scena": scena, "rotazione": rot, "uscite": u}) test_chunk.queue_free() if candidati.size() > 0: var scelto = candidati.pick_random() var nuovo_chunk = scelto.scena.instantiate() add_child(nuovo_chunk) nuovo_chunk.position = Vector3(pos_griglia.x * dimensione_chunk, 0, pos_griglia.y * dimensione_chunk) nuovo_chunk.rotation.y = scelto.rotazione * (-PI / 2.0) # MODIFICA: Salviamo il riferimento al nodo per poterlo eliminare in futuro scacchiera[pos_griglia] = {"tipo": "bioma", "uscite": scelto.uscite, "nodo": nuovo_chunk} else: var backup = catalogo_zona[0].instantiate() add_child(backup) backup.position = Vector3(pos_griglia.x * dimensione_chunk, 0, pos_griglia.y * dimensione_chunk) # MODIFICA: Salviamo il riferimento al nodo scacchiera[pos_griglia] = {"tipo": "bioma", "uscite": {"nord":false, "est":false, "sud":false, "ovest":false}, "nodo": backup} func _richiede_connessione(pos_vicino: Vector2i, lato_richiesto: String) -> int: if not scacchiera.has(pos_vicino): _registra_cella_con_laser(pos_vicino) if scacchiera.has(pos_vicino): return 1 if scacchiera[pos_vicino]["uscite"][lato_richiesto] else 0 return -1 func _get_nome_bioma(valore: float) -> String: if valore < -0.4: return "Campagna" elif valore < -0.15: return "Campagna-Foresta" if not usa_terzo_bioma: return "Foresta" if valore < 0.15: return "Foresta" elif valore < 0.4: return "Foresta-Altro" else: return "Altro (Deserto/Neve)" # --- AGGIORNAMENTO GPS A SCHERMO --- func _radar_bioma_treno() -> void: if path_treno == null or path_treno.curve == null or path_treno.treno_istanziato == null: return var pos_treno = path_treno.treno_istanziato.global_position var grid_x = roundi(pos_treno.x / dimensione_chunk) var grid_z = roundi(pos_treno.z / dimensione_chunk) var valore_attuale = generatore_rumore.get_noise_2d(grid_x, grid_z) var bioma_attuale = _get_nome_bioma(valore_attuale) var progresso_attuale = path_treno.progresso_treno var lunghezza_totale = path_treno.curve.get_baked_length() var distanza_cambio = 0.0 var prossimo_bioma = "" for step in range(1, 31): var progresso_futuro = wrapf(progresso_attuale + (step * dimensione_chunk), 0.0, lunghezza_totale) var pos_futura_locale = path_treno.curve.sample_baked(progresso_futuro, true) var pos_futura_globale = path_treno.to_global(pos_futura_locale) var f_x = roundi(pos_futura_globale.x / dimensione_chunk) var f_z = roundi(pos_futura_globale.z / dimensione_chunk) var valore_futuro = generatore_rumore.get_noise_2d(f_x, f_z) var bioma_futuro = _get_nome_bioma(valore_futuro) if bioma_futuro != bioma_attuale: distanza_cambio = step * dimensione_chunk prossimo_bioma = bioma_futuro break if lbl_bioma and lbl_distanza: lbl_bioma.text = bioma_attuale if prossimo_bioma != "": lbl_distanza.text = str(distanza_cambio) + "m โž” " + prossimo_bioma else: lbl_distanza.text = "Nessun cambio nei prossimi 600m" # --- NUOVA FUNZIONE: SPAZZINO DEI CHUNK LONTANI --- func _pulisci_chunk_lontani(centro_attuale: Vector2i) -> void: var celle_da_rimuovere = [] var margine_sicurezza = 2 # Eliminiamo solo i chunk fuori dal raggio visivo + 2 for pos_griglia in scacchiera.keys(): var cella = scacchiera[pos_griglia] if cella["tipo"] == "ostacolo": continue var dist_x = abs(pos_griglia.x - centro_attuale.x) var dist_z = abs(pos_griglia.y - centro_attuale.y) # Se il chunk รจ troppo lontano dalla telecamera... if dist_x > raggio_visivo + margine_sicurezza or dist_z > raggio_visivo + margine_sicurezza: # 1. Distruggiamo il modello 3D per liberare memoria video if cella.has("nodo") and is_instance_valid(cella["nodo"]): cella["nodo"].queue_free() # 2. Ci segniamo questa posizione per rimuoverla dalla logica celle_da_rimuovere.append(pos_griglia) # 3. Puliamo la scacchiera (liberando memoria RAM) for pos in celle_da_rimuovere: scacchiera.erase(pos)