← Novità

v0.50

2026-09-08 → 2026-09-16 · 130 voce/i di changelog

La 0.50 e' fuori su tutti e quattro i canali, e i 47 tester sono stati avvisati2026-09-16

Chiusura del giro /rilascio-beta cominciato la notte del 16/09. Tag v0.50-personaggi-nemici-multiplayer-inviti-megaupdate.

  • Tempo 1: desktop (dmg 172 MB, zip 113 MB) su iCloud e Google Drive; TestFlight con note *What to Test* in italiano, gruppo esterno «Divano» e betaAppReviewSubmissionWAITING_FOR_BETA_REVIEW; AAB in bozza sul canale alpha (versionCode 50) con le note di rilascio via --solo-note.
  • Tempo 2, sbloccato da betaReviewState APPROVED: play_upload.sh --pubblica ha portato la bozza da draft a completed senza ricaricare il bundle. 📌 E' la prima volta che SU-774 gira su un canale vero: la via --bozza al Tempo 1 e --pubblica al Tempo 2 funziona, la riserva --invia non e' servita.
  • Tempo 3: push FCM al topic su_beta (messages/4692916125686022932, testo che non dice «aggiorna»); devices.json rigenerato (3 dispositivi di 2 persone, ultima_versione 0.50, obbligatoria_dal 2026-09-23); manifest version_stage.json pubblicato con versione minima multiplayer 0.50 — obbligatorio, perche' le RPC sono cambiate dall'ultimo tag.
  • 47 tester avvisati: 46 su WhatsApp dall'MCP (30 Android, 15 Apple, 1 solo desktop) e 1 su Telegram scritto a mano dal client web. Tre lanci separati di genera_link_messaggi.py con --piattaforma android|apple|pc, ognuno con la chiusura del suo canale: un lancio unico avrebbe mandato «Aggiorna da Play» anche ai tester iOS e desktop.
  • ⚠️ Il lock ~$Beta_Tester_Homeless_City.xlsx era orfano dall'11/09 (Excel non in esecuzione): --forza con l'ok di Ivan.
  • Simboli di debug nativi caricati da Ivan (confermato in chat il 16/09), ultimo passo del giro come vuole la regola del 2026-09-05: da qui in poi gli stack trace nativi in Android vitals si leggono invece di restare indirizzi esadecimali. Lo zip combinato TMP/su709_simboli/Godot_native_debug_symbols.4.7.2.combinato.zip (731 MB) e' verificato per questa build — motore 4.7.2 e md5 della libeosg uguale a tools/android/eosg_16kb/simboli/ (1351c493c0b3621959e65725c8599445). Cartella e console gia' aperte.

Su Play saliva l'AAB con il version/code della release precedente (`export_all.sh`)2026-09-16

Trovato lanciando il Tempo 1 della 0.50: play_upload.sh --bozza respinto con HTTP 403 — Version code 43 has already been used, con config/version gia' a 0.50 e il pre-volo tutto verde.

  • 🔧 Causa: i preset Android in files/homeless_city/export_presets.cfg sono due — «Android» (APK, preset.3) e «Android Play (AAB)» (preset.5, quello che si carica sullo store). _allinea_version_code() di tools/release/export_all.sh cercava il primo platform="Android" e usciva dal ciclo: allineava sempre e solo l'APK, e l'AAB restava al numero della release prima. Sulla 0.43 non si era visto perche' i due numeri coincidevano gia'.
  • ✅ Ora la funzione raccoglie tutti i preset Android e li allinea uno per uno, stampando il nome di quello toccato: ✅ pre-volo «Android Play (AAB)»: version/code 43 → 50 (da versione 0.50). Se in un preset manca la chiave, si ferma dicendo quale.
  • ✅ Riesportato l'AAB e ricaricato: versionCode 50 in bozza sul canale alpha, i tester continuano a ricevere la 43 completed fino al Tempo 2.
  • 📌 Il pre-volo di play_upload.sh non poteva accorgersene: legge il versionName (0.50, giusto) e il version/code del canale, non il version/code del bundle.

Release 0.50: note in-game in otto lingue, versione a 0.50 (`/rilascio-beta`)2026-09-16

Giro di rilascio beta chiesto da Ivan («numero versione 0.50»). Delta enorme: dall'ultimo tag di release v0.43-carte-dispetto-lobby-pad-touch-carnagioni sono passate 170 voci di CHANGELOG, cioe' sette versioni di lavoro compresse in una sola uscita.

  • ✅ Sezione v0.50 in cima a files/homeless_city/assets/release_notes_it.txt piu' le 7 gemelle (_en, _fr, _es, _de, _ru, _zh, _pt): 22 voci nel formato SU-721 - TITOLO: testo, sezioni vecchie intatte. Le novita' grosse raccontate al giocatore: possessione dei nemici da fantasma, INVITA UN AMICO con le lattine d'oro e la sezione SOLO SU INVITO, progressi sull'account con Google/Apple, Brina ghiacciata coi suoi nemici e la torcia, il fenicottero della Collina, la chiamata del tunnel in multiplayer, classifica in diretta, bonus metro con tetto e timbri del controllore, busking per skin, quindici facce nuove, POSIZIONA PULSANTI, tasti QWER.
  • 📌 Il riccone della Collina resta fuori dalle note: e' spento per la release (SU-814, deciso da Ivan il 15/09), quindi il giocatore non lo incontra.
  • ⚠️ I nomi propri tradotti non si inventano: quartieri, negozio e voci di menu presi dalle chiavi vere di files/homeless_city/assets/translations/ (BRINA → HOARFROST/GIVRE/ESCARCHA/RAUREIF/ИНЕЙ/霜降/GEADA, SOLLEONE → DOG DAYS/CANICULE…, POSIZIONA PULSANTI → POSITION BUTTONS…). Il primo getto di inglese e francese aveva nomi inventati (FROSTBITE, GIVRETTE, Cagnard), corretti prima del commit.
  • config/version di files/homeless_city/project.godot a 0.50; la guardia 1b di prepare_release.sh e' verde su tutte e otto le lingue.
  • [NOVITA'] La possessione — da fantasma, in multiplayer, entri dentro un nemico e usi il suo attacco: yeti, pinguini, fenicotteri, poliziotti.
  • [NOVITA'] Invita un amico — un codice regalo nel menu principale: chi lo usa fa arrivare una lattina d'oro a lui e una a te, da spendere nel negozio SOLO SU INVITO.
  • [NOVITA'] I progressi sull'account — colleghi Google o Apple una volta e lattine, carte, skin e record ti seguono da un telefono all'altro, anche da Android a iPhone.
  • [NOVITA'] Brina diventa di ghiaccio — nevica sempre, sui marciapiedi si pattina, e il quartiere ha i suoi nemici: pinguini, pupazzi di neve e lo yeti. Per terra ci sono le torce, e dove cadono prende fuoco.
  • [NOVITA'] La chiamata del tunnel — in multiplayer il tunnel della metro si apre a tutti insieme: chi scende lancia la chiamata, gli altri decidono se seguirlo.
  • [NOVITA'] La classifica in diretta — online sta in alto a destra mentre giochi, e le righe scivolano quando qualcuno ti passa davanti.
  • [NOVITA'] Il bonus metro cambia regola — si ferma a 400 lattine, dal quarto giro paga in carte e vite, dal terzo i treni arrivano infuocati. Ogni vittoria lascia un timbro del controllore, e a 3, 7 e 15 timbri si apre un cosmetico della Baracca.
  • [NOVITA'] Quindici personaggi con la loro faccia — facce, ritratti e fantasmi tutti diversi, e un numero di busking per ognuno. Suonare adesso fa salire di livello davvero.
  • [NOVITA'] Posiziona i pulsanti — joystick e tasti si trascinano dove si vuole sullo schermo; su tastiera i poteri passano a Q, W, E, R.
  • ⚠️ Guardia RPC accesa: dall'ultimo tag sono cambiate le RPC di NetworkManager.gd, World.gd e la firma di _net_state in Player.gd. La versione minima multiplayer del manifest di canale va a 0.50 (SU-934), altrimenti una build vecchia entra in lobby e non gioca, in silenzio.

I moduli privacy dei due store compilati davvero: Apple pubblicato, Play inviato in revisione (SU-794)2026-09-16

Ivan in chat: «facciamo i moduli di play e apple», poi «Verifica per sicurezza i dati su play». Le risposte erano già decise nella voce 224; qui sono state messe in console.

  • Apple, App Privacy pubblicata: Indirizzo email nuova (Funzionalità dell'app, collegata all'identità), Interazione con il prodotto da «solo Analisi, non collegata» ad «Analisi + Funzionalità dell'app, collegata», Altre tipologie di dati nuova (Funzionalità dell'app, non collegata) per IP e metadati della GET del manifest. Niente tolto, tracciamento «No» ovunque; da 5 a 7 tipologie. Il wizard chiude ogni tipologia con «Pubblica», quindi sono tre pubblicazioni.
  • Play: STORE_ASSETS/google_play/data_safety_2026-09-16.csv importato da Ivan e verificato sull'Anteprima della scheda dello Store, voce per voce, in tutti e sei i gruppi di modifiche — compreso «ID dispositivo o altri ID» che ora compare senza il bollino «Facoltativo», cioè richiesto.
  • ✅ URL dell'informativa in console allineato (deciso in chat): da https://blackwindita.github.io/street-university-beta-devices/ a https://www.streetuniversitygame.com/privacy/, lo stesso di Apple e dell'URL di cancellazione. Salvato insieme al questionario e inviato nella stessa revisione.
  • ⚠️ Trappola, e per un po' ci eravamo cascati: su Play «Salva» non manda in revisione, mette solo in coda — serve «Invia N modifiche per la revisione» dalla Panoramica della pubblicazione. La dicitura «Pubblicazione gestita disattivata» era stata letta come «allora Salva pubblica subito», ed è falsa. Scritto in DISTRIBUZIONE_STORE.md §8.2 con i link diretti al modulo e alla panoramica, che non si ricostruiscono dal nome del pacchetto.
  • ✅ Verificato che il punto 3 del ticket era già a posto: le due righe di regolamento nella schermata invita (ACCOUNT_INVITI_REGOLA_COSMETICA e ACCOUNT_INVITI_REGOLA_VALORE, 8 lingue, mostrate da files/homeless_city/scripts/ui/MainMenu.gd).
  • 📌 Stato: Apple live, Play «Modifiche in fase di revisione» — Google dichiara di solito entro 7 giorni. Resoconto in TMP/su794/report_moduli_store.md.

Informativa privacy 1.5 pubblicata nelle due copie, e risposte dei moduli di Play e Apple per la prossima versione (SU-794)2026-09-16

Ivan in chat il 15/09: «794 facciamo tutto per informativa moduli store ecc, così poi siamo pronti a pubblicare sugli store la nuova versione». Lotti Codex L794, L794b, L794c (Sol), istruttoria I794 (Sol).

  • 🔧 Le due copie non erano più identiche: il sito aveva una 1.4 del 12/09 scritta prima della scelta A (conteggio inviti con una query dal client), ../street-university-beta-devices era alla 1.3. Ora la 1.5 è identica in ../street-university-site/privacy/index.html (commit 0678489) e ../street-university-beta-devices/index.html (commit 615e939), 16 settembre 2026.
  • ✅ Istruttoria I794 sul diff v0.43-carte-dispetto-lobby-pad-touch-carnagioni..dev: oltre agli inviti, sette flussi mancanti o parziali, ora descritti: salvataggio dei progressi su EOS Player Data Storage (progressi.json, prima l'informativa diceva che i progressi restavano sul dispositivo), registro dei nickname su Firestore con moderazione e letture pubbliche, accessi Apple/Google collegati a EOS e trasloco, liberazione del nome con token EOS, manifest delle versioni da GitHub su tutte le build, dati di lobby e partita, telemetria con orari e indicatore dei codici beta.
  • ✅ Inviti: riscatti e legami (SU-993/SU-994) senza letture dal client, conteggio dal server con cache di 60 s e contatore orario col PUID, scadenza a 365 giorni con TTL attivo su riscatti/scade e legami/scade, link d'invito e hosting GitHub Pages del sito.
  • ✅ Email: i token di Apple (scope name+email) e Google la contengono e vanno interi a EOS: dichiarata.
  • DISTRIBUZIONE_STORE.md §8.2-§8.3 e STORE_ASSETS/google_play/data_safety_2026-09-16.csv (da data_safety_2026-08-31.csv): Interazioni con l'app, Indirizzo email, Altre azioni per il salvataggio, ID dispositivo per l'IP del manifest (deciso da Ivan il 16/09), Prevenzione frodi/sicurezza per i limiti degli inviti. Bozza operativa in TMP/su794/moduli_store.md.
  • 📌 Seguiti aperti: SU-995 (bocciati del registro leggibili senza accesso, nessuna scadenza), SU-996 (CANCELLA ACCOUNT nel gioco, Apple 5.1.1(v)).
  • ⚠️ NON fatto qui: import del CSV nella Play Console e App Privacy su App Store Connect (dal browser). Fatti subito dopo, voce 225.

Inviti: il legame fra chi invita e chi riscatta non si legge più dal gioco (SU-994)2026-09-15

Ivan in chat il 15/09, per l'informativa di SU-794: «Chiudere la lettura». Il legame di SU-993 si leggeva senza accesso conoscendo i due codici, contro la scelta A del 14/09 (nessun registro degli inviti leggibile senza autenticazione). Lotto Codex L994 (Sol).

  • tools/firestore/firestore.rules: legami con get negato come list; create invariata, il divieto a vicenda resta nelle regole (existsAfter sul legame inverso). Compilazione con l'API :test: 0 errori.
  • files/homeless_city/scripts/autoload/Inviti.gd: via il GET preventivo del legame; una create rifiutata dalle regole (403 / PERMISSION_DENIED) dà l'esito nuovo rifiutato invece di «codice inesistente». 409 resta «già aperto», codice malformato «inesistente», il proprio codice «specchio»; gli update (mark_run_finished) non cambiano.
  • files/homeless_city/scripts/ui/MainMenu.gd e files/homeless_city/assets/translations/ui_menu.csv: ACCOUNT_INVITI_ESITO_RIFIUTATO al posto di ACCOUNT_INVITI_ESITO_A_VICENDA, 8 lingue: «Questo regalo non si apre: di solito è un invito a vicenda.» Una riga in tutte le lingue (misura: francese 418/564 px a 844x390, 578/685 px a 1024x768).
  • ✅ Prove: files/homeless_city/scripts/tools/probe_su993_riscatto.gd 44/44, files/homeless_city/scripts/tools/probe_su981_inviti.gd OK (0 letture dal client), compile-check verde, rilanciati dall'orchestratore dopo il reimport. tools/firestore/prova_inviti_regole_su981.sh ora attende 403 sulla lettura del legame.
  • ⚠️ NON provato: pubblicazione delle regole (Ivan) e prova dal vivo in stage; telefono, e lo scatto del messaggio.

Link d'invito su iPhone: il codice arriva anche col gioco chiuso (SU-992)2026-09-15

Ivan in chat il 15/09 sull'iPhone: «se ho l'app installata apre il gioco ma non ho l'invito precompilato», poi «se il gioco è aperto funziona, mentre se parte da chiuso no […] attendere che prema sul titolo e una volta fatto quello portarmi al menu invita un amico col codice».

  • 🔧 Causa misurata con righe di log sul telefono: il +load del plugin (tools/ios/su_native_auth/su_native_auth.m) arriva prima di quello di GDTApplicationDelegate, che crea la lista dei servizi; il ritentativo in coda al main thread arrivava 0,24 s dopo, quando iOS aveva già consegnato il link nella connessione della scena. Ora il servizio si registra alla notifica UIApplicationDidFinishLaunchingNotification, che UIKit manda dopo i +load e prima di connettere la scena. Log dell'avvio a freddo: registrato alle 06.577, scene:willConnect con 1 attività web alle 06.588, codice preso dal menu dopo il titolo alle 09.912.
  • 🔧 files/homeless_city/scripts/ui/MainMenu.gd: nel menu il codice si chiede anche ogni secondo, perché iOS può consegnare il link dopo le notifiche di ritorno in primo piano (costo: una chiamata nativa che restituisce una stringa vuota).
  • ✅ iPhone vero (Ivan): link da WhatsApp col gioco aperto → schermata dell'invito col codice; col gioco chiuso → titolo, poi schermata dell'invito col codice. Il delegate di scena è SwiftUI.AppSceneDelegate, ma i callback arrivano a Godot e al nostro servizio.
  • 📌 Righe di diagnosi [SU-992] ancora nel plugin (NSLog e ultime 40 righe in NSUserDefaults, rilette al lancio dopo): da togliere prima della release.

Invita un amico: messaggio col link del sito, e il link apre il gioco col codice già nel campo (SU-992)2026-09-15

Ivan in chat il 15/09: il messaggio «deve essere nella lingua attuale del giocatore… Ci vediamo su Street University! Scarica il gioco dallo store (link ad una pagina sul mio sito…)», e «fai anche il tasto che apre l'app o il link se lo mando su whatsapp». Lotto Codex N992 (Astra), review R992 (Sol).

  • ✅ Messaggio di CONDIVIDI (files/homeless_city/assets/translations/ui_menu.csv, 8 lingue): «Ci vediamo su Street University! Scarica il gioco dallo store: <link>» e a capo «Il mio codice regalo: <codice>», con link https://www.streetuniversitygame.com/invito/?c=<codice> (files/homeless_city/scripts/ui/MainMenu.gd, costante INVITE_LINK_BASE). COPIA copia ancora il solo codice.
  • ✅ Sito (../street-university-site, pubblicato): pagina invito/ con codice e COPIA, tasto «Ho già il gioco: apri», badge ufficiali di App Store e Google Play in 8 lingue (link degli store in due costanti da cambiare all'uscita), .well-known/apple-app-site-association e assetlinks.json (per ora solo la nostra chiave di release: quella di firma di Play si aggiunge all'uscita), .nojekyll.
  • ✅ Android (tools/android/plugin_su_auth/java/com/divano/streetuniversity/auth/): SUAuthRedirectActivity smista streetuni://invito e https://www.streetuniversitygame.com/invito in una coda del plugin (take_invite_code) e riporta davanti il gioco; i redirect di login restano identici. tools/android/installa_template_android.sh: filtro https con autoVerify, con un passo a parte (3g-bis) perché il blocco dell'activity si salta per marcatore sui template già patchati e il filtro non entrava nell'APK.
  • ✅ iOS (tools/ios/su_native_auth/): servizio registrato all'avvio su GDTApplicationDelegate (addService:; in Godot 4.7.2 il delegate implementa e inoltra scene:openURLContexts:, scene:continueUserActivity:, scene:willConnectToSession:options:, verificato con nm su libgodot.a), codice in coda per take_invite_code; entitlement applinks:www.streetuniversitygame.com nel preset iOS (files/homeless_city/export_presets.cfg). App ID con Associated Domains e profili AdHoc e Distribution rigenerati via API.
  • files/homeless_city/scripts/autoload/NativeAuth.gd take_invite_code(); MainMenu.gd _su992_check_invite_link(): nel menu (anche al ritorno in primo piano e dopo il titolo) apre INVITA UN AMICO col codice nel campo e RISCATTA selezionato, senza riscattare; in partita, tutorial o lobby il codice aspetta.
  • ✅ Prove: sonda files/homeless_city/scripts/tools/probe_su992_link.gd 18/18; javac e build Apple verdi; compile-check verde. Honor vero: streetuni://invito con l'app aperta e link https con l'app chiusa → schermata dell'invito col codice, 0 SCRIPT ERROR (TMP/su992/n992_honor/). Apple serve già il file di verifica dalla sua CDN.
  • ⚠️ NON provato: link da WhatsApp sull'iPhone (lo prova Ivan). Sull'Honor il link https chiede «Street University o Chrome» perché all'installazione il telefono era offline e la verifica di Android non è passata: va reinstallato con la rete.
  • 🔧 Review R992: un link arrivato durante un riscatto in corso restava nel plugin fino a un'altra navigazione; ora si ricontrolla a riscatto finito. Gli altri due rilievi (callback di scena assenti, link a freddo perso) valevano per Godot 4.6.2, non per la 4.7.2 esportata.

Invita un amico: una lattina d'oro anche a chi riscatta, e niente inviti a vicenda (SU-993)2026-09-15

Ivan in chat il 15/09: «se invito un amico, nel momento in cui questo riscatta il codice, otteniamo entrambi una lattina a testa, 10 lattine vanno benissimo, ma […] non ci si può invitare a vicenda», e «puoi riscattare un solo codice per utente, ma puoi inviare il tuo molte volte». Lotti Codex L993 e L993b (Sol), review R993 (Sol).

  • BETATESTING/apps_script_registro_nickname.gs (fuori da git): conteggio_inviti legge anche il riscatto dell'account verificato e risponde riscatto_completato (vero solo con la partita finita); cache per account invariata. Test tools/firestore/prova_release_verificato.mjs 67/67. Deploy da fare (Ivan): compatibile con le build vecchie.
  • files/homeless_city/scripts/autoload/Inviti.gd: lattine = min(inviti conclusi + 1 se il proprio riscatto è concluso, 10). A ogni riscatto si scrive nella stessa commit anche inviti/<env>/legami/<codice di chi invita>_<codice di chi riscatta>; prima di scrivere si chiede il legame inverso e solo se esiste (HTTP 200) l'esito è «a vicenda». Dopo la fine partita il menu chiede subito il conteggio, e se il server risponde ancora dalla cache ritenta una volta dopo 65 s.
  • tools/firestore/firestore.rules: il riscatto richiede il suo legame nella stessa commit e l'assenza del legame inverso anche dopo la commit (existsAfter, così due riscatti incrociati nella stessa commit non passano); legami create-only, get sì e list no; impronte solo esadecimale minuscolo (con le maiuscole lo stesso account riscattava più codici). Compilazione con l'API :test di Firebase: 0 errori. Da pubblicare (Ivan) SOLO insieme alla build nuova: un'app vecchia non scrive il legame e il suo riscatto verrebbe rifiutato.
  • files/homeless_city/scripts/ui/MainMenu.gd (schermata invito, ascolto della fine partita) e files/homeless_city/assets/translations/ui_menu.csv: nuovo esito ACCOUNT_INVITI_ESITO_A_VICENDA e ACCOUNT_INVITI_ESITO_OK riscritto, 8 lingue.
  • ✅ Prove: sonda files/homeless_city/scripts/tools/probe_su993_riscatto.gd 22/22 (formula, pre-controllo 200/404/403/timeout, retry oltre la cache); compile-check verde; prova live delle regole estesa in tools/firestore/prova_inviti_regole_su981.sh (riscatto a vicenda, commit incrociata, impronta maiuscola: tutti rifiutati), da lanciare dopo la pubblicazione.
  • ⚠️ NON provato: regole e server dal vivo, telefoni. I riscatti fatti prima non hanno legame: per loro il divieto a vicenda non vale (nessuna migrazione).

Invita un amico: CONDIVIDI apre il pannello di condivisione del telefono (SU-992)2026-09-15

Ivan in chat il 15/09, sull'iPhone con ff4b0db: «la condivisione del codice dal tasto condividi? non funziona». Causa: _account_inviti_share chiedeva OS.has_method("share_text"), che in Godot 4.7 non esiste, quindi da SU-792 CONDIVIDI finiva sempre nel ripiego (messaggio negli appunti + «Condivisione non disponibile»). Lotto Codex S992 (Sol), gate G992 (Luna).

  • tools/ios/su_native_auth/ (su_native_auth.m, su_native_auth.h, gdext_glue.c): metodo share_text(text) -> bool nella GDExtension Apple; su iOS presenta UIActivityViewController dal view controller in cima (popover centrato su iPad), su macOS risponde false. Binari rigenerati con tools/ios/build_su_native_auth.sh (iOS, simulatore, macOS).
  • tools/android/plugin_su_auth/java/com/divano/streetuniversity/auth/SUNativeAuthPlugin.java: share_text con Intent.ACTION_SEND text/plain e chooser sul thread UI, nessun permesso nuovo. Entra nella build con tools/android/installa_template_android.sh --solo-patch (da rilanciare prima dell'export: tools/release/export_all.sh non ricopia i sorgenti del plugin).
  • files/homeless_city/scripts/autoload/NativeAuth.gd: share_text() pubblico e capacità additiva share; le chiavi di login non cambiano. files/homeless_city/scripts/ui/MainMenu.gd: CONDIVIDI passa da NativeAuth, e dove torna false (desktop, web) resta il ripiego agli appunti.
  • ✅ Prove: sonda files/homeless_city/scripts/tools/probe_su992_condividi.gd sul Mac 11/11 (false senza errori, capacità di login invariate, avviso condividi_copia); compile-check verde. Honor vero (release, dev): CONDIVIDI apre «Condividi con» col messaggio e il codice, tornando indietro l'avviso «Pronto da condividere», 0 SCRIPT ERROR (TMP/su992/honor/). Build installata anche sull'iPhone.
  • ⚠️ NON provato da noi: il pannello sull'iPhone (lo prova Ivan), login Apple/Google end-to-end dopo la ricostruzione del plugin.

Il riccone della Collina spento per la release, da ripensare (SU-814)2026-09-15

Ivan in chat il 15/09: «disattiva temporaneamente il riccone in collina, così rilascio senza quel personaggio e penso a come implementarlo bene».

  • files/homeless_city/scripts/autoload/Quartieri.gd (COLLINA): flag "riccone": false, con la nota per riaccenderlo. È il solo interruttore: World._setup_riccone_collina() non costruisce il piano degli ingressi e il monociclo non entra mai. Codice, sprite e suoni restano; la voce «Riccone» del pannello F1 lo fa ancora entrare. I fenicotteri non cambiano. Nessuna RPC toccata.
  • ✅ Sonda files/homeless_city/scripts/tools/probe_riccone_spento.gd sulla città vera alla COLLINA: 7/7 (piano vuoto, nessun riccone dopo 25 s, 9 fenicotteri, F1 funziona). Controprova col flag riacceso: 4 controlli bocciati, 256 ingressi in programma. Compile-check di Quartieri.gd e World.gd verde.
  • ⚠️ NON provato: multigiocatore a due peer, telefoni. files/homeless_city/scripts/tools/probe_su814_debugbtn.gd si aspetta il flag acceso alla COLLINA e ora lo segnala.

INVITA UN AMICO nel menu principale: sottomenu con invito e negozio, lattine d'oro controllate da sole (SU-982)2026-09-15

Ivan in chat il 15/09, dopo aver provato la catena degli inviti: «averlo nel menu dell'account è troppo nascosto… lo metterei nel menu principale… un sottomenu con invita un amico, la parte "solo su invito"… e deve controllare automaticamente se ci sono nuove lattine d'oro». Lotto L982 (dev-sonnet, tre giri), review R982 e R982b (Sol).

  • files/homeless_city/scripts/ui/MainMenu.gd: voce INVITA UN AMICO subito dopo MULTIGIOCATORE; sottomenu invita_home (stile di MP_HOME, SU-969) con INVITA UN AMICO e SOLO SU INVITO; la voce esce da ACCOUNT; il negozio esclusivo torna alla schermata da cui è stato aperto (il cartello nella scelta del personaggio resta). Nella schermata dell'invito i numeri noti si vedono subito con «(aggiornamento…)».
  • ✅ Lattine d'oro da sole: tornando al menu con account connesso, il conteggio si chiede in sottofondo al massimo ogni 5 minuti; se ci sono premiabili nuovi la voce mostra «+N», che sparisce aprendo il sottomenu. Stato per account in user://inviti.cfg (files/homeless_city/scripts/autoload/Inviti.gd).
  • 🔧 Review, tre giri: un cambio d'account durante la richiesta poteva accreditare le lattine di un account a un altro, e la richiesta proseguiva in partita. Cura strutturale: il PUID passa esplicito lungo tutta la catena (count_completed(puid), token chiesto a EOS per quel PUID, accredito solo a quel PUID), un conteggio in volo per PUID, controllo in sottofondo spento con sessione multigiocatore, partita, tutorial o schermate diverse dal menu. Nuovo tentativo quando EOS diventa pronto all'avvio a freddo; segnale ricalcolato a login, logout e trasloco.
  • ✅ Server (BETATESTING/apps_script_registro_nickname.gs, fuori da git): cache del conteggio da 300 a 60 s e niente più lucchetto globale nel percorso di conteggio_inviti (serializzava tutte le richieste di tutti i giocatori). Test Node tools/firestore/prova_release_verificato.mjs 64/64. Deploy da fare: TMP/su982/passi_ivan.md.
  • ✅ Prove: sonda files/homeless_city/scripts/tools/probe_su982_badge.gd 26/26 (sequenza A→B→C→B, token vuoto, lobby MP, EOS pronto dopo, login/logout); provino files/homeless_city/scripts/tools/shot_su982_inviti.gd a 844x390 e 1024x768; Honor vero (release, dev): menu, sottomenu, invito, negozio e ritorno, ACCOUNT senza la voce, [SU-981] conteggio ok nel log, 0 SCRIPT ERROR (TMP/su982/honor/). 2 chiavi nuove in 8 lingue.

Inviti: righe di log del conteggio, e il KO sul telefono era la cache di 5 minuti (SU-981)2026-09-15

KO di Ivan il 15/09 (build df417e9, dev): l'iPhone inserisce il codice dell'Honor e finisce una partita, l'Honor apre INVITA UN AMICO quasi subito e non vede niente. Su Firestore il riscatto c'era, completo (run_finita=true). Riaprendo dopo: conteggio e lattina d'oro arrivati, e la lattina spesa. Causa: il server tiene il totale in cache 300 s, e l'Honor l'aveva chiesto prima che il riscatto dell'iPhone risultasse finito. Lotto Codex C981c (Sol).

  • files/homeless_city/scripts/autoload/Inviti.gd, NicknameRegistry.gd, files/homeless_city/scripts/ui/MainMenu.gd (_account_inviti_fetch_count): una riga [SU-981] per ogni esito di riscatto, fine partita e conteggio (sessione non viva, token EOS vuoto, trasporto con HTTP del primo salto e del redirect, ok totale=n), senza token, PUID o codici interi. Nessun cambio di comportamento; tools/autotest/probe_su981.sh VERDE.
  • 📌 Misurato dal Mac sull'Apps Script distribuito: la POST impiega 1,4-8,7 s (una volta 120 s), il redirect di Google 0,5-15 s e a volte risponde con una pagina d'errore. È lì che vanno i ~10 s che Ivan vede.

Metro infuocata dal giro 3: tutti i treni, sui binari e in banchina, col faro ambra (SU-979)2026-09-14

Ivan in chat il 14/09: «953 scelgo B e vale per tutti i treni che ci sono in metro dal giro 3 in poi, sia binari che banchina»; livrea approvata in SU-978 («978 ok»). Lotto L979 (dev-sonnet), review R979 (Sol).

  • files/homeless_city/scripts/world/BonusLevel.gd:136-172: TEX_TRAIN_FIRE, soglia TRENO_INFUOCATO_DAL_GIRO = 3, FARO_AMBRA; _train_texture() e _faro_colore() usate nei 4 punti che assegnavano TEX_TRAIN (convogli del corridoio, metro della banchina, ante sx e dx) e nei 2 fari. La scelta segue la variabile giro, la stessa di topi e zombie: in multigiocatore host e client concordano senza RPC nuove.
  • ✅ Asset files/homeless_city/assets/sprites/world/subway_train_fire.png (identico byte per byte a quello approvato) con .import uguale a subway_train.png. Sorgenti fuori da temp: sprites_raw/WORLD/su978/ (raw di Astra del mockup, ridotta, base intermedia, definitiva); path aggiornati in tools/sprite/su978_infuocato.py e su978_ante_fisse.py.
  • ✅ Provino files/homeless_city/scripts/tools/shot_su979_metro.gd + tools/autotest/shot_su979_metro.sh: giro 2 prima e dopo, 0 pixel diversi su 329.160 (844x390) e 786.432 (1024x768); giro 3 con convoglio del corridoio e faro ambra, metro in banchina che apre le porte (TMP/su979/).
  • ✅ Sonda files/homeless_city/scripts/tools/probe_su979_livrea.gd + tools/autotest/probe_su979_livrea.sh, giri 1-5: texture e faro giusti per giro, velocità 850 invariata, 0 controlli falliti.
  • ⚠️ Review sulla sonda, non corretta: la cadenza dei treni si stampa ma non si verifica; se il runner muore nel corridoio la banchina non viene controllata e l'esito resta verde; un'anta con texture nulla non verrebbe notata; il faro mancante nel corridoio non fa fallire. NON provato: multigiocatore a due peer vero, telefoni.

Metro infuocata: la carrozza definitiva da approvare, col colore del mockup B e le porte dell'originale (SU-978)2026-09-14

Ivan in chat il 14/09 ha scelto la livrea B di SU-953 per tutti i treni dal giro 3. Si genera e si presenta; il montaggio è SU-979, bloccato da questo. Lotti G978 (dev-sonnet, tre giri) e C978d (Codex Sol), confronti con Astra Q978Q978h.

  • 🔧 Due strade scartate lungo il giro: una ricolorazione deterministica dell'originale (geometria perfetta, ma marrone e rumorosa, niente a che vedere col mockup) e una colonna scura forzata sopra le porte del mockup (il controllo passava, ma a ×5 c'erano doppi contorni e le ante ritagliate dal gioco contenevano parete e brace).
  • ✅ Catena finale: tools/sprite/su978_infuocato.py (colore del mockup B esteso sull'alfa dell'originale) → tools/sprite/su978_ante_fisse.py (nei rettangoli delle ante, ricavati da BonusLevel.gd PORTE_X e fascia delle ante, i pixel dell'originale ricolorati con la tavolozza delle porte del mockup; pulizia dei residui accanto) → tools/sprite/su978_ritocco_vetri.py (tre vetri stretti di 1-2 px riallineati al gemello, correzione indicata da Astra).
  • ✅ Misure: 0 pixel di maschera alfa diversi dall'originale; montanti rilevati nell'arte esattamente in PORTE_X per le 6 porte, nessuna corsa scura in più entro ±5 px; 0 pixel di brace nelle 12 ante ritagliate come in gioco; 0 magenta; vetri 4/4 e 5/5 come le coppie dell'originale. Astra (Q978h): difetti precedenti tutti risolti.
  • ✅ Scheda di approvazione TMP/su978/scheda_approvazione_su978.png: oggi, mockup B, definitiva, e scena vera al giro 3 in frenata e a porte aperte (provino files/homeless_city/scripts/tools/shot_su978_treni.gd + tools/autotest/shot_su978_treni.sh, che cambia la texture a runtime cercando gli sprite per texture). Controlli: tools/sprite/su978_controllo_geometria.py, su978_ante.py, su978_zoom_porte.py, su978_scheda.py.
  • 📌 Le immagini stanno in sprites_raw/WORLD/temp/su978/ (raw di Astra del mockup, ridotta, base intermedia, definitiva subway_train_fire.png): la cartella temp è fuori da git finché Ivan non approva.

Inviti: il conteggio lo fa il server e i riscatti non si leggono più dal client (SU-981, da pubblicare)2026-09-14

Ivan in chat il 14/09 su SU-794: «Scelgo A». La revisione dell'informativa 1.4 aveva trovato i registri dei riscatti (impronta SHA-256 del PUID, codice, due date, stato) leggibili da chiunque, perché il conteggio faceva una runAggregationQuery dal client senza Firebase Auth. Lotti Codex C981 e C981b (Sol), review di sicurezza R981.

  • tools/firestore/firestore.rules: nessuna lettura dei riscatti dal client (get, list, aggregazioni, collection group). Creazione una sola volta e run_finita falso → vero invariati. Validate (pubblica_regole.sh --solo-valida, ruleset ac0d233b), NON pubblicate.
  • files/homeless_city/scripts/autoload/Inviti.gd count_completed(): chiede il conteggio all'azione conteggio_inviti dell'Apps Script del registro nickname, col canale di NicknameRegistry.gd e l'ID token Connect esposto da EOSBridge.gd (niente log né file). Riscatto e fine partita fanno solo commit con precondizione, nessuna lettura. Errori: stesso ramo muto di prima, il premio non cambia.
  • ✅ Server (BETATESTING/apps_script_registro_nickname.gs, fuori da git): il PUID viene solo dal token verificato (firma RS256 con le chiavi Epic, iss esatto, tokenType idToken, client, prodotto, deployment dell'ambiente, scadenza), il codice con la stessa derivazione del client. Risponde solo {ok, totale}; ogni errore è non disponibile. Cache 300 s, 30 chiamate l'ora per PUID, 5.000 al giorno. Gli stessi controlli sul token valgono ora anche per release_verificato di SU-917.
  • ✅ Prove: sonda files/homeless_city/scripts/tools/probe_su981_inviti.gd (5 casi, 0 letture Firestore dal conteggio), 5 vettori di codice identici fra GDScript e Node, tools/firestore/prova_release_verificato.mjs 64/64 con claim contraffatti. Un ID token Connect vero di dev passa la verifica nuova e viene rifiutato con il deployment di un altro ambiente. tools/firestore/prova_inviti_regole_su981.sh pronta per dopo la pubblicazione.
  • ⚠️ Da fare da Ivan, in quest'ordine (TMP/su981/passi_ivan.md): deploy del server sullo stesso deployment, build nuova ai tester, poi pubblicazione delle regole. Le build vecchie dopo le regole perdono il conteggio remoto e tengono le lattine d'oro locali. Restano fuori: i riscatti si possono ancora creare in forma anonima (disegno di SU-791) e il codice a 32 bit può collidere.

Carte: statistiche di TROMBETTA e BRAVO RAGAZZO su due righe, testo mai sopra i pallini del livello (SU-980)2026-09-14

Segnalato da Ivan in chat il 14/09 con uno screenshot: VIRTUOSO DELLA TROMBETTA liv. 2 → 3 mostrava «resa +43%raggio +36%» e «+54%» sopra i pallini. Lotto L980 (dev-sonnet, tre giri), review R980 (Sol).

  • 🔧 Causa del testo attaccato: il separatore «·» di CARTA_EFF_TROMBETTA e CARTA_EFF_BRAVO. Il font lo ha (has_char vero), ma a corpo 13 non accende nemmeno un pixel. L'ipotesi del ticket («manca dal font») era sbagliata.
  • files/homeless_city/assets/translations/ui_meta.csv:31,37 e ripieghi in files/homeless_city/scripts/systems/PowerUpSystem.gd: il formato ora non ripete i nomi e va a capo fra le statistiche, «resa %s → %s\nraggio %s → %s» (e «wanted %s → %s\nrep %s → %s»), in tutte e 8 le lingue, con gli argomenti riordinati [attuale1, prossimo1, attuale2, prossimo2].
  • files/homeless_city/scripts/ui/LevelUpScreen.gd _fit_effect_font_size(): il testo dell'effetto si misura con le righe vere e interlinea compresa (get_multiline_string_size da solo sottostimava: 45 contro 63 px su 3 righe). Se non entra prima dei pallini si riduce il corpo del solo effetto, mai sotto il 70%.
  • ✅ Sonda files/homeless_city/scripts/tools/probe_su980_testi.gd su 360 combinazioni carta × livello × lingua: sovrapposizioni 57 → 0, caratteri che non si vedono 0. Scatti TMP/su980/ col provino files/homeless_city/scripts/tools/shot_su980_carte.gd, prima e dopo.
  • ⚠️ Il corpo ridotto resta in 75 combinazioni su 360 (TROMBETTA in 6 lingue, BRAVO RAGAZZO in it/en/de e alcune altre carte): nome su due righe + livello + due righe d'effetto non stanno nella carta a corpo pieno. Per il corpo pieno servirebbe più spazio verticale nella carta, non toccato. «wanted» resta anche in italiano, com'era.

Camioncini: le ombre disegnate da Ivan, gelataio e hot dog, alle tre fasce (SU-938, parte B)2026-09-14

Ivan in chat il 14/09: «per le ombre te le ho messe solo nel camioncino dei gelati, applica le stesse anche a quello per gli hot dog sulla falsa riga». Aveva disegnato sui tre modelli sprites_raw/ombre/modelli/van_gelataio_*.png il CONTORNO rosso dell'ombra, non la forma piena.

  • tools/ombre_da_scarabocchio.py: oltre al rosso pieno riempie l'interno dei contorni chiusi (tutto ciò che dal bordo non si raggiunge senza attraversare il rosso). Prima teneva solo il rosso e dai contorni di Ivan sarebbe uscito un anello sottile. Nuovo --trasferisci <da> <a>: mette lo stesso rosso sul modello dell'altro camioncino, centrato sul centro del mezzo (i due hanno lo stesso fotogramma 120x84; alla sera i canvas differiscono di 8 px per lato).
  • ✅ Ombre in files/homeless_city/assets/sprites/ombre/: van_gelataio_{mattina,mezzogiorno,sera}.png dai disegni di Ivan e van_hotdog_{mattina,mezzogiorno,sera}.png con le stesse forme. Mattina 3.376 pixel d'ombra, mezzogiorno 1.654, sera 5.263. Sorgenti in sprites_raw/ombre/ (i disegni di Ivan così com'erano); i modelli in sprites_raw/ombre/modelli/ sono tornati puliti.
  • ✅ Provino tools/autotest/shot_su938_ombre.sh assente: camioncino hot dog alle 8, 12 e 17 con e senza ombra (TMP/su938b/van_tre_fasce_intero.png). Alle 8 la macchia scende a destra sotto il mezzo, a mezzogiorno resta quasi tutta sotto la carrozzeria, alle 17 sale dietro il mezzo verso destra.
  • files/homeless_city/assets/data/ombre_correzioni.json: due correzioni salvate da Ivan con SPOSTA OMBRA a mezzogiorno — fontanile y -18, forziere (bin_closed.png) y -2.

Brina: il materiale sotto i piedi si chiede a un indice, non a tutti gli isolati a ogni tick (SU-977; sull'Honor non sposta niente, e la voce non costava)2026-09-14

Da SU-957: spegnendo marciapiedi_ghiaccio l'Honor guadagnava +4,6% fps / +7,0% di 1% low, in UNA prova per voce contro un rumore di ±2,4% / ±3,8%. Lotti Codex C977 e C977b (Sol), review R977, collaudo K977 (collaudatore).

  • ✅ Causa: files/homeless_city/scripts/player/Player.gd:2033-2079 chiede il ghiaccio da _surface_velocity() a ogni tick di fisica, e terreno_materiale_at() (files/homeless_city/scripts/world/WorldGenerator.gd:1333) scorreva piazze, parchi e tutti i footprint ricostruendo un Rect2 per ciascuno. Seme 9570912: 43 footprint, 22 scansionati in media per domanda.
  • ✅ Cura: _build_terrain_material_index() costruisce una volta per generazione un indice a celle da 192 px con i Rect2 esatti delle stesse regioni, per i due rami geometrici (tilemap / piastre), e terrain_material_at_indexed() guarda solo la cella. Sul Mac 11,8 → 1,16 µs a chiamata (100.000 chiamate). Disegno e RNG non toccati.
  • ✅ Sonda files/homeless_city/scripts/tools/probe_su977_terreno.gd + tools/autotest/probe_su977.sh: vecchia e nuova confrontate su griglia a 4 px più i bordi, 5 semi di Brina e un seme per ciascuno degli altri 7 quartieri, entrambi i rami: 4.042.376 punti, 0 differenze. Dopo la review un fallimento esce 1 davvero e le righe DIFF si fermano a 20 (controprove forzate).
  • --su977-vecchio (solo build di debug, via SU957Args) rimette la strada vecchia per la controprova con un solo APK. Harness tools/autotest/su977_honor.sh: tre bracci (cura / vecchio / voce spenta) in ordine ABCCBA × 2, porta termica a 35,0 °C prima di ogni prova, timeout 150 s. Provino files/homeless_city/scripts/tools/shot_su977_brina.gd: Brina cura contro vecchio, 0 pixel diversi sul suolo (1.012 su 329.160 tutti in torce e neve animate).
  • ⚠️ Prima misura sull'Honor (K977) nulla: da 34 a 40 °C in 14 minuti, fps da 36,5 a 25,6 in entrambi i rami, e l'ordine metteva sempre la cura prima.
  • ✅ Seconda misura (K977b, harness termica, 12 prove in 99 minuti, n=4 per braccio, TMP/su977/honor_results.tsv): cura 36,17 fps / 21,21 di 1% low, vecchio 35,63 / 20,45, voce spenta con la cura 35,28 / 19,00. Cura contro vecchio +1,5% / +3,7%, dentro le bande (fps 4-7%, 1% low 6-18%). Voce spenta contro cura −2,5% / −10,4%, segno opposto e dentro la banda. Il +4,6% / +7,0% di SU-957 non si ripete: veniva da una prova sola, e i marciapiedi di ghiaccio non costano fps misurabili sull'Honor. La cura resta (risposta identica, 10× meno lavoro sul Mac). 02_vecchio rifatta a mano (logcat chiuso senza riga); 03_spenta partita a 38 °C dopo 900 s di attesa.

Selezione del personaggio su iPhone: pallini dentro il pannello, «SOLO SU INVITO» diventa un cartello (SU-970)2026-09-14

Segnalato da Ivan il 13/09 con due screenshot dell'iPhone 14 Pro, allegati al ticket il 14/09 (TMP/su970/ivan/). Lotto L970 (dev-sonnet), review R970 (Sol), gate X970 (Luna).

  • files/homeless_city/scripts/ui/MainMenu.gd _skin_build_dots / nuova _skin_dots_visibili: con 17 personaggi (le due punk esclusive sbloccate) il minimo di slot_w vinceva e la fila usciva dal pannello. Ora sta sempre dentro con mezzo passo di margine per lato (a 852×393: margine 0 → 11,3), e quando i pallini non ci stanno diventa una finestra scorrevole centrata sul personaggio, con i due estremi più piccoli. _refresh_skin_visuals ricostruisce la finestra a ogni cambio.
  • ✅ «SOLO SU INVITO» era una Label piatta in coordinate di schermo, sopra il tetto: ora nasce da _attach_back_button() (parametri nuovi opzionali; le altre 25 chiamate restano identiche, verificato dalla review) come cartello simmetrico a INDIETRO, dentro la safe area, e apre lo stesso negozio.
  • ✅ Pannello con margini pari sopra e sotto la fila dei cartelli (852×393: 11,5 / 16,1 → 13,8 / 13,8) e bottone d'azione staccato dal bordo destro (0 → 18,8).
  • 🔧 Gate: i pallini ricostruiti restavano MOUSE_FILTER_IGNORE (non in keep_taps di _make_screen_passthrough), anche al primo ingresso: il tap su un pallino non faceva niente finché non si premeva una freccia. Ora ogni pallino porta la meta su722_back, che il passthrough già rispetta. Sonda col tap vero: IGNORE → STOP, _skin_idx cambia al primo ingresso e dopo apri/chiudi negozio.
  • ✅ Provino files/homeless_city/scripts/tools/shot_su970_skin.gd (iPhone 852×393 e 844×390 con safe area simulata, Honor 877×415, desktop 4:3 e 16:9, 8 lingue, tap sui pallini); safe area simulabile solo dai provini (_su970_safe_area_debug, sui telefoni veri resta DisplayServer.get_display_safe_area()). Scatti TMP/su970/prima/ e TMP/su970/dopo/. 1 riga in tutte le 8 lingue, prima e dopo.
  • 🔧 Sull'Honor vero (build di release) il provino non bastava: sul telefono menu_text_factor() ingrandisce i testi (×1,3) e i cartelli si misurano in pixel fisici, cosa che sul Mac non succede. Tre difetti visti solo lì, curati e riverificati sul telefono in italiano e francese (TMP/su970/honor_970f/):
    • «▸ LATTINE INSUFFICIENTI» usciva dal bordo destro del pannello: _skin_fit_confirm_label() ancora il testo al bordo destro e lo fa crescere a sinistra, riducendo il corpo solo se non basta (mai sotto il 60%). Sul telefono: 63 px dal bordo.
    • «SOLO SU INVITO» copriva l'etichetta «DEV / v0.43», che resta anche in release: il cartello si sposta a destra quanto basta, alla stessa altezza di INDIETRO.
    • Spostato a destra, il cartello stringeva il pannello (x 327-1073 → 462-937 nella copia a 1400 px) e la descrizione andava su due righe attaccata al bottone: il vincolo confrontava col margine di rispetto un pannello appena ricentrato. Ora conta solo la sovrapposizione vera. Pannello x 214-1185, 15 pallini visibili, descrizione su una riga, 37 px fra descrizione e bottone.
  • _skin_info_worst_lines(): riserva per la descrizione le righe della lingua più lunga (il cambio lingua a caldo non ricostruisce la schermata), misurate con Font.get_multiline_string_size che spezza per parole come l'autowrap (review R970b: il rapporto larghezza/scatola sottostimava, 2 righe contro 3 in francese). Prezzo: sul desktop 4:3 la vetrinetta perde ~29 px per la riga in più riservata al tedesco.
  • ⚠️ Aperto: i bersagli di tocco restano sotto i 44 pt sull'iPhone (pallini ~25, bottone d'azione ~27, cartelli ~21-30): è la formula comune a tutte le schermate del menu (BACK_BTN_MIN_TOUCH_PX in pixel fisici), non cambiata qui.

Treno speciale del bonus: tre mockup nella scena vera, fantasma, infuocato e scassato (SU-953, DESIGN)2026-09-14

Ivan sul ticket il 14/09: «io farei tutte e tre le opzioni come mockup per poi decidere, e lo metterei solo dal terzo giro e solo quando arriva in banchina per portare gli zombie, non durante i binari». Nessun file di gioco toccato: BonusLevel.gd e gli asset restano com'erano. Lotto M953 (dev-sonnet), confronto a quattro mani con Astra (Q953).

  • ✅ Scheda TMP/su953/scheda_su953.png: OGGI, A FANTASMA, B INFUOCATO, C SCASSATO, ciascuno in frenata e a porte aperte al giro 3 (scena vera 844×390), più la carrozza 504×109 a dimensione di gioco e ingrandita.
  • ✅ Provino files/homeless_city/scripts/tools/shot_su953_treni.gd: entra al giro 3, salta il corridoio, entra in banchina e cambia a runtime la texture della metro ferma (fiancata e ante), modulate 0,55 per il fantasma e faro ambra per l'infuocato. ⚠️ Spegne lo smoothing della camera: position_smoothing segue il tempo reale, e saltando avanti nel livello la camera restava all'inizio col treno fuori campo.
  • ✅ Generazione con Astra tools/sprite/su953_treni.sh, riduzione BOX tools/sprite/su953_riduci.py, controllo geometria tools/sprite/su953_controllo_geometria.py, scheda tools/sprite/su953_scheda.py. Raw e ridotte in TMP/su953/: sono mockup, non arte approvata, e non entrano in sprites_raw/.
  • ⚠️ Per la generazione definitiva la geometria va bloccata sull'originale: Astra ha misurato le porte fuori dalle colonne di PORTE_X fino a 5 px nel fantasma e nell'infuocato (sagoma diversa fino a 10 px), 1-2 px su una porta nello scassato, e residui di magenta ai bordi di tutte e tre. In gioco le ante si ritagliano da quelle colonne.
  • NON mostrati: movimento, suono (il silenzio del fantasma), velocità irregolare dello scassato.

Il tossico non deruba più i fantasmi (SU-966, correzione decisa da Ivan)2026-09-14

Ivan in chat il 14/09: «966 applica». La correzione proposta dal lotto A966 in SU-966 (commit 0253c8b) e lasciata fuori perché il criterio 4 chiedeva il comportamento di allora.

  • files/homeless_city/scripts/world/World.gd:4913-4918 (junkie_try_steal_puppets): il borseggio salta i giocatori fantasma. Prima rubava anche al client morto con l'host vivo (10 $ misurati). RPC cambiate: nessuna.
  • ✅ Sonda files/homeless_city/scripts/tools/probe_su966_nemici_mp.gd + tools/autotest/probe_su966_mp.sh: nel caso guest un secondo tossico va a 6 px dal fantasma del client, con l'host lontano 300 px. Dopo: 0 furti sull'host, 0 borseggi e soldi invariati (495 → 495) sul client. Controprova rossa col World.gd di prima montato nella sola copia di lavoro (SU966_WORLD=): 1 furto sull'host, 485 → 475 $ sul client. ciclo 59 OK / 0 falliti.
  • 🔧 Due giri a vuoto della sonda: con host e fantasma vicini il tossico derubava l'host e il codice di prima passava lo stesso; ora i furti si distinguono dal segnale di furto dell'host, non dalla distanza.

Salvataggio sulla nuvola: nessuna lettura EOS nuova dopo la scadenza d'uscita (SU-967, residuo corretto)2026-09-14

Ivan in chat il 14/09: «967 correggi». Il residuo dichiarato in SU-967 (commit 40815ac, review R967c). Lotto Codex C967e (Sol).

  • files/homeless_city/scripts/autoload/ProgressSync.gd:539,600: la scadenza d'uscita si ricontrolla prima di ogni nuova lettura, anche dopo l'attesa di una richiesta pendente e dopo il timer, e subito prima di preparare la read_file nativa. Dopo la scadenza non nasce nessuna richiesta.
  • ProgressSync.gd:339: quando sync_on_exit() scade, la scadenza d'uscita non viene più ripristinata, quindi resta valida per le coroutine ancora vive mentre EOS si smonta. Nelle uscite chiuse bene il ripristino resta com'era.
  • ✅ Sonda files/homeless_city/scripts/tools/probe_su967_pending.gd, 28/28 (26 casi di prima + 2 nuovi), rilanciata al gate. Trasporto che risponde sempre result=9 con la scadenza vicina: 1 lettura, 0 letture dopo la scadenza. Uscita senza callback: uscita in 5008 ms, scadenza che resta valida, la coroutine rientra in 0 ms. probe_su920_progress_sync.gd 113 VERDE; probe_su920_eos_vero.gd su EOS vero VERDE nei 5 modi.
  • 📌 Il caso vecchio «uscita dopo un salvataggio in corso» ora usa una lettura lenta di 4000 ms invece di 4600: stesse verifiche, più margine sotto i 5 s della scadenza, perché il primo giro della sonda era rosso per temporizzazione.
  • ⚠️ Nota per chi rilancia tools/autotest/probe_su967_c967d.sh: con una SU_TMP in /tmp la parte SU-920 si rifiuta di partire (la sua user:// deve stare in TMP/) e il runner esce 1. Col default del runner è verde.

Il timbro del controllore ha la sua arte: berretto, pinza, tessera e impronta montati (SU-782)2026-09-14

Ivan in chat il 14/09: «berretto A pinza A tessera A timbro A». Generazione del 13/09 (lotto Codex C782b su Astra, BLOCCATO in sandbox perché senza rete, lanciata dall'orchestratore con lo script del lotto). Montaggio inline dell'orchestratore.

  • ✅ Montate le varianti A nei path che il codice di be01a97 già cerca (files/homeless_city/scripts/autoload/Skins.gd:425,432,439,445): files/homeless_city/assets/ui/stamp_cap.png, stamp_puncher.png, stamp_pass.png (32×32) e stamp_mark.png (16×16), con i loro .import. Codice invariato.
  • ✅ Raw 1254×1254 e riduzioni BOX della variante A in sprites_raw/UI/su782/ (raw/, ridotte/); la B in sprites_raw/archivio_varianti_scartate/su782/. La cartella temp non c'è più. In LFS, aggiunti per cartella.
  • ✅ Provino files/homeless_city/scripts/tools/shot_su782_baracca.gd a 844×390, guardato: coi 15 timbri i tre riquadri della Baracca mostrano berretto, pinza e tessera, e l'impronta sta accanto al contatore. Scatti in TMP/su782/montaggio/.
  • 🔧 Nella generazione, due controlli troppo rigidi dello script del lotto: pretendeva 1024×1024 (il generatore dà 1254×1254) e scartava un soggetto che tocca il bordo invece di dargli un margine.
  • ⚠️ tools/autotest/probe_su782.sh cancellava tutta TMP/su782/ a ogni lancio (rm -rf "$OUT"): il 14/09 ha portato via la scheda di approvazione citata su Jira, i prompt e lo script di generazione. Ora cancella solo i suoi fase*.txt. I disegni erano già al sicuro in sprites_raw/.
  • 📌 A 32 px la riduzione rende i disegni morbidi più che pixel art netta; Ivan ha approvato così, senza chiedere la riduzione della palette.

Menu multigiocatore su telefono: voci da 44-48 pt e scritte grandi (SU-969)2026-09-13

Ivan in chat: «menu multigiocatore scritte troppo piccole, modificare layout per avere scritte grosse su mobile da cliccare col dito». Lotto Claude L969 (dev-sonnet), tre giri al gate, review R969.

  • files/homeless_city/scripts/ui/MainMenu.gd, solo le schermate MP. Si riusano la conversione pt→px di SU-976 (_window_px_per_pt) e il corpo del menu principale (OptionsPanel.opt_font_voci). Altezza della voce toccabile più bassa, in pt stimati sull'iPhone, a 844×390 / 1024×768: MP_HOME 26,0/51,1 → 48,0/76,8; UNISCITI lettera 40,6 → 48,0, CONNETTI 26,4 → 48,0; LISTA PARTITE 22,4/44,1 → 48,0/76,8 (righe e RIAGGIORNA); lobby 20,1/39,6 → 44,0/76,8. Nessuna chiave di traduzione nuova.
  • LISTA PARTITE: ogni partita su due righe dentro la stessa voce: host a 34 px, il corpo del menu principale; posti · GUARDA/PIENA · quartiere al 91% (844×390) e all'82% (1024×768), riallineati al nome. 2 partite per pagina invece di 5.
  • Lobby: nomi degli 8 giocatori, riga di stato e aiuto a 25 px (il 75% del corpo, pavimento dichiarato). Per far posto il titolo scende all'8,8% dell'altezza e gli spazi si stringono. L'aiuto sta dentro il pannello con 8,0-9,7 px di margine in 8 lingue × 2 profili; prima usciva di 97 px.
  • 🔧 Al gate, dagli scatti: nella prima versione il testo delle righe di LISTA PARTITE e i nomi della lobby erano rimasti piccoli dentro righe alte; l'aiuto della lobby usciva dal pannello; i tag degli slot (HOST/TU/PRONTO) e i visual del codice non si ritraducevano cambiando lingua a schermata aperta (lobby in tedesco con i tag in francese), ora richiamati da _ricostruisci_testi().
  • 🔧 Dalla review R969: col testo ingrandito al massimo (1,5×) RIAGGIORNA usciva di 41 px e PUBBLICA di 15, perché il pannello era limitato al 96% ma i figli no; ora nessuna voce toccabile fuori, con margine 0 px o più nelle 5 schermate. La frase dello spettatore (MENU_LOBBY_ENTRI_A_GUARDARE) si misura e, se non ci sta, si taglia (clip_text): peggiore il francese al 79% della riga. Nomi nuovi nel codice rinominati in inglese.
  • ✅ Provino files/homeless_city/scripts/tools/shot_su969_mp_menu.gd + tools/autotest/probe_su969_mp.sh tutti (prima col MainMenu di HEAD nella sola copia di lavoro, poi quello nuovo): prima 13 OK / 6 falliti, dopo 38 OK / 0. get_line_count() = 1 per voci, nomi e stato in 8 lingue. Scatti in TMP/su969/ (z_su969_browse_844x390_it_dopo.png, z_su969_lobby_1024x768_host_de_dopo.png).
  • ⚠️ Non misurati: iPhone e Honor veri (pt stimati con la scala dello schermo). Col testo al massimo alcune voci toccano il bordo con margine 0. L'aiuto della lobby nel ramo LAN senza EOS (solo harness, si gioca online) può essere tagliato.

Online, con l'host morto i nemici a mestiere continuano ad attaccare chi è vivo (SU-966)2026-09-13

Trovato durante SU-964; Ivan in chat: «964 voglio un ticket». Lotto Claude A966 (architetto-opus), review R966 («nessun problema concreto»).

  • Diagnosi: con l'host morto si spegnevano tutte e sei le guardie if not GameState.run_active di ModularNPC.gd (tossico, spazzino, gattara, giullare, rivale, ronda). L'IA gira solo sull'host, e run_active è la run dell'host.
  • Cura in files/homeless_city/scripts/npc/ModularNPC.gd: _match_running() (:959) vale run_active or clock_only, lo stesso criterio che SU-964 ha usato per il riccone, e sostituisce le sei guardie. _local_target() (:972) restituisce il barbone locale, oppure null se è un fantasma: senza, a guardie riaccese il tossico derubava il fantasma dell'host (1 borseggio, 490 → 480 $ misurati). Nel giullare è cambiata solo la riga della guardia, niente della logica di SU-975. RPC cambiate: nessuna.
  • Sonda files/homeless_city/scripts/tools/probe_su966_nemici_mp.gd (+ .tscn) e tools/autotest/probe_su966_mp.sh, a Fumarola (da SU-973 il tossico nasce solo lì e a Nottefonda), 54/54 OK. Attacchi nei 10 s, tossico/spazzino/gattara, prima → dopo la morte: host morto, codice di prima 3/2/8 → 0/0/0 con 0 colpi sul client; con la cura 2/1/8 → 2/2/8, il client perde 2 vite. Client morto: 3/2/8 → 3/2/9 in entrambi i casi. Tutti morti e singolo giocatore: 0/0/0, 0 px, in entrambi. Il fantasma dell'host resta a 500 → 500 $.
  • 📌 Altri punti col sintomo, non curati (fuori dai file del lotto o da misurare): DistrictSpawner.gd:477 _home_enemy_arch() non fa più comparire la gattara o lo spazzino di casa con l'host morto; World.gd:5150/5169 rifiuta il giullare chiesto dalla carta di un client vivo; World.gd:4115 drop_coin_reward, sospetto bottino all'host morto. Non colpiti perché guardano la run di chi riceve il colpo: DistrictFoe.gd:488, Cactus.gd:372, SunHunter.gd:158. Polizia, cani e piccioni non hanno la guardia e non sono misurati.
  • ⚠️ Difetto di oggi, non curato: World.gd:4906 junkie_try_steal_puppets deruba anche i fantasmi (1 borseggio da 10 $ sul client morto con l'host vivo, codice di prima). Il criterio 4 chiede di non cambiare quel caso: correzione proposta in TMP/su966/patch_World.diff.
  • ⚠️ Non provati: partite con 3 o più giocatori, i telefoni, e rivale, ronda e giullare (stessa guardia, non misurati).

PARTITA RAPIDA chiede prima di entrare in una partita già iniziata; velo di caricamento anche alla partenza in multigiocatore; le lobby EOS riavevano la versione vuota (SU-976, SU-971)2026-09-13

Ivan in chat il 13/09: la rapida non deve entrare da sola in una partita iniziata; «loading anche nel caricamento del multiplayer». Lotto Claude A976 (architetto-opus), review R976, gate K976.

SU-976 — la domanda della partita rapida

  • NetworkManager.gd: la scelta sta in una funzione pura, quick_match_decision (entra / chiedi / ospita). La ricerca LAN e quella EOS finiscono nello stesso _quick_match_resolve. Con sole partite già iniziate con posti liberi parte il segnale nuovo quick_match_question, e lo stato resta SEARCHING finché non arriva quick_match_answer (join / search / host / exit), numerato contro le ricerche annullate che si risvegliano dopo. cancel_search chiude anche la domanda. Senza UI collegata la rapida si ferma come con ESCI.
  • MainMenu.gd: schermata mp_quick_ask a 4 voci (ENTRA come ospite, CONTINUA A CERCARE, OSPITA UNA PARTITA, ESCI) con tocco, tastiera e joypad; indietro vale ESCI. Voci da 48 pt a 844×390 e da 76,8 pt a 1024×768, con la conversione pt→px _window_px_per_pt, che su iOS e Android usa screen_get_scale. 6 chiavi MENU_QUICK_ASK_* in 8 lingue in ui_menu.csv, ogni voce su una riga in tutte (la peggiore è il tedesco, al 90% della riga).
  • ✅ Il responder LAN risponde anche a partita iniziata (con in_game), perché la domanda nasca anche in LAN, e tace nel 3-2-1. Dalla review: quando da una partita piena esce un ospite si riaccende (_on_peer_disconnected, _force_drop_peer); prima la partita restava invisibile anche col posto libero.
  • ✅ Sonde tools/autotest/probe_su976_mp.sh + files/homeless_city/scripts/tools/probe_su976_rapida.gd: regola 13/13 (i 5 casi del ticket, più 8/8 che apre la propria e 7/8 che entra); due peer ENet 16/16 sulle quattro risposte; con la domanda a video, 4 s senza entrare né ospitare; piena 5/5 con controprova rossa. Regressione probe_su926_mp.sh regola 10/10 e lista 26/26: la LISTA PARTITE mostra già «IN CORSO 5/8 · GUARDA» e «IN CORSO 8/8 · PIENA» (SU-926). Scatti guardati al gate: TMP/su976/z_su976_domanda_844x390_de.png.
  • 🔧 Bug trovato dal lotto sull'Honor e corretto qui: EOSBridge._game_version() leggeva nm.GAME_VERSION, costante tolta dal commit 990f0ea (SU-874/SU-896). Su dev era SCRIPT ERROR e restituiva una stringa vuota: bucket della lobby streetuni- e filtri di ricerca senza versione. Ora chiama nm.game_version(); bucket e filtri passano da helper puri (lobby_bucket_id, search_public_filters, search_code_filters), verificati a streetuni-0.43 / 0.43 con controprova rossa. Non era in nessun tag. Corretto anche files/homeless_city/scripts/tools/shot_su426_lobby.gd:91.

SU-971 — il velo alla partenza

  • NetworkManager.gd:2635: a fine 3-2-1 LoadingVeil.go_to_scene(…, true) al posto di change_scene_to_file. È l'unico cambio scena MP, quindi copre la prima partenza, la partenza dopo il giornale (SU-876) e l'ingresso da spettatore (SU-926).
  • 📌 La domanda del ticket: il velo del singolo carica in un thread mentre la rete gira, quindi le RPC dell'host per World/… arriverebbero a un World che non c'è ancora. Per il MP (hold_network, LoadingVeil.gd:138) la rete si mette in pausa (multiplayer_poll=false) dal via a scena montata, con caricamento bloccante dopo un frame di velo. Il caricamento in thread è stato misurato e scartato: +181…+225 ms e uno stallo a 3990 ms.
  • ✅ Host e client, prima e seconda partenza, spettatore: 0 frame grigi e 0 neri dopo la comparsa del velo; prima c'era 1 frame grigio. Dal via al primo frame giocabile (media di 2 giri): host 2187 → 2184 e 687 → 709 ms, client 1830 → 1855 e 349 → 386 ms. 0 errori di rete alle partenze, posizioni di partenza identiche sui due peer. Singolo invariato (probe_su544_velo: 2394/2240/2351 contro 2380/2368/2328 ms di HEAD). Sonde tools/autotest/probe_su971_mp.sh + files/homeless_city/scripts/tools/probe_su971_velo_mp.gd, numeri in TMP/su971/tempi.txt.
  • ⚠️ Spettatore: 1806 → 2100 ms (+0,29 s), sopra il tetto di 0,2 s del ticket. Sono le RPC accumulate durante il caricamento, che ora arrivano al World invece di andare perse. L'alternativa misurata (rete ripresa a velo tolto) dà il primo frame a ~1840 ms ma poi la città resta ferma 329 ms. Scelta del builder: velo un attimo più lungo, poi fluido. Decide Ivan.

Review R976 — rilievi scartati, con il perché

  • «Critico» multiplayer_poll=false durante il caricamento come pausa globale: prima change_scene_to_file caricava in modo bloccante sul thread principale, quindi la rete non veniva letta comunque; ora la pausa dura lo stesso più un frame (misurato +1…+37 ms), non è get_tree().paused e la rete riparte anche in _exit_tree.
  • Due «Alto» sulla porta discovery fissa 7778 e su join_game() con DEFAULT_PORT: valgono solo con --mp-port, che esiste per la sola sonda; sui telefoni ogni host usa la porta di default. ⚠️ Effetto collaterale per le sonde: due sonde LAN in parallelo sullo stesso Mac ora si contendono la 7778.
  • ⚠️ Non provati: la ricerca e l'ingresso su EOS vero, e la domanda su iPhone e Android (su questo Mac screen_get_scale vale 1). Il corpo della domanda sul telefono è piccolo (~9 pt): non è una voce da toccare. RPC cambiate: nessuna (un segnale nuovo e un campo nel pacchetto UDP di scoperta LAN).

Honor a Brina: il costo lo fanno i marciapiedi di ghiaccio, misurato sul telefono (SU-957)2026-09-13

Ivan in chat il 13/09: «fallo poi te sull'honor da solo alla prossima sprint». Primo giro (6286fb8): lo strumento e una lettura sul Mac che non separava le voci. Qui: lotto Claude H1 (collaudatore) sull'Honor 10, lotto Codex C957 per far arrivare gli argomenti al gioco, review R957.

  • 🔧 Il primo tentativo sull'Honor è fallito per l'ambiente, 0 prove su 18: su Android gli argomenti passati con am start (--es command_line, e anche --esa command_line_params, la chiave trovata nel dex) non arrivano mai a OS.get_cmdline_user_args(); GodotActivity logga with parameters []. È la stessa causa di --rec, già documentata in tools/autotest/README.md.
  • Rimedio, solo nelle build di debug: files/homeless_city/scripts/tools/SU957Args.gd somma agli argomenti da riga di comando quelli del file user://su957_args.txt, e lo leggono TestBot.gd, Quartieri.gd (blocco SU-957, dopo la guardia di release) e probe_su957_brina.gd. Stesso seme e stessa firma del mondo da file e da riga di comando. In release il file è ignorato.
  • 🔧 Dalla review R957: il file non si consumava, e un giro interrotto lasciava bot e sonda accesi a ogni avvio debug successivo. Ora si legge una volta per processo e si cancella (sonda files/homeless_city/scripts/tools/probe_su957_args.gd: 3 letture, stessi 2 argomenti, file sparito, ramo release che non lo legge). Il rilievo «bloccante» sul valore predefinito non costante è smentito dal compile-check e dalle 18 prove sul telefono. ⚠️ Le misure sono state prese col file non ancora consumato: gli argomenti ricevuti erano gli stessi.
  • tools/autotest/su957_honor.sh: 18 prove A-B-A da 60 s, con due difetti trovati sul telefono e corretti. adb logcat | grep -m1 | tee restava appeso dopo il match, ora passa da una FIFO e chiude esplicitamente il logcat. logcat -c non svuotava in tempo, e la riga della prova prima veniva presa per buona: ora il grep è ancorato all'etichetta della prova.
  • Tabella (Honor 10, seme 9570912, TMP/su957/honor_results.tsv), fps medi / 1% low: Centro 36,2 / 24,5; Brina piena 35,6 / 21,1 (9 prove); rumore Brina contro Brina ±2,4% / ±3,8%. Spegnendo una voce, rispetto alle due Brina piene accanto: marciapiedi_ghiaccio +4,6% / +7,0%; neve_sempre +2,6% / +5,1%; parchi_ghiaccio +4,2% / −0,3%; torce +2,5% / −2,7%; pinguini −1,2% / +2,3%; yeti −1,1% / −3,9%; pupazzi −3,5% / −2,7%.
  • 📌 Verdetto (TMP/su957/verdetto.md): marciapiedi_ghiaccio è l'unica voce che sposta entrambi i numeri fuori dal rumore, nella stessa direzione; è quella dove SU-828 chiede il materiale del terreno a due sorgenti. neve_sempre dà un segnale più debole, solo sull'1% low. Il divario vero con il Centro è sull'1% low (−13,8%), e i marciapiedi ne recuperano meno di un quarto: il resto non si isola con questi sette interruttori.
  • ⚠️ È un segnale, non una certezza: una prova per voce, con uno scarto di circa 2 volte il rumore. Nessuna cura scritta: il ticket vieta di toccare Brina finché la misura non dice cosa costa. Sull'Honor è rimasta la build release.

Salvataggio sulla nuvola: una lettura EOS lenta non fa più fallire il giro dopo con «già in corso» (SU-967)2026-09-13

Nato da SU-920: nel log dell'Honor, dopo una partita online, la lettura EOS scadeva a 5 s, il cancel_request non la fermava, e il tentativo dopo trovava la richiesta nativa ancora aperta (result=9). Lotto Codex C967C967b (ripreso dopo il disco pieno) → C967c e C967d (correzioni dalle review R967, R967b), gate K967, review finale R967c.

  • Diagnosi: al timeout di 5 s ProgressSync scollegava la callback e liberava _busy mentre l'handle nativo restava vivo. cancel_request esiste nel binding, ma l'SDK la dichiara best-effort (eos_playerdatastorage.h:167,190): non garantisce la chiusura.
  • Cura (files/homeless_city/scripts/autoload/ProgressSync.gd): letture e scritture aspettano la callback finale; a 5 s si logga soltanto «lento». Tetto assoluto a 30 s, senza contare il tempo in background (5 s di nuovo dal resume); al tetto si libera _busy ma si ricorda l'handle aperto, e il giro dopo aspetta la sua fine (callback o get_file_request_state) o, con result=9, riprova nello stesso giro invece di contare un fallimento. sync_on_exit() ha una scadenza unica di 5 s e non avvia una seconda transazione se quella in corso ha già sincronizzato lo stesso payload.
  • Log su disco: le sole righe [SYNC]/[ProgressSync] vanno anche in user://logs/progress_sync.log con flush(), in due file da 256 KiB a rotazione. La riga [SYNC] fase=remoto ora porta la durata vera in durata_ms.
  • Sonde: files/homeless_city/scripts/tools/probe_su967_pending.gd + tools/autotest/probe_su967_c967d.sh, 26 casi, rilanciati al gate. Il caso del log dell'Honor (callback a 5,6 s): prima 3 letture, 1 «già in corso», 1 cancel; dopo 1 lettura, 0, 0, un solo upload result=0. Callback persa: _busy libero al tetto, il giro dopo riesce. Uscita dopo un salvataggio in corso: esito vero in 4,5 s, nessuna seconda richiesta. probe_su920_progress_sync.gd 113/113; probe_su920_eos_vero.gd su EOS vero VERDE nei 5 modi A B V R G, rilanciato dall'orchestratore dopo ogni giro.
  • ⚠️ Residuo dichiarato, non corretto (review R967c, quarto rilievo sullo stesso file, il giro è stato fermato): nello spegnimento, se l'attesa di una richiesta pendente finisce proprio sul limite, può partire una nuova read_file() che resta pendente mentre EOS si chiude; e sul timeout d'uscita la scadenza ripristinata può riportare a 30 s il tetto di un'operazione che non l'ha ancora controllata. Casi limite della sola chiusura dell'app.
  • ⚠️ Non misurati sull'Honor: la durata vera della lettura EOS dopo una partita online, un solo upload al primo giro nel caso reale, le righe su disco con l'app uccisa. Il prossimo log dell'Honor avrà durata_ms e il file progress_sync.log.

Il giullare nasce e cammina per strada, non tira attraverso i palazzi, e la sua carta cade dove si raccoglie (SU-975)2026-09-13

Ivan in chat: il giullare è comparso in mezzo ai palazzi e da lì lanciava birilli; i cani lo hanno ucciso e la carta è rimasta irraggiungibile. Lotto Codex C975C975b (ripreso dopo il disco pieno) → C975c e C975d (correzioni dalle review R975, R975b), gate K975, review finale R975c, ultima correzione inline dell'orchestratore.

  • Spawn: fino a 24 punti nell'anello, accettati solo se World.is_position_walkable; se falliscono tutti, ricerca deterministica a cerchi da 10 px, così la richiesta non si perde (files/homeless_city/scripts/world/DistrictSpawner.gd:206,264-288,319-349). 10 semi × 5 spawn: fuori strada 20/50 → 0/50; anello quasi tutto ostruito con un varco: trovato.
  • Camminata (files/homeless_city/scripts/npc/ModularNPC.gd:399-401,822-851): il giullare conserva l'ultima posa calpestabile e annulla il passo che entrerebbe in un ostacolo, scivolando sull'asse libero. Il controllo gira solo su host e singolo giocatore, mai sui puppet, che restano alla posa dell'host. Inseguimento 3.000 s simulati: fuori strada 12.000/30.000 campioni → 0.
  • Aggiramento (ModularNPC.gd:1553-1578, World.gd:10410-10434): con un palazzo in mezzo prende i due vertici liberi. La tappa si chiude a 8 px, come i waypoint della classe base: a 40 px il giullare alternava i due angoli senza passare. Palazzo 59×12: 358 alternanze e nessun arrivo → 0 alternanze, arrivo in 2,3 s.
  • Tiro: parte solo con la linea di tiro libera sugli ostacoli alti, compresi camioncini, pensiline e il monociclo del riccone, con lo stesso predicato che ferma davvero il birillo (World.gd:904-908,8013-8039). Muro in mezzo: 0 lanci; monociclo in mezzo: 1 lancio e 2 s di ricarica → 0 e 0. Birillo forzato contro un muro: si schianta a 3 px, non colpisce. PinProjectile.gd invariato.
  • Carta (World.gd:5222-5245, ModularNPC.gd:1888-1908): spostata sul punto calpestabile più vicino PRIMA che id e posizione vadano ai client. Giullare ucciso a ridosso di un palazzo, 10 casi dai cani e 10 dal barbone: irraggiungibili 20/20 → 0/20. Scatto a finestra TMP/su975/pin_muro.png: carte sul marciapiede, barbone sopra, il gioco risponde «La carta dorata non ti riconosce».
  • 🔧 Review a tre giri, tutti con difetti veri: richiesta di spawn persa, controllo di posa che scavalcava l'host sui puppet, giullare incastrato al muro, tiro contro camioncini e pensiline (R975); alternanza fra due angoli, che la sonda «secondi da fermo» non poteva vedere, e monociclo (R975b); linea di tiro calcolata a ogni tick, anche fuori portata (R975c). Quest'ultima corretta dall'orchestratore: il calcolo sta in _jester_has_clear_shot (ModularNPC.gd:1592), in coda alla condizione del tiro.
  • 📌 MP: spawn, camminata, tiro e carta restano decisi dall'host. RPC cambiate: nessuna.
  • ⚠️ Non provati: una partita vera a due peer, e la raccolta automatica della carta nella sonda -s (overlap 0/20; lo scatto a finestra mostra la raccolta avviata). Sonde: files/homeless_city/scripts/tools/probe_su975_jester.gd, probe_su975_pin_card.gd, probe_su975_corrections.gd.

Lo splash iniziale dura 1 secondo invece di 2 (SU-968)2026-09-13

Ivan in chat: «loading iniziale 1 secondo anziché 2». Modifica inline dell'orchestratore; misura sull'Honor del lotto Claude H1 (collaudatore).

  • SPLASH_MIN_SEC da 2,0 a 1,0 in files/homeless_city/scripts/ui/Boot.gd:84; il print «SU-416 splash» lo legge dalla costante.
  • ✅ Honor 10, build release firmata con la chiave di release, 5 avvii a freddo per lato, marcatori logcat (Displayed → «SU-416 splash» → menu): tratto dello splash 2,036 → 1,067 s; avvio totale 6,063 → 5,192 s (−0,87 s). I 4,0-4,3 s prima dello splash sono motore Android e autoload, fuori dal ticket. Il menu non ha mai sforato di più di 70-130 ms il minimo. Dati in TMP/su968/misure.md e TMP/su968/prima_dopo.tsv.
  • ⚠️ Non misurati: l'iPhone (non raggiungibile da qui) e il lampo grigio o nero fra splash e menu (screenrecord non c'è su questo Honor).
  • 📌 Sull'Honor è rimasta installata la build release della 0.43 con SU-968 (TMP/su968/su968_release.apk).

Il giornale del proprio collasso non ha più joystick e bottoni sopra (SU-974)2026-09-13

Ivan in chat: su telefono, in multigiocatore, al game over joystick e bottoni virtuali restano disegnati sopra il giornale. Lotto Claude L974 (dev-sonnet), gate Codex K974 (luna).

  • ✅ Causa: VirtualControls._hidden_by_modal() nasconde il pad solo con una modale visibile del gruppo touch_modal (SU-224) o con l'albero in pausa. Il giornale FINALE apre con l'albero già in pausa; quello PROVVISORIO del proprio collasso (SU-873, stesso codice per host e client) è volutamente senza pausa, e World._net_enter_spectator_mode tiene il pad acceso perché il fantasma vola con lo stick e possiede coi pulsanti (SU-886/SU-942). Il giornale non era mai iscritto al gruppo.
  • ✅ Cura in una riga: RunReport si iscrive da solo a touch_modal nella sua _ready() (files/homeless_city/scripts/ui/RunReport.gd:256), come già LevelUpScreen, SuperCeremony e Quaderno. PageCurl è figlio del giornale e segue la sua visibilità; HUD.gd e World.gd non toccati.
  • ✅ Provino files/homeless_city/scripts/tools/shot_su974_giornale_touch.gd a 844×390 e 1024×768, guardato al gate: prima joystick e diamante A/B/X/Y sul foglio, dopo foglio pulito; da fantasma prima del giornale e dopo la sua chiusura il pad c'è. Scatti in TMP/su974/prima_844/, TMP/su974/dopo_844/, TMP/su974/prima_1024/, TMP/su974/dopo_1024/.
  • 🔧 Al gate: i primi scatti erano a 720×1280, formato che il gioco non ha, e la controprova era stata fatta con git stash su un working tree condiviso con altri lotti in volo. Rifatti orizzontali, con il «prima» ottenuto da una copia del file; nessuno stash rimasto.
  • ⚠️ Non provati: una partita vera a due peer (host e client separati), il ritorno dal giornale in lobby o nella partita dopo (la sonda chiama _complete()), il singolo giocatore (coperto dalla pausa, verificato a lettura). Il provino è uscito con la cornice nera perché la finestra resta a schermo intero; il layout misurato è comunque quello giusto.

Tossici solo a Fumarola e Nottefonda: via da Brina (SU-973)2026-09-13

Ivan in chat: «no tossici a brina, tossici per ora solo a fumarola e nottefonda». Lotto Codex C973C973b (Sol, ripreso dopo il disco pieno), gate K973 (luna).

  • ✅ La regola «per ora» sta in un posto solo: NPC_ESCLUSI_PER_QUARTIERE in files/homeless_city/scripts/autoload/Quartieri.gd:276-288 esclude arch_junkie da Centro, Mercato, Gattopoli, Solleone, Brina e Collina; ogni quartiere la richiama nei suoi npc_esclusi, che lo spawner già consuma (Quartieri.gd:953-956). Per riaprire basta togliere un nome da quella tabella.
  • ✅ Sonda files/homeless_city/scripts/tools/probe_su973_tossici.gd + tools/autotest/probe_su973.sh, 5 semi del mondo per quartiere, rilanciata al gate: Brina 94 → 0; Fumarola 9 → 9, Nottefonda 40 → 40; gli altri cinque erano già a 0.
  • tossico_grazia_ingresso (SU-765) tolto dalla configurazione di Brina: il ramo di lettura in ModularNPC.gd:2382-2385 resta ma nessun quartiere espone più il flag, quindi è inerte.
  • 📌 Nessuna altra strada fa nascere un tossico: lo spawn ricontrolla gli esclusi anche sulle forzature (DistrictSpawner.gd:107-133), l'unico evento forzato è il giullare, e le varianti scure (npc_dark_manifest.gd:91-96) mappano solo frame. Il bypass resta per F1 e sonde.
  • ⚠️ Pool di Brina dopo il filtro (NPCDatabase.gd:694-700): non vuoto, 4 archetipi, ma il rivale pesa 30 su 65 (46%, con il tetto di 2 in partita). Segnalato e non toccato: il ticket vieta di cambiare gli altri nemici.
  • ⚠️ Nei sei quartieri senza tossici non si possono più fare: incontro, furto, bottiglia e KO del tossico (ModularNPC.gd:872-1057,1828-1832); l'avanzamento dell'impresa e della carta OCCHIO AI FURTI, 3 furti (MetaProgress.gd:109-111,1116-1122, PowerUpSystem.gd:122,638-640); la possessione del tossico (World.gd:2082-2085,11519-11521). Restano possibili a Fumarola e Nottefonda. Non curato: la domanda è a Ivan.
  • 🔧 Al gate: la sonda cercava la copia «prima» con res://../../TMP/…, che esiste solo se la copia di lavoro sta in TMP/ (sandbox Codex); da /tmp falliva il ramo «prima». Ora il wrapper le passa il path assoluto (--su973-before-copy).
  • ⚠️ Non provato a due peer: gli spawn li decide l'host e il client crea il puppet con l'archetipo ricevuto (World.gd:1156-1172,2490-2499,2627-2667), verificato a lettura.

La torcia vola meno lontano: gittata da 160 a 110 px (SU-972)2026-09-13

Ivan in chat: «meno gittata fiaccola». Lotto Codex C972C972b (Sol, ripreso dal parziale dopo il disco pieno), gate K972 (luna).

  • GITTATA da 160,0 a 110,0 px in files/homeless_city/scripts/entities/TorchProjectile.gd:23 (−30%), l'unica manopola da ritoccare se Ivan la vuole diversa.
  • ✅ Nessun 160 scritto a mano nel gioco: TorchProjectile.gd:95 calcola la posizione con la costante, Player._throw_torch (files/homeless_city/scripts/player/Player.gd:2612-2636) istanzia il proiettile e manda origine e direzione, TorchFireball.gd usa il punto di caduta ricevuto. Il 160 resta solo nelle sonde vecchie (probe_su812_torcia.gd, teaser_nemici_05_torcia.gd).
  • ✅ Sonda files/homeless_city/scripts/tools/probe_su972_torcia.gd + tools/autotest/probe_su972_torcia.sh, rilanciata al gate: caduta a 110,0 px; pupazzo a 100 px sciolto (fuoco a 10 px), pupazzo a 159 px no (49 px, oltre il raggio di 48). Il wrapper ora ha pipefail: una sonda rossa non esce più 0.
  • 📌 MP: World.gd:4644-4661 inoltra origine, direzione e tipo, e il peer crea lo stesso TorchProjectile con la stessa costante: si ferma nello stesso punto. Verificato a lettura, non a due peer.
  • ⚠️ Effetto collaterale visibile: VOLO_SEC resta 0,6 s, quindi la torcia vola anche più lenta (da ~267 a ~183 px/s) e l'arco da 44 px sembra più ripido. Non toccato: il ticket chiede la sola gittata.

La camera non smussa più l'inseguimento: spento lo smoothing, tremolio della camera dimezzato (SU-950, secondo giro)2026-09-13

Il primo giro (00895cb) aveva solo misurato e dato il verdetto: è la camera. Ivan in chat: «proviamo la strada dello smoothing camera e capiamo che succede su pc». Lotto Claude L950 (dev-sonnet), gate Codex K950 (luna).

  • position_smoothing_enabled = false sul Camera2D di files/homeless_city/scenes/world/Main.tscn e di files/homeless_city/scenes/world/TutorialWorld.tscn. La velocità 6,0 resta scritta ma non conta più.
  • ✅ Tre varianti misurate con la stessa sonda (files/homeless_city/scripts/tools/su950_tremolio.gd); camera in movimento in px di schermo, tablet | telefono: base (smoothing 6, IDLE) 14,3 | 13,6 · (a) smoothing spento 7,9 | 7,5 · (b) smoothing 6 nel passo fisico 13,3 | 13,3 (cambia il periodo, non l'ampiezza) · (c) smoothing 20 nel passo fisico 5,4 | 6,2. Conferma finale della cura: 6,5 | 6,2.
  • 📌 Scelta (a) e non (c): in movimento sono dello stesso ordine, ma (a) da ferma dà 0,000 px contro i 3,7 di (c) e i 5,0 della base, e non lascia una velocità da ritarare sui telefoni.
  • ⚠️ Barbone e scritte nel mondo restano a ~2,9 px: la posizione della camera arriva già con un fotogramma esatto di ritardo rispetto al barbone. È preesistente e fuori dalle cause elencate dal ticket; non toccato.
  • 📌 Cosa cambia per chi gioca: la camera segue il barbone rigida, senza il piccolo ritardo che smussava scatti e curve. Interactable.gd:2087 (_metro_riallinea_camera) e i toggle di Su911Perf.gd restano coerenti (verificato a lettura).
  • ⚠️ Non provati: la percezione su PC e sui telefoni, e il gioco vero (sequenza metro compresa). Misure prese con altri Godot in parallelo: scarto del 5-12% sui valori assoluti, stessa forma. Dati in TMP/su950/giro2/.

Le copie di lavoro delle sonde non riempiono più il disco: clone APFS e pulizia dopo 24 ore (strumenti di collaudo)2026-09-13

Lo sprint della sera si è fermato con ENOSPC: no space left on device su tutti i lotti insieme: tre builder Claude e cinque lotti Codex, questi ultimi morti senza FINE.json dopo 43 minuti. Ivan ha trovato TMP/ piena di cartelle _proj che crescevano.

  • 📌 Cosa erano. tools/autotest/common.sh (sync_proj) fa girare ogni sonda su una copia del progetto Godot in ${SU_TMP}_proj. Serve a importare fuori dal progetto vero, togliere le credenziali EOS (force_enet) e forzare la finestra (override.cfg). Ogni lotto usa un SU_TMP suo, per non pestarsi con quelli in parallelo, e ogni SU_TMP nuovo produceva una copia intera (2,6 GB con .godot/imported) che nessuno cancellava. Dal 06/09 ne erano nate 173: 156 in TMP/ e /tmp/su_501_* (~367 GB) più 17 annidate (15 dentro TMP/su912_fix/, 39 GB). I lotti Codex le mettono in TMP/ perché la loro sandbox non scrive in /tmp.
  • Tolte tutte, con l'ok di Ivan e dopo aver controllato che nessun processo le usasse (girava solo il suo editor, sul progetto vero). Disco da 17 a 438 GB liberi, TMP/ da ~300 a 17 GB.
  • La copia nasce come clone APFS (cp -cpR, solo su macOS): misurato 7 s e 10 MB contro 2,6 GB. -p conserva gli mtime, quindi l'rsync che segue non riscrive niente; il sync successivo impiega 2 s.
  • Pulizia automatica a ogni sync_proj: si cancellano le copie non usate da più di 24 ore (SU_PROJ_TTL_MIN), anche annidate fino a 3 livelli in TMP/, e solo cartelle che contengono project.godot. L'ultimo uso sta in un file accanto alla copia (<copia>.uso), non dentro: dentro, l'rsync --delete lo toglierebbe, e un run parallelo potrebbe prendere per abbandonata una copia che sta ancora nascendo.
  • ✅ Avviso su stderr quando restano meno di 20 GB liberi.
  • ⚠️ Il primo elenco guardava solo TMP/*_proj e ha mancato le 15 copie di TMP/su912_fix/: la pulizia ora scende di livello con find … -prune.

RIENTRI ATTESI (messi In revisione il 2026-09-13, /sprint della sera da job in background): tredici chiavi — SU-950, SU-972, SU-973, SU-974, SU-968, SU-782 (arte da approvare, niente montaggio), SU-975, SU-967, SU-957, SU-976, SU-971, SU-966, SU-969. Non aperti, in attesa di Ivan o di dati: SU-970 (manca lo screenshot dell'iPhone), SU-954, SU-953, SU-871, SU-870, SU-795, SU-790, SU-783 e SU-781 (telemetria 0.44), SU-461.

Codici beta e RESET PROGRESSI arrivano alla nuvola: indagine su device, nessuna modifica al gioco (SU-920, quarto giro)2026-09-13

Richiesta di Ivan in chat dopo il collaudo del 13/09: «anche se faccio i codici tuttimondi tuttipersonaggi deve salvare in cloud i progressi, allo stesso modo deve fare quando resetto dalle opzioni di gioco i progressi, ora non lo fa». Lotto Claude S920D (dev-sonnet), consulto Codex C920D (Astra) e poi un'indagine sui log veri dei due telefoni. Decisione di Ivan: SU-920 torna In revisione così com'è; i timeout EOS diventano SU-967.

  • ✅ Nessuna chiamata mancante: i due percorsi chiedono già la sincronizzazione dal commit b9c0d3a. Codici in MainMenu.gd ~7304/~7314, reset in MainMenu.gd ~4237, dopo il segno mark_progress_reset().
  • ✅ Sonde estese. files/homeless_city/scripts/tools/probe_su920_progress_sync.gd: scenari G5a e G5b (codici, reset e dispositivo rimasto indietro), 113/113, con controprova rossa senza la sync. files/homeless_city/scripts/tools/probe_su920_eos_vero.gd: --modo=R (reset su EOS vero) e --modo=G (scrittura e lettura a più blocchi, 4261 e 13.561 byte, sha256 identico). files/homeless_city/scripts/tools/su920_payload_size.gd: salvataggio reale 1513 B, tutto sbloccato 2533 B.
  • Prova su device, Honor aperto sul menu, 16:46-16:48: reset scritto sulla nuvola 2,4 s dopo e TUTTIPERSONAGGI 4 s dopo, entrambi dal sorvegliante («cambio locale»). L'iPhone li riceve riaprendo.
  • 📌 Letti i log dei device. iPhone da devicectl (TMP/sprint_0913/iphone_logs/). Honor con un APK debug firmato con la chiave di release, che rende leggibili logcat e i user://logs delle sessioni release precedenti (TMP/sprint_0913/honor_logs/, TMP/sprint_0913/apk_debug/). Poi l'Honor è tornato alla release 8283f03.
  • ⚠️ Tre «non arriva» spiegati, uno no. Alle 15:41 l'Honor era su un altro account (PUID …1300). Il reset delle 11:03 era della build del mattino ed è salito solo alle 14:40. Non spiegato: alle 16:38, sulla release, il codice non è salito per 6 minuti, e il log si è troncato quando l'installazione ha chiuso l'app (le print restano nel buffer).
  • 📌 Visto sull'Honor dopo le partite online: lettura EOS in timeout (5 s) due volte, poi result=9 (AlreadyPending), poi scritta → SU-967.
  • 📌 Nel log dell'Honor, un secondo invio vuoto del campo CODICE un secondo dopo quello valido («codice non valido (0 caratteri)»): innocuo per la sync, da guardare se a schermo sostituisce il messaggio di successo.

NON PROVATO: il caso delle 16:38 non si è riprodotto. Gate Codex non lanciato: nessun file del gioco cambiato, solo sonde.

Lo yeti senza pezzetti staccati nelle camminate (SU-965, KO)2026-09-13

KO di Ivan dal collaudo su device del 13/09 (build 390d001): «alcuni sprite della camminata a sinistra e destra hanno ancora qualche pixel fuori posto ritagliato dallo sprite vicino, verificare». La zampata del colpo resta accettata. Lotto Codex C965k (Astra), gate K965k, più una correzione al gate.

  • files/homeless_city/assets/sprites/yeti_sheet.png — censimento delle componenti connesse cella per cella: 7 frammenti, 82 pixel, tolti. Quattro sono pezzi del corpo della cella accanto (braccio in E0, piede in E2, mano in SE0 e SE2), già presenti nel raw: il ritaglio è della griglia del raw, non dell'offset di SU-965. Tre sono scarti ad alfa quasi zero (S3, S5). Nessuna arte staccata voluta.
  • ⚠️ Corretto al gate, non dal builder: in tutte le camminate NE (colonne 0-3 e 5) l'ultima riga della cella era una linea semitrasparente larga 47 px (alfa medio 32) sotto i piedi. Il builder l'aveva lasciata perché toccava il corpo e quindi non era un «frammento». Tolti 191 px, solo quelli senza niente sopra: i pixel dei piedi restano. Visibile solo nel provino a zoom ×4.
  • ✅ Numeri: statura CC e piedi invariati in tutte le 30 celle; colonna del colpo diff 0; raw invariato (SHA).
  • ✅ Provino TMP/su965/shot_yeti_camminata.png (tutte le camminate con gli specchi, ×4), guardato prima e dopo la linea NE. files/homeless_city/scripts/tools/shot_su965_yeti_colpo.gd ora produce anche questo scatto.
  • NON PROVATO su device.

Il puntino dell'avversario dice dove sta davvero: niente più scarto (SU-955, KO al terzo giro)2026-09-13

KO di Ivan dal collaudo su device del 13/09: «il puntino ora si muove a caso sulla mappa addirittura andando solo in orizzontale riesce pure ad andare in verticale e cambiare verso durante il movimento». Era il prezzo dichiarato alla chiusura (181): decorrelare il verso in 300 px vuol dire un puntino che gira mentre l'avversario cammina. Messo davanti a tre alternative (scarto fisso per avversario, posizione vera in ritardo, nessuno scarto), Ivan ha scelto «nessuno scarto» — scritto sul ticket. Lotto Codex C955b (sol), gate K955b.

  • 📌 Istruttoria: i livelli della carta non toccavano solo la precisione. Il livello 1 accende i puntini sulla mappa, il 2 aggiunge le frecce (files/homeless_city/scripts/systems/PowerUpSystem.gd:545-568); nessun testo nelle 8 lingue promette una precisione che migliora (CARTA_EFF_RADAR_MAPPA, CARTA_EFF_RADAR_FRECCE), e l'arte per gradino racconta puntini → frecce. Quindi i due livelli restano utili e nessun testo va riscritto.
  • PowerUpSystem.gd — scarto e ampiezza sempre zero; tolti il campo spaziale, SIXTH_SENSE_BLUR_PX e SIXTH_SENSE_PRECISION_BASE. Le due funzioni restano (ritornano zero) perché mappa, freccia e sonde le chiamano per nome.
  • files/homeless_city/scripts/ui/MapOverlay.gd e files/homeless_city/scripts/world/World.gd — puntino e freccia sulla stessa posizione vera, _net_target_pos dell'avversario. Zero rete.
  • ✅ Numeri a entrambi i livelli: errore da fermo 0 px, in moto orizzontale 0 px, deriva verticale 0 px, delta mappa/freccia 0 px, angolo del moto 0°. Sonde files/homeless_city/scripts/tools/probe_su955_verso.gd, files/homeless_city/scripts/tools/su955_radar_fermo.gd.
  • 📌 Tre giri su un ticket perché un KO ambiguo («shiftato a destra di un palazzo») è stato letto due volte a modo nostro. La lezione è in memoria: se la cura ha un prezzo che si vede, si chiede prima.
  • NON PROVATO online su device.

Da posseduti il proprio segnalino sulla mappa segue il nemico (SU-963)2026-09-13

Dal collaudo di Ivan del 13/09: «non si muove l'indicatore sulla mappa se possiedo un nemico, rimane fermo dove l'ho posseduto». Lotto Codex C963 (sol), gate K963.

  • 📌 Causa: la mappa disegnava il nodo Player, che da posseduti resta fermo (set_physics_process(false), SU-948) mentre si muove il nemico.
  • files/homeless_city/scripts/ui/MapOverlay.gd — il segnalino locale usa il corpo guidato, riusando World._net_player_interest_position() (files/homeless_city/scripts/world/World.gd:2578), che esisteva già; il registro della possessione si riempie anche sui client (_cl_possessed, World.gd:11306-11309), quindi vale su entrambi i telefoni. Fine possessione: ritorno immediato sul barbone, senza interpolazione, perché posizioni intermedie sarebbero false. Indicatori degli altri invariati, scarto del fiuto non toccato.
  • Numeri (sonda files/homeless_city/scripts/tools/probe_su963_indicatore.gd, rilanciata dal thread principale): nemico posseduto portato per 412,3 px — errore massimo indicatore/nemico 412,3 px prima, 0,000 px dopo; a fine possessione indicatore/barbone 0,000 px; altro giocatore 0,000 px di variazione.
  • 📌 Criterio 3 senza oggetto: l'unica freccia sul proprio giocatore è quella verso il rifugio, e da fantasma non si aggiorna (files/homeless_city/scripts/player/Player.gd:1081-1083 esce prima di _update_shelter_arrow); la possessione esiste solo da fantasma.
  • NON PROVATO: EOS vero e la resa della proiezione sulla mappa (la sonda misura in coordinate mondo).

In multiplayer la pausa apre solo il menu; e lo spettatore ha il tasto per cambiare giocatore (SU-959, SU-960)2026-09-13

Due ticket dal collaudo di Ivan del 13/09 a due telefoni. Lotto Claude A959 (architetto-opus), tre giri; gate K959, review R959 e R959b.

SU-959 — «in multiplayer il menu pausa deve solo aprire il menu, ma non deve fermare nulla del mondo sotto».

  • 📌 Chi fermava il mondo in MP: HUD._toggle_pause (get_tree().paused senza guardare se la partita è MP) e MapOverlay.open() (sempre). Ventaglio e cerimonia avevano già _paused_mode = not is_mp. Le statistiche smettevano di calare perché GameState è un autoload che si ferma con l'albero.
  • files/homeless_city/scripts/ui/HUD.gd — in MP il menu non tocca l'albero; finché è aperto l'HUD sale a 70, sopra ventaglio (60), mappa (50) e cerimonia (40), riallineato a ogni frame invece che negli 8 punti di chiusura. files/homeless_city/scripts/ui/LevelUpScreen.gd, files/homeless_city/scripts/ui/SuperCeremony.gd, files/homeless_city/scripts/ui/MapOverlay.gd: sotto il menu prendono i tocchi su uno scudo, fermano la scelta automatica dei 12 s e restano ad aspettare; guardia anche nei gestori dei tocchi.
  • Numeri (due peer, pausa aperta dal client 60 s): fame client −49,69 / host −49,72, energia −54,0 e igiene −36,0 su entrambi; col codice di prima il client calava di −0,10. Scatti TMP/sprint_0913/A959/su959_dopo/1a_menu_partita_viva.png, TMP/sprint_0913/A959/su959_dopo/2a_ventaglio_sotto_menu.png, TMP/sprint_0913/A959/su959_dopo/3a_mappa_sotto_menu.png, guardati. Single player invariato.
  • Sotto il menu non si gioca: files/homeless_city/scripts/player/Player.gd (is_gameplay_input_blocked()), files/homeless_city/scripts/world/World.gd (posseduto, X del fantasma e tocco dello spettatore) e files/homeless_city/scripts/world/Interactable.gd (A sugli edifici, tenute di metro e banca) ignorano l'input col menu MP aperto e nel frame in cui si chiude. 2 s di comandi a menu aperto: 0,0 px, nessuna azione.
  • ⚠️ Presi dalla review, non dal builder: il tocco dello spettatore sullo sfondo cambiava giocatore sotto il menu; con MANTIENI JOYPAD NEI MENU acceso una tenuta già avviata depositava in banca ($490→$410) o partiva in metro a menu aperto; una fine partita con Pausa → Opzioni aperte lasciava le Opzioni sopra il game over e la musica in ALWAYS sulla fanfara (HUD._close_pause_for_run_end()).
  • 📌 Ipotesi della review smentita: «lo scudo a z 0 lascia passare i tocchi alle carte a z 100». Misurato: col menu aperto sul punto della carta la GUI sceglie lo scudo — Godot sceglie per ordine dei nodi, non per z_index.
  • 📌 Voluto: col menu MP aperto cuori e barra XP si disegnano sopra carte e mappa (il menu è lo strato più alto).

SU-960 — «non ho il joypad ne i bottoni, quindi non posso premere x per cambiare giocatore».

  • HUD.enter_spectator_mode() — un tasto X a schermo, con lo sprite del pad, solo a chi entra a partita iniziata su touch e senza controller; fa quello che fa la X di SU-942. Il fantasma collassato tiene il pad intero (gli serve per volare e possedere). Un tocco = un passo (P2→P3); col controller il tasto sparisce; partita nuova → comandi normali. Scatti TMP/sprint_0913/A959/su960/1_spettatore_touch.png, TMP/sprint_0913/A959/su960/2_dopo_tocco_giocatore_successivo.png.

Sonde files/homeless_city/scripts/tools/probe_su959_pausa_mp.gd (26/26), files/homeless_city/scripts/tools/probe_su959_sp.gd (11/11), files/homeless_city/scripts/tools/probe_su959_input.gd (28/28), files/homeless_city/scripts/tools/shot_su960_spettatore.gd (13/13). NON PROVATO su due telefoni veri; il tasto X con un dito vero (nel provino il tocco è iniettato).

Il salvataggio sulla nuvola: file assente pubblicato, scritture quando cambia davvero, e l'ID dell'account in vista (SU-920, KO al terzo giro)2026-09-13

KO di Ivan del 13/09 (Honor + iPhone + iPad): «i progressi non sono salvati su epic! sono sballati tra device». Le due correzioni precedenti (e8d6759, 6dc16eb) erano provate solo con sonde che sostituivano trasporto e identità. Prima del terzo tentativo un consulto Codex (D920, Astra, sola lettura) sui log reali dell'11/09; poi lotto Claude A920 (architetto-opus), due giri; gate K920, review R920.

  • 📌 Cosa diceva il consulto, e cosa ne è rimasto sul codice: confermati quattro difetti (sotto); smentito sul Mac il trasporto EOS; confermato come meccanismo che accessi con provider diversi danno PUID diversi e quindi file diversi — nei log dell'11/09 l'Honor passa da …11f0 a …1300 dopo un logout. Questo il codice non può aggiustarlo: si può solo renderlo visibile.
  • files/homeless_city/scripts/autoload/ProgressSync.gdfile assente = nuvola vuota: con EOS result=18 il locale ora si pubblica; prima finiva in «rinviato» e ogni avvio rileggeva 18 per sempre. Timeout ed errori veri restano «rinviato».
  • Si scrive quando i progressi cambiano, non solo a fine partita: acquisto di personaggio e Baracca, codici beta, reset (files/homeless_city/scripts/ui/MainMenu.gd), un controllo ogni 3 s fuori partita per gli sblocchi che non passano dal menu, e l'app mandata in sfondo (su telefono l'app si sospende e quasi mai si chiude: prima si scriveva solo su WM_CLOSE_REQUEST). Al ritorno dallo sfondo si rilegge.
  • La lettura d'avvio non si consuma su un fallimento: si ritenta al ritorno al menu e dallo sfondo, e si considera fatta solo a upload riuscito.
  • ⚠️ Presi dalla review, non dal builder: un acquisto fatto durante l'attesa della lettura EOS veniva sovrascritto dalla fusione (ora il locale si rilegge dopo la risposta); il sorvegliante segnava un contenuto come caricato prima dell'esito, e dopo una scrittura fallita non ritentava più (ora ritenta con attesa crescente fino a ~1 min); la riga ID non si aggiornava perdendo la sessione.
  • Righe di log fisse [SYNC] identita provider=… puid=…<4> ambiente=… e [SYNC] fase=locale_pre|remoto|fuso|locale_post|upload result=… skin=… quartieri=… livello=… reset_epoch=…: la prossima prova su device dice da sola dove si perde. «livello» conta le mappe ENDLESS, perché in meta.cfg un livello del giocatore non esiste.
  • «ID …xxxx (ambiente)» nella schermata ACCOUNT, in alto a destra: due dispositivi con suffissi diversi non possono condividere il salvataggio. Scatto TMP/sprint_0913/A920/shot/account_id_844x390.png.
  • Player Data Storage VERO, dal Mac, senza credenziali nuove (files/homeless_city/scripts/tools/probe_su920_eos_vero.gd, Device ID del portachiavi, file di prova sonda_su920_eos.json e non progressi.json): A legge 18, pubblica in 977 ms, sblocca e riscrive in 225 ms; B, nuovo processo con un suo locale, legge, fonde e ha l'unione (skin 1→2→3). Un terzo processo verifica che il progressi.json vero dell'account del Mac non sia stato toccato e che i cfg veri abbiano lo stesso SHA prima e dopo. L'account di Ivan non è stato toccato.
  • ✅ Sonda offline files/homeless_city/scripts/tools/probe_su920_progress_sync.gd: 81 controlli verdi (37 del primo giro + 44), con sblocchi prodotti da Skins.buy/Skins.unlock/MetaProgress.unlock veri e non da campi sintetici; controprove rosse sul codice precedente: 17 KO al primo giro, 5 al secondo, tutti quelli attesi. Regressioni verdi: fine partita, SU-937, SU-912, SU-786, SU-927.
  • NON PROVATO: i telefoni veri (sospensione su iOS e Android), i PUID dei tre dispositivi di Ivan. È il terzo giro: se torna KO, con le righe [SYNC] si porta a Ivan la diagnosi, non un quarto tentativo.

Da fantasma il mondo continua a muoversi: pupazzi, cactus e riccone (SU-964)2026-09-13

Dal collaudo di Ivan del 13/09 a due telefoni: «i pupazzi di neve non si muovono sul client quando muore». Lotto Claude A964 (architetto-opus), tre giri; gate K964, review R964.

  • 📌 Quale caso era: i pupazzi si fermavano sul telefono di chi muore, client o host che fosse, e solo loro fra i nemici di quel quartiere (pinguini, yeti e passanti continuavano). Causa files/homeless_city/scripts/world/ParkFoe.gd:122: il giro nel parco usava DifficultyManager.elapsed, l'orologio della partita, che va avanti solo col giocatore vivo (DifficultyManager.gd:109). SU-888 aveva già dato al fenicottero l'orologio da spettatore, ma non al pupazzo.
  • Criterio 3 preso alla lettera — «da fantasma si guarda una partita viva»: l'inventario di chi usa quell'orologio per muovere qualcosa ha trovato fermi anche i cactus di SOLLEONE (files/homeless_city/scripts/world/Cactus.gd) e il riccone di COLLINA con l'host morto (files/homeless_city/scripts/world/Richman.gd, più files/homeless_city/scripts/world/World.gd, che smetteva di inviarne la posizione: senza, sul telefono del VIVO il riccone restava a 0 px).
  • ✅ L'orologio da spettatore ora sta in ParkFoe.ora() (tolte le aggiunte singole in Snowman.gd e Flamingo.gd, così il fenicottero non va al doppio); scioglimento, pozza e rimontaggio del pupazzo lo seguono; l'animazione della camminata avanza.
  • Numeri (percorso in 10 s dopo la morte, prima → dopo): pupazzo sul morto 0,0 → 269,6 px (client) e 0,0 → 269,5 (host), sul vivo 269,6-269,8 invariato; cactus sul morto 0,0 → 347,2 px; riccone con l'host morto 0,0 → 893,7 sul morto e 0,0 → 888,0 sul vivo. Fenicotteri 154-189 px. Pupazzo allineato sui due telefoni (188,932 / 188,932). Nessuna RPC e nessun pacchetto nuovi.
  • ⚠️ La sonda passava anche rotta, preso dalla review: gli exit code dei peer erano solo stampati e il touch finale dava 0; il path con lo spazio («Progetti Claude») spezzava la lista dei log e i conteggi uscivano sempre 0; la prova della pozza misurava l'orologio e non lo scioglimento, e un primo tentativo di riscriverla passava per finta (bruciatura anticipata → valore negativo = «intero»). Ora tools/autotest/probe_su964_mp.sh esce 1 su qualunque FALLITO; controprova rossa con lo scioglimento sull'orologio vecchio: 3 FALLITO, exit 1.
  • 📌 Trovato e NON toccato, decide Ivan: con l'host morto si spegne per tutti il comportamento ostile di tossico, spazzino, gattara (files/homeless_city/scripts/npc/ModularNPC.gd:926, 1141, 1351, 1461, 1898, 2146, stesso orologio): i vivi smettono di essere derubati e colpiti. È gameplay, non movimento. Anche tinta del quartiere e buio della notte cambiano sul telefono del morto (World.gd ~7780-7839, ~8464), non misurato.
  • match_alive e non partita_viva nel World.gd (rinominato al gate). NON PROVATO su EOS coi due telefoni.

Lo yeti non ha più la zampata tagliata: cella visiva da 52 (SU-965)2026-09-13

Dal collaudo di Ivan del 13/09: «yeti gli sprites non sono tagliati bene quando fa l'animazione di attacco». Lotti Codex C965 (Astra, diagnosi), C965b (Astra, montaggio), C965c (lo script del provino).

  • 📌 Né il raw né la riduzione, ma la cella: nel raw sprites_raw/yeti_sheet.png la posa del colpo sta intera nella sua cella (0 pixel sui bordi). Ridotta alla stessa scala della camminata (0,1971) è alta 47-49 px sulla componente connessa: in una cella da 48 non entra col margine. Una riduzione BOX rifatta a 48 tocca ancora i bordi, e rimpicciolirla l'avrebbe fatta «sgonfiare» a ogni colpo. Nel 47 del riquadro grezzo della riga S c'era anche un frammento staccato.
  • files/homeless_city/assets/sprites/yeti_sheet.png 288×240 → 312×260, sempre 6×5: celle VISIVE da 52, colonna 4 ricavata dal raw con BOX alla scala della camminata; le altre 25 celle copiate identiche all'offset (2,4) — diff 0 pixel ciascuna. Il raw non è cambiato.
  • files/homeless_city/scripts/world/DistrictFoe.gd — campo sprite_cell_size (0 = usa cella), pivot e controllo del foglio sulla cella visiva; files/homeless_city/scripts/world/Yeti.gd:96 lo mette a 52. La cella LOGICA resta 48: contatto, portata e colpo invariati.
  • Numeri, colonna 4 riga per riga (y_min/y_max prima → dopo): E 0/45→3/49, SE 0/46→4/50, S 0/47→3/49, NE 0/47→4/51, N 0/47→3/51 (in NE e N il fondo coincide coi piedi, come nella camminata). Statura CC camminata 39-46 / colpo 47-49 (le braccia alzate). Piedi nel mondo camminata/colpo: delta 0 px in tutte le pose. Pinguino 32×32 e fenicottero 32×32 identici a prima.
  • ✅ Provino in-engine guardato: TMP/su965/shot_yeti_colpo_col4.png (colpo a sinistra, camminata a destra, zoom ×4) — le cinque zampate sono intere.
  • 📌 Nello stesso scatto le celle di camminata E, SE e NE mostrano pochi pixel staccati accanto allo yeti: c'erano già (celle copiate identiche), fuori dal ticket.
  • Sonde files/homeless_city/scripts/tools/probe_su965_yeti_cella.gd, files/homeless_city/scripts/tools/shot_su965_yeti_colpo.gd. NON PROVATO su device.

Il pinguino si lascia possedere (SU-961)2026-09-13

Dal collaudo di Ivan del 13/09 su EOS vero: «pinguino non me lo fa possedere». Lotto Codex C961 (sol), patch riscritta in C961b, sonda sul file vero in C961d.

  • 📌 Non mancava nessun pezzo di configurazione: il pinguino è nel gruppo e nell'elenco dei possedibili (files/homeless_city/scripts/world/Penguin.gd:86, files/homeless_city/scripts/world/World.gd:11401). La causa è la tenuta: per i 3 s del tasto premuto il fantasma doveva restare entro 48 px dal bersaglio, controllato sia in locale sia dall'host, e il pinguino a 45 px/s ne percorre 135. Yeti e fenicottero passavano perché in quei 3 s si spostano poco.
  • World.gdPOSSESSION_HOLD_LEASH = 192.0: l'aggancio resta a 48 px (POSSESSION_RANGE), durante la tenuta il limite sale a 192 px nei due controlli, locale e host.
  • ⚠️ Corretto al gate: la prima patch del builder toglieva del tutto il controllo di distanza durante la tenuta — un fantasma poteva agganciare un nemico e andarsene quanto voleva, contro il «non toccare la tenuta» del ticket. Il guinzaglio finito copre il pinguino e vieta la fuga.
  • ✅ Sonda files/homeless_city/scripts/tools/probe_su961_pinguino.gd sul World.gd vero: pinguino che cammina 135 px (distanza 40→175) → posseduto; fantasma a 193 px → tenuta annullata; yeti e fenicottero a 40 px → posseduti; il colpo del pinguino da posseduto è la scivolata di SU-948 (2,50 s, −1 vita).
  • NON PROVATO su device: la sonda non passa dalla rete.

Online le monete lasciate a terra da un altro si vedono, e le raccoglie uno solo (SU-956, KO)2026-09-13

KO di Ivan del 13/09: «non vedo le monete caduto dagli altri giocatori, tipo se chiedo l'elemosina e le lascio a terra non le vedo!». Il giro del 12/09 aveva curato il punteggio che segue ogni moneta (5e869bc, resta valido) su una premessa falsa: le monete degli altri non esistevano sull'altro schermo. Lotto Claude A956 (architetto-opus), due giri; gate K956, review R956.

  • Il difetto nei numeri, prima della cura (sonda a due peer con monete vere): elemosina da $12 lasciata dall'host → il guest ne vede 0 su 3, e viceversa. Le monete del colpo di gatto invece erano già replicate (8/8), ma una moneta contesa veniva pagata due volte (+$2 a entrambi).
  • files/homeless_city/scripts/world/World.gd:4105-4555 — in multiplayer drop_coin_reward e la fontana del bidone d'oro danno un id di rete a ogni moneta e mandano agli altri posizioni, tagli e XP già decisi (≤5 monete per pacchetto, 10 per lotto di fontana, <300 byte). La raccolta la decide l'host: vince la prima richiesta, la moneta sparisce per tutti, il $ va solo al vincitore. files/homeless_city/scripts/pickups/CoinDrop.gd, files/homeless_city/scripts/pickups/MoneyCoin.gd: in MP chiedono all'host e restano nascoste fino alla risposta.
  • Dopo: elemosina 3/3 nei due versi; gatto 8/8; contesa host +$12, guest +$0, nessuna copia; furto a piedi guest +$2 / host +$12. Punteggio della propria riga ancora nello stesso fotogramma («2. P43 63»).
  • ⚠️ Preso dalla review, non dal builder: l'host accettava la richiesta di raccolta da qualunque distanza. Ora misura dal Player del richiedente al punto di nascita della moneta, con una portata di 246 px (17 raccolta + 8 corpo + 96 di latenza a 270 px/s × 0,35 s + 125 di CALAMITA al massimo, che l'host non conosce per gli altri); 400 px se ha visto partire ASPIRATUTTO di quel peer, 640 per la mancia della propria suonata. Richiesta da 445 px: rifiutata, moneta ancora a terra per tutti; poi ci cammina sopra: accettata. Il PARACADUTE non aspira più le monete condivise (le avrebbe richieste all'infinito).
  • 📌 Ipotesi della review valutate e lasciate fuori: che l'host verifichi valore e XP di ogni moneta. In questo gioco i $ sono locali a ogni client dappertutto: sarebbe un sistema anti-trucco nuovo, non questo ticket. _cl_coin_drops_spawned resta any_peer perché le monete del guest partono dal guest; farla authority vuole una RPC di inoltro in più.
  • ⚠️ Da sapere alla release: il minimo MP va alzato. Due RPC nuove (_cl_coin_drops_spawned, _srv_coin_claim) e _cl_coin_collected cambia firma (id, winner) e ora arriva solo dall'host — il cambio di firma la guardia rg_guardia_rpc_mp non lo vede.
  • NON PROVATO: EOS sui due telefoni (quanto si sente il ritardo del $, e se 96 px di latenza bastano: monete che spariscono e ricompaiono sotto i piedi vorrebbero dire di alzarlo); tre giocatori; chi entra a partita iniziata non riceve le monete già a terra; forziere, bidone e piccione d'oro li vede solo chi li ha, quindi agli altri le monete escono da un punto vuoto.
  • Follow-up: commenti ormai falsi in files/homeless_city/scripts/world/ChestLoot.gd:35-39, files/homeless_city/scripts/world/GoldenPigeon.gd, files/homeless_city/scripts/systems/SuperPower.gd:1114; la carta dorata (net_golden_card_taken) ha ancora la doppia raccolta chiusa qui per le monete.

Il nemico posseduto si ferma contro il rifugio e i palazzi (SU-962)2026-09-13

Dal collaudo di Ivan del 13/09 su EOS vero: possedendo uno yeti «passa attraverso il rifugio». Lotto Codex C961 (sol) più due giri di correzione dalle review incrociate (C961b, C961c), gate K962, review R962 e R962b.

  • 📌 Causa: l'IA dello yeti evita gli edifici coi rettangoli blocchi (files/homeless_city/scripts/world/Yeti.gd:211), mentre rifugio e palazzi sono StaticBody2D del layer 1. Il passo del posseduto (DistrictFoe._possession_step) guardava solo i blocchi: contro i corpi fisici veri non si è mai fermato. DistrictFoe è un Node2D, non un corpo fisico, quindi nessuna collisione arrivava da sola.
  • files/homeless_city/scripts/world/DistrictFoe.gd:681-788 — solo nel passo posseduto: query circolare (raggio 8) sui corpi statici del layer 1, passo spezzato in tratti ≤8 px, ripiego sugli assi per scorrere lungo il muro. IA invariata al pixel.
  • Numeri, 3 s di spinta: yeti contro il rifugio (300,500)→(341,25, 500), contro un palazzo (780,500)→(821,25, 500); pinguino uguale; fenicottero 341,67 e 821,67. Scorrimento lungo il muro (1330,350)→(1361,8, 467,3). Traiettoria dell'IA prima/dopo: errore massimo 0,0000 px.
  • ⚠️ Tre difetti presi dalle review, nessuno visto dal builder: (1) un nemico posseduto mentre era già dentro un solido restava incastrato per sempre — ora esce (centro del rifugio → x 400→565,9 in 3 s); (2) si controllava solo il punto d'arrivo, quindi uno scatto attraversava una parete da 2 px — ora no, anche con lo scatto vero del fenicottero a 180 px/s; (3) da dentro un solido si accettava qualsiasi passo che toccasse gli stessi ostacoli, anche andando più a fondo: da poco dentro il lato sinistro si attraversava tutto l'edificio — ora il passo è accettato solo se nessuna profondità di penetrazione aumenta (spinto a destra resta a x 354, a sinistra esce a 188). La sonda passava anche con un nemico immobile: aggiunti il moto in campo libero (30/30 px) e la controprova rossa --su962-force-block.
  • Sonda files/homeless_city/scripts/tools/probe_su962_collisioni.gd, rilanciata dal thread principale: 0 bocciature.
  • NON PROVATO su device né nella città generata intera: la sonda usa corpi statici veri del layer 1 ma costruiti a mano.

La fermata della metro ha anche le scintille a + della banca (SU-952, KO)2026-09-13

KO di Ivan del 13/09: «deve anche avere i brillii fatti a + che si vedono sulla banca». Il bagliore dorato del 12/09 era accettato ma incompleto: mancava il glitter di SU-535. Lotto Codex C952 (sol), gate K952.

  • files/homeless_city/scripts/world/Interactable.gd:2802,2821_monta_glitter_banca() generalizzata (padre, riferimento, nome, quantità) e riusata, non duplicata: la banca la chiama come prima, la fermata con GlitterFermataPremio. Stesso BankGlitter.gd, stessa forma a +.
  • 8 scintille sulla fermata contro le 20 della banca: la densità della facciata riportata sulla pensilina (20 × 86×75 / 128² = 7,87 → 8). Banca invariata: 20, stessa posizione, origine (-64,-64), z 2, curva 0,55.
  • Interactable.gd:2403-2416 — nasce e muore con il bagliore della fermata: la guardia _fermata_glow != null impedisce il doppio montaggio, _smonta_bagliore_fermata() libera anche il glitter. Nodi rimasti dopo riscossione / arco spento / fine run / rigenerazione: 0/0/0/0 (sonda files/homeless_city/scripts/tools/probe_su952_metro_glow.gd, rilanciata dal thread principale: SU952_OK).
  • ✅ Provino a finestra dal thread principale (il gate Codex non può aprire la finestra): TMP/su952_ko/su952_fermata_oro.png e TMP/su952_ko/su952_banca_oro.png, guardati — le + si leggono su entrambe, stesso oro. shot_su952_metro_glow.gd ora aspetta i due glitter e li ferma a metà respiro.
  • NON PROVATO su device.

Il puntino dell'avversario non sta più «sempre a destra» (SU-955, KO)2026-09-13

KO di Ivan del 13/09: «ora il puntino è fermo, ma è shiftato a destra rispetto a dove si trova il barbone, direi di un palazzo circa». Il giro del 12/09 aveva provato che il puntino sta fermo, non verso dove sbaglia. Lotto Codex C955 (sol), gate K955.

  • Il «sempre a destra» era vero, ed era locale: il campo del 12/09 variava con coefficienti così piccoli (0,0025/0,00175 per px) che entro 300 px lo scarto aveva quasi sempre lo stesso verso — risultante media dei versi 0,898 su 1. In un incontro, dove l'avversario resta nello stesso isolato, si vedeva un solo verso. Su tutta la città l'addensamento era più debole (media vettoriale/raggio 0,026-0,184 per peer) e per questo una misura globale non l'avrebbe preso.
  • files/homeless_city/scripts/systems/PowerUpSystem.gd:642-649 — campo spaziale continuo a frequenze più fitte, senza più la cella arrotondata da 16 px (SIXTH_SENSE_BLUR_STEP_PX tolta): vicinato di 300 px 0,044, istogramma degli 8 versi 279-295 su 2304 posizioni × 3 peer (prima 243-355), media vettoriale/raggio 0,001-0,016. Firma, chiave, ampiezza per livello (10-100% di sixth_sense_blur_px(), media 55%) e zero rete invariati.
  • ✅ Vincoli del primo giro rimisurati: fermo 10 s 0,000000 px; mappa e freccia stesso punto (delta 0); due avversari nello stesso punto a 305,6 px e 126° di distanza.
  • ⚠️ Il prezzo, dichiarato: perché il verso cambi dentro 300 px, in movimento lo scarto deve girare più in fretta — salto massimo 3,5 px per px percorso (prima ~0,7 in media, con scalini da 11,2 px ai bordi delle celle). Non è eliminabile: decorrelare in 300 px con un raggio medio di 154 px vuol dire ~2 px di spostamento per px. Se su device il puntino di chi cammina sembra nervoso, la manopola è la frequenza, e si torna a vedere un verso dominante.
  • 📌 Resta per scelta del ticket: da fermo il puntino sta in media a 154 px dalla posizione vera (max 280) — «circa un palazzo» — ma in un verso che cambia da zona a zona. Se è l'ampiezza a sembrare un errore, è SU-362/SU-380, non questo ticket.
  • ✅ Controprova del disegno: con lo scarto forzato a zero il puntino cade sulla posizione vera (256 posizioni, errore 0 px): il «destra» non veniva da MapOverlay.gd.
  • Sonda files/homeless_city/scripts/tools/probe_su955_verso.gd, rilanciata dal thread principale: ESITO=OK.
  • NON PROVATO: partita online vera sui due telefoni.

Il fantasma si gira, e da nemico posseduto si usano i suoi poteri (SU-949, SU-948)2026-09-12

Due ticket dal collaudo di Ivan del 12/09 sera, EOS vero sui due telefoni. Nessuna RPC nuova e nessuna firma cambiata: il minimo multiplayer non si alza — verificato con la guardia di SU-947, scritta stamattina, che su questo diff non stampa niente.

SU-949 — «quando mi giravo in varie direzioni guardava sempre in basso». Non era nessuno dei due sospetti scritti nel ticket: il verso viene scelto dalla velocità e viaggia in rete, misurati entrambi verdi.

  • La causa era una terza, e spiega anche il «sempre»: al fantasma ci si arriva quasi sempre prendendole. react_to_hit() arma _hit_react_left (0,5 s), ma il fantasma esce da _physics_process nel ramo _ghost_process, che non chiama _update_hit_react(): il timer non scende mai più, e la guardia in cima a _play_dir_anim() (files/homeless_city/scripts/player/Player.gd:3781) blocca ogni cambio di animazione per sempre. _last_dir resta "s" — guardare in basso — e i puppet copiano fedelmente il blocco. Ecco perché lo si vedeva dall'host sul fantasma di un client: era vero su entrambi gli schermi.
  • ✅ Fix in Player.gd:1327 (_apply_ghost_visuals() spegne posa, contraccolpo, lampeggio e _net_last_anim) e :1800 (show_hit_reaction_fx() esce su is_ghost: è il ramo di rete, reliable, e poteva riarmare il blocco).
  • Misura: giro «colpo poi collasso», 7 versi sbagliati su 8 prima, 0 su 8 dopo; 24 casi su 24 con locale e puppet allineati.
  • 📌 Il criterio 3 del ticket non ha oggetto: il fantasma non esiste in single player.

SU-948 — «sono diventato uno yeti, ma a quel punto il tasto x deve diventare l'attacco dello yeti».

  • X = il colpo del nemico (−1 vita, causa yeti, ricarica 4,0 s — la stessa dello yeti guidato dall'IA, non una nuova), Y = lo scatto. Y e non «super» perché il pulsante S a schermo compare solo con un super battezzato. Da posseduti il Player ha set_physics_process(false): entrambi i tasti sono liberi.
  • Lo scatto del fenicottero non esisteva e nasce qui: 181 px/s misurati per 1,5 s contro i 100 px/s di andatura, ricarica 8,0 s (CICLO_SEC, l'unico tempo proprio che ha). Secondo scatto rifiutato a 7,5 s, accettato a 8,5 s.
  • Il recinto non trattiene il posseduto: l'IA resta dentro (0 frame su 360 fuori dal parco, invariata), da posseduto esce fino a 384 px oltre il bordo, e a possessione finita rientra nello stesso frame senza incastrarsi — nei 5 s dopo percorre 102 px restando dentro. Il rientro è il ricalcolo di una funzione pura (ParkFoe.rientra_in_zona()), non una camminata: stesso risultato su ogni peer. Era la trappola già pagata in SU-886, un NPC guidato fuori dal mondo.
  • Lo scatto viaggia come codice nuovo su una RPC che esiste già (FOE_INPUT_DASH = 12 su _srv_foe_input): una build vecchia cade fuori da tutti i rami e lo ignora. È il motivo per cui il minimo MP resta dov'è.
  • ⚠️ Trovato dalla review: la ricarica dello yeti non si armava sull'host per le vittime remote. L'host assegnava il punto senza eseguire _colpo_riuscito(), quindi a tre peer si poteva colpire B e poi C entro i 4 s, aggirando proprio il numero che il ticket chiedeva di rispettare. Corretto staccando la ricarica dalla *festa* del colpo — posa, grugnito, messaggione, che appartengono a chi l'ha subito — e mettendola in arma_ricarica_colpo() (files/homeless_city/scripts/world/Yeti.gd:118), che l'host arma sulla propria copia e impone alle altre (World.gd:11290). Misura: puo_ferire true → false, _cd_colpo 4,0 s, posa 0,0 s — la ricarica viaggia senza il messaggio in faccia a chi non è stato colpito — e riapre dopo 4,2 s.
  • ⚠️ Trovato dalla review: la mira orientava solo la posa. La distanza di contatto è circolare da sempre, identica per l'IA, e a 22 px «alle spalle» non significa granché; ma i 10 px di portata in più del colpo volontario (POSSESSION_STRIKE_EXTRA) erano nuovi di questo ticket, e ora si concedono solo davanti (prodotto scalare fra mira e bersaglio, DistrictFoe.gd:617). Misura: davanti a 28 px −1 vita, alle spalle a 28 px −0, alle spalle a 18 px (contatto vero) −1. Il contatto dell'IA resta byte per byte quello di prima.
  • 📌 Un rilievo della review verificato e NON corretto, di proposito: il danno lo toglie il peer della vittima (GameState.lose_life), non l'host, che convalida solo il punteggio. È vero — ma è il modello di tutto il gioco, scritto come regola in DistrictFoe.gd:40 («le vite sono per-peer, nessun relay») e usato dall'IA e dal posseduto attraverso la stessa identica funzione prova_contatto(). Allineare il solo posseduto avrebbe voluto dire riscrivere il danno dei nemici IA, che il ticket vieta espressamente di toccare. Resta un limite preesistente, dichiarato.
  • ⚠️ Non provato: due peer veri (la sonda è a processo singolo, quindi il criterio 5 è dimostrato sul meccanismo e non in partita) e il colpo su un telefono. 📌 Manca un pezzo di UI: l'HUD non dice che da posseduti X colpisce e Y scatta — serve una chiave I18n nuova su 8 colonne, fuori dal perimetro di questo lotto.

La musica del bonus: due piste escluse coi numeri, e il difetto resta da riprodurre (SU-954, nessuna cura)2026-09-12

Ivan: «si è interrotta la musica nel bonus stage se esce la modalità seleziona carta con tante carte». Due giri di misura, nessuna riga di codice di gioco toccata: il vincolo era che senza vedere il difetto nei numeri la cura non si scrive, e non si è visto.

  • Esclusa: il brano che finisce. bonus_stage.ogg.import ha loop=true, il giro lo fa il motore.
  • Esclusa: il ventaglio che ferma il lettore. Primo giro, sonda che entra nel BonusLevel vero e usa il LevelUpScreen vero: pool reale da 10 carte e stress fino a 16, in tutti i campioni playing=true, stream_paused=false, process_mode=3, posizione che avanza senza buchi. Nessuna soglia di carte trovata.
  • Esclusa: il bus Music, che era l'ipotesi migliore. Veniva da un consulto: il bonus passa per un pitch-shift e un passa-basso globali che scrive World, e in SP World si congela entrando nel tunnel — se un parametro restasse su un valore estremo, la musica sarebbe inudibile pur suonando, e nessuno dei campi campionati se ne accorgerebbe. Misurato con la chiusura vera della carta e altri 6 s di osservazione: 8 run, 33.688 campioni, in condizione normale, «caldo», «kazoo» e mista, fino a 32 carte e con un'attesa da 150 s. pitch_scale e cutoff_hz restano coerenti prima, durante e dopo; Music sempre 0 dB, mai mute, mai solo; picchi fra −28,0 e −4,4 dB e zero campioni sotto −80 dB; la musica della città riprende in 8 run su 8.
  • 📌 Un difetto della prima sonda, trovato dal consulto: i campioni «dopo» non erano dopo la chiusura del ventaglio — la carta non veniva mai confermata. Il momento in cui il difetto si manifesterebbe non era mai stato osservato. Ora la sonda conferma davvero e continua a guardare.
  • ⚠️ Il limite che resta, ed è quello che conta: in headless il driver audio è Dummy, e i picchi misurano il mixer, non l'uscita fisica. Un'interruzione che nasce nel backend audio del dispositivo non è visibile da qui. Il ticket resta aperto in attesa delle condizioni esatte in cui Ivan l'ha sentita.
  • ✅ Resta a repo la sonda files/homeless_city/scripts/tools/probe_su954_bonus_cards.gd, che ora sa entrare nel bonus vero, aprire il ventaglio, confermare una carta e campionare per fotogramma i due lettori e tutti i parametri dei bus: è lo strumento pronto per quando ci sarà il caso vero.

Brina: lo strumento per sapere quale voce costa, e perché il Mac non basta (SU-957)2026-09-12

Ivan, dal collaudo: «performance honor a brina, indagare, rispetto a centro sono molto più basse». Brina ha sette cose che il Centro non ha — neve_sempre, parchi_ghiaccio, marciapiedi_ghiaccio, pinguini, yeti, pupazzi, torce — e il ticket serve a dire quale costa, col numero. Questo lotto costruisce lo strumento; il verdetto lo darà la misura sull'Honor, che deve fare Ivan.

  • Un interruttore per voce, da riga di comando (files/homeless_city/scripts/autoload/Quartieri.gd:773,791,914): --su957-off=<voce> spegne una delle sette lasciando accese le altre sei, senza ricompilare e senza modificare file.
  • La porta di release è chiusa a doppia mandata, ed era la trappola da evitare (il 10/09 una feature di misura stava per finire in una build da store): serve OS.is_debug_build() e il flag esplicito --su957-probe. Senza entrambi, spegnimento e seme sono azzerati e inerti — una build release non può attivarla nemmeno ricevendo per errore gli stessi extra dalla riga di comando di Android.
  • Il seme non cambia quando si spegne una voce, ed è il punto più delicato del lotto: se togliendo i pinguini si spostasse tutto il resto, le due prove non sarebbero confrontabili e la misura non varrebbe niente. Verificato su 18 avvii: seed 9570912 e base_sig=06d7dee8fec1 identici in ogni Brina; spegnendo una specie si svuota solo la sua firma, le altre quattro non si muovono. Serve una riga in WorldGenerator.generate() (:872) che chiede il seme all'autoload prima del primo tiro — dietro la stessa doppia guardia, quindi il ramo storico del gioco normale è intatto.
  • ⚠️ Il Mac non sa separare le voci, ed è un risultato, non un fallimento. Con gli fps a soffitto (~145, e su questo Mac il valore assoluto mente comunque) l'1% low dà oscillazioni e segni contrari: spegnere i marciapiedi di ghiaccio darebbe +7,11%, spegnere i pupazzi −19,90% — cioè togliere roba peggiorerebbe, che non è un risultato fisico ma rumore. Le sette voci vanno misurate sul telefono. Tabella grezza in TMP/su957/su957_results.tsv.
  • ⚠️ NON MISURATO sull'Honor 10: il criterio 1 e il verdetto del ticket restano aperti. Il comando pronto, con l'ordine A-B-A completo su 18 prove da 60 s, è nel commento del ticket.

Anche la fermata della metro brilla d'oro quando il premio è acceso (SU-952)2026-09-12

Ivan: «deve brillare anche la metro come la banca al raggiungimento del deposito (quando è dorata insomma)». La fermata sapeva già di essere la porta del bonus (_arco_banca_attivo) e riceveva l'arcobaleno, ma non si accendeva.

  • Lo stesso bagliore della banca, montato sulla fermata (files/homeless_city/scripts/world/Interactable.gd:2337): secondo Sprite2D additivo figlio di FermataOro, stesse costanti BANCA_GLOW_* — stessa tinta (1,000, 0,860, 0,350), stesso alpha 0,450, stesso respiro da 1,6 s. Figlio dello sprite esistente per ereditare ritaglio, posizione e z-order già collaudati.
  • Una fermata accesa per volta: l'unicità resta quella di _arco_banca_attivo, senza una seconda selezione della metro — due fari direbbero due premi.
  • Nessun nodo appeso (criterio 3): sonda headless, BaglioreFermataPremio prima/dopo la riscossione 1 / 0, e zero anche dopo spegni_arcobaleno_banca(). Il mondo che si rigenera non lascia orfani.
  • ⚠️ «Lo stesso oro» è stato misurato, non guardato — e la risposta è "quasi". Al picco del respiro, delta RGB8 rispetto allo stesso pixel a bagliore spento: banca (29,90 / 26,45 / 10,40), luma 26,02 su 8577 pixel variati; fermata (34,60 / 36,64 / 7,55), luma 34,11 su 4238. La fermata rende quindi il 31% più forte. Non è il bagliore a essere diverso — tinta, alpha e respiro sono identici, misurati entrambi al picco 1,000000 — è la base: la banca somma sulla propria facciata avorio, la fermata su uno sprite FermataOro già dorato di suo (FERMATA_ORO_TONALITA 0,125, SATURAZIONE 0,80, VALORE_MUL 1,05, VALORE_ADD 0,28). Lasciato com'è: correggerlo significherebbe scostarsi dal «stesso bagliore della banca» che il ticket chiede. Decide Ivan guardando.
  • 📌 Tre giri sul provino, e nessuno dei tre difetti era nel codice del gioco. (1) Il primo scatto fotografava una run appena nata, $0 in HUD e la banca lontana: la condizione non era forzata affatto. (2) Il secondo aveva le guardie giuste ma le ROI della misura erano calcolate in coordinate del viewport logico e lette su un'immagine con dimensioni vere diverse — campionava x=915 su un buffer largo 844, e «nessun pixel variato» era un artefatto del guardare fuori dal quadro. ⚠️ Su questo Mac --windowed non rispetta la risoluzione richiesta: le dimensioni si leggono dall'Image, mai da quello che si è passato sulla riga di comando. (3) Solo al terzo la misura ha risposto. Scatti in TMP/su952/.

Il tremolio nel movimento laterale: misurato, e il verdetto è la camera (SU-950, sola misura)2026-09-12

Ivan aveva chiesto di verificare, non di curare: prima la misura, poi il verdetto se è un difetto o il prezzo di una scelta già fatta. Nessun file del gioco è stato toccato — c'è solo una sonda nuova, files/homeless_city/scripts/tools/su950_tremolio.gd, che carica Main.tscn vero e campiona ogni fotogramma reale (niente --fixed-fps, che avrebbe cancellato proprio il disallineamento fisica/render che stiamo cercando).

  • Da fermi non tremola niente: 0,000 px su barbone, testo-mondo, testo-HUD e camera, a entrambe le risoluzioni. La causa sta nel movimento.
  • In movimento, in pixel di schermo: barbone e testo-mondo 2,725 px sul tablet (1024×768, scala 1,0) e 1,459 px sul telefono (844×390, scala 0,5078); testo-HUD 0,000 px in tutti i casi; camera reale (dopo lo smoothing) 15,515 e 7,158 px, cioè 5-6 volte il barbone, e non sta ferma quasi mai — un salto ogni 1,01 fotogrammi contro 1,8 del barbone.
  • 📌 Quali scritte tremolano, che era una delle domande del ticket: quelle nel mondo (figlie del barbone, numeri byte-identici ai suoi), non quelle dell'HUD, che sul suo CanvasLayer non si muove di un millesimo. Sono due strade diverse e solo una è in gioco.
  • Verdetto: causa 1, la camera. Lo position_smoothing_speed a 6,0 amplifica il difetto invece di specchiarlo: la camera oscilla cinque volte più del bersaglio che insegue. Trovato anche fuori dalle tre cause candidate: Engine.get_physics_frames() mostra ~45% dei fotogrammi renderizzati senza un nuovo tick fisico e il 4-5% con due o tre insieme — fisica e rendering fuori passo, a monte della camera.
  • ⚠️ Una conclusione intermedia corretta in corsa, e vale la pena saperlo. La causa 2 (scala del content scale non intera) era stata esclusa perché «stessi numeri alle due risoluzioni»: l'ampiezza era però misurata in coordinate logiche, dove non poteva cambiare per costruzione. Rifatti i conti in pixel di schermo, la causa 2 resta esclusa ma per una ragione vera: il rapporto fra le due risoluzioni (1,87× sul barbone, 2,17× sulla camera) segue il rapporto delle scale (1,97×) entro il rumore, senza nessun salto anomalo attribuibile al non-intero. 📌 È lo stesso inganno di SU-895: una verifica che passa anche in un mondo dove il difetto c'è non ha dimostrato niente.
  • ⚠️ Non misurato: la percezione visiva (ci sono i numeri e sei scatti per risoluzione, in TMP/su950/), e il comportamento con lo snap 2D riacceso — che resta una decisione di Ivan, non una cosa da provare di nostra iniziativa. Una sola run per risoluzione: il rapporto osservato è dentro il rumore di un campione singolo, non una media.

Al game over sparisce tutto quello che c'era sopra, e i punti online seguono ogni moneta (SU-958, SU-956)2026-09-12

Due difetti visti da Ivan nel collaudo online del 12/09. Stesso file, stesso lotto.

SU-958 — «se si è in pausa o si sceglie una carta e si muore rimane in sovrimpressione». In MP né la pausa né il ventaglio fermano il mondo per gli altri, quindi si può collassare con le carte ancora in mano: _on_game_over() chiudeva solo la mappa, e sul ramo «restano vivi» nessuno chiudeva il resto.

  • Pausa e ventaglio si chiudono nel punto più precoce, accanto alla mappa e prima dello smistamento fra i rami (files/homeless_city/scripts/ui/HUD.gd:6272-6293): vale per MP e SP insieme, che è ciò che il ticket chiede con «a prescindere».
  • Nessuna carta assegnata a caso: il nuovo LevelUpScreen.close_for_game_over() non chiama XPSystem.resolve_level_up() né applica nulla — il level-up resta pendente. Sonda: has_pending_level_up è true prima e dopo il game over.
  • La musica torna com'era: process_mode da ALWAYS (3) a PAUSABLE (0) dopo la chiusura, con l'ordine di SU-842 — prima riparte l'albero, poi si ripristina. Nessun brano sopra la fanfara.
  • ⚠️ Trovato dalla review, non dal builder: la pagina Opzioni restava aperta. Morendo dentro Pausa → Opzioni, _opt_panel e l'avviso MP sarebbero rimasti sopra il giornale — il blocco chiudeva la pausa ma non le sue sottopagine. Ora chiama _chiudi_opzioni() per primo, nello stesso ordine di _toggle_pause().
  • ⚠️ Trovato dalla review: la chiusura poteva essere annullata da sé stessa. L'uscita anticipata su _open == false non fermava un'apertura già accodata con call_deferred da _resolve(): se il game over arrivava nello stesso fotogramma, le carte riapparivano dopo. Ora un flag «run finita per me» si accende in testa alla chiusura, prima di ogni controllo, e rende inerti _open_ventaglio() e _open_next_forced().
  • 📌 Un rilievo della review verificato e smentito: _forced_queue non può attraversare due run, perché _cl_game_starting cambia scena e ogni run nasce con un LevelUpScreen nuovo. Svuotata comunque alla chiusura, per non tenere in memoria carte promesse a una run finita.

SU-956 — «online i punti non salgono ad ogni moneta che raccogli». Prima la misura: la raccolta in MP funzionava già, il difetto era la cadenza della classifica in partita.

  • I dieci numeri del criterio 1, sonda a due peer ENet, cinque monete una alla volta. Riga grezza di classifica PRIMA/DOPO: [0, 0, 0, 0, 6, 6, 6, 6, 12, 12] — resta ferma per due o tre monete di fila. Punteggio locale nello stesso intervallo: 0 → 3 → 6 → 9 → 12 → 15, sale a ogni moneta. Il difetto è la fotografia dell'host, vecchia fino a ~4 s (il client dichiara ogni 2 s se cambia, l'host ripubblica ogni 2 s).
  • La propria riga si disegna dal punteggio locale (HUD.gd:2734-2750), e si ridisegna su ScoreSystem.score_changed, che parte sincrono da add_money() (HUD.gd:794-804): dopo la cura la riga a schermo dice «15» nello stesso fotogramma della raccolta.
  • Per gli altri non si inventa niente: la riga dell'host resta la sua fotografia (misurato: grezza 0, testo «2. P07 0»). Zero pacchetti nuovi, zero RPC, NET_SCORE_RATE invariato — è solo un ridisegno locale, che esce subito se il pannello non è a schermo o si è in SP.
  • ⚠️ Limite noto, ed è voluto dal ticket: si aggiorna il valore della propria riga, non l'ordine né il taglio dei primi quattro, che restano della fotografia dell'host. Per qualche secondo si può quindi vedere un punteggio più alto sotto uno più basso. Riordinare in locale significherebbe inventare la posizione degli altri, che il ticket vieta esplicitamente.
  • ⚠️ Non provato: lo scatto a due peer di SU-958 (la sonda è a processo singolo e chiama _on_game_over direttamente) e la raccolta con la moneta fisica in SU-956 (la sonda passa da add_money()).

In lobby il guest vede il quartiere scelto dall'host, e lo vede cambiare (SU-941, rilavorazione del KO)2026-09-12

KO di Ivan del 12/09 sera, EOS vero sui due telefoni: «ok ma non si aggiorna durante la lobby sul guest il quartiere, lo vedo sull'host (brina) ma non sul guest (centro)». La partita partiva già nel quartiere giusto — quello era provato a due peer l'11/09 — ma chi entrava non sapeva dove stesse andando finché non partiva.

  • 📌 Cosa era stato capito male: il ticket diceva «l'host decide il livello», e il primo giro l'ha letto come una regola sull'avvio della partita. Era anche una regola sulla lobby: la scelta dell'host è un'informazione che gli altri devono vedere mentre aspettano.
  • Causa, trovata prima del rimedio: MainMenu._refresh_mp_lobby_quartiere() calcolava l'etichetta del guest da _lobby_quartiere_idx, una variabile locale inizializzata una volta sola dalla Quartieri.selected_id del guest stesso e mai toccata da quello che faceva l'host. Nessun campo del registro di lobby portava quella scelta al client.
  • Nessuna RPC nuova e nessuna firma cambiata: la scelta viaggia come chiave "quartiere" dentro NetworkManager.players[1], che è già nel payload di _cl_lobby_state/_broadcast_lobby esattamente come unlocked e skin. Il minimo multiplayer non si muove per questo ticket. Nuovo set_lobby_quartiere(id), host-only.
  • Sonda a due peer ENet veri (files/homeless_city/scripts/tools/su941_lobby_ko_mp.gd) che legge il testo della label, non le variabili interne — cioè quello che un giocatore vede: guest «QUARTIERE: IL CENTRO» → l'host sceglie Brina → guest «QUARTIERE: BRINA» → l'host cambia a Mercato restando in lobby → guest «QUARTIERE: IL MERCATO». falliti=0 su entrambi i processi, rilanciata dall'orchestratore su un ambiente pulito.
  • ⚠️ La sonda non era riproducibile da zero, ed è stata la scoperta più utile del lotto. Passava solo contro la copia di progetto di tools/autotest/common.sh, da cui force_enet toglie eos_credentials.cfg. Sul progetto vero le credenziali ci sono, EOSBridge.available() torna true, e l'host prende la strada EOS invece di ENet: resta appeso al login (NotFound) e non arriva mai in lobby — nessuna riga di esito, e sembra un fallimento del fix. Si gira con SU911_PERF=1, la leva di SU-911 che spegne piattaforma e login in una build di debug, ed è ora scritto nella docstring della sonda.
  • ⚠️ Non provato: EOS vero (solo ENet), più di due peer, e l'avvio partita dopo il cambio in lobby (criterio 1, già passato l'11/09 e non toccato).

La guardia RPC della release vede anche i cambi di firma, non solo i nomi nuovi (SU-947)2026-09-12

Nato al cancello del giro del mattino: SU-946 aveva aggiunto un terzo argomento a _cl_busk_result, e la guardia rg_guardia_rpc_mp non aveva detto niente — confrontava solo l'elenco dei nomi. Nessun id RPC slitta (in Godot 4 l'id è l'indice del nome nell'ordine alfabetico, SU-895), ma una build vecchia che riceve un pacchetto a tre argomenti lo scarta in silenzio: nel caso di SU-946 erano le monete del COMIZIO IN PIAZZA che non arrivavano al client.

  • La guardia confronta ora la FIRMA — nome, numero di argomenti e i tipi dove sono dichiarati (tools/release/release_git_lib.sh:77,100,207,243), e classifica separatamente nuove, tolte, rinominate e firma cambiata. Una rinomina è dedotta solo con firma unica 1:1. Il parser regge dichiarazioni su più righe, commenti, stringhe e default con virgole; nomi e valori di default non entrano nella firma.
  • Una firma cambiata pesa come una RPC nuova: il testo dell'avviso dice che la versione minima MP va alzata alla versione che esce, e spiega i due guasti distinti — id che slittano, pacchetto scartato — entrambi silenziosi.
  • Controprova rossa sulla storia vera (non su una fixture): su ad88cb6 la guardia vecchia non stampa niente, la nuova stampa _cl_busk_result (2 argomenti: int, PackedInt32Array) -> _cl_busk_result (3 argomenti: int, PackedInt32Array, bool). Un tag confrontato con se stesso continua a non stampare nulla.
  • 📌 Una premessa del ticket smentita dalla misura. Il criterio 3 parlava di «25 RPC nuove di World.gd e 13 di NetworkManager.gd» fra v0.43 e dev: i numeri veri sono 106 e 23. Quello che conta — nessun falso positivo — regge comunque, e in modo più forte di come era chiesto: vecchia e nuova guardia producono insiemi identici su quel tratto (106 World, 23 NetworkManager, 1 Player, 1 sonda), e l'unica differenza è l'etichetta «aggiunte» → «nuove». Firme cambiate: 0.
  • 📌 Osservazione, non difetto: fra le RPC contate c'è anche scripts/tools/su879_relay_nodo.gd, che è una sonda e non codice di gioco. Vale identico prima e dopo questa modifica, quindi non cambia nulla per la release; se un giorno desse fastidio, il posto da toccare è la selezione dei file, non il confronto delle firme.
  • ⚠️ Non provato: prepare_release.sh non è stato lanciato (è il giro di release e tocca git); la guardia è stata chiamata da sola.

Il puntino di un avversario fermo adesso sta fermo (SU-955)2026-09-12

Ivan, dal collaudo online del 12/09: «se l'altro giocatore rimane fermo sul posto, sulla mappa comincia a girare il suo puntino su radar di quartiere a caso». Lo scarto del fiuto era una funzione del tempo (ang = ph + t * 0.35): ruotava attorno alla posizione vera anche quando quella non cambiava. Il «vagare lento» di SU-362 è giusto su un bersaglio che si muove, su uno fermo diventa un'orbita che il giocatore non ha chiesto.

  • Lo scarto ora dipende dalla POSIZIONE del bersaglio, non dall'orologio (files/homeless_city/scripts/systems/PowerUpSystem.gd:635-646): posizione arrotondata a celle da 16 px (SIXTH_SENSE_BLUR_STEP_PX), fase e raggio da lì, la chiave del peer dov'era. L'imprecisione resta — è un radar di quartiere, non un GPS — ma smette di muoversi da sola. Fermo 10 s: spostamento totale 0,000000 px. In movimento lo scarto cambia ancora (percorso 186,410 px) e resta dentro il limite del livello (raggio massimo 105,721 px su 280,000).
  • ⚠️ Trovato al cancello, non dal builder: la freccia a schermo e la cartina avrebbero raccontato due posti diversi. World.gd:1637 chiama la stessa funzione ma non le passava nessuna posizione, e il parametro aveva un default Vector2.ZERO: la freccia avrebbe calcolato lo scarto sul punto (0,0), sempre lo stesso, mentre la cartina lo calcolava sulla posizione vera. La sonda del builder non poteva vederlo, perché simulava la freccia passandole l'argomento giusto — cioè misurava un mondo diverso da quello che gira.
  • Le due strade ora leggono la stessa sorgente: la cartina prende _net_target_pos quando c'è, e da oggi lo fa anche la freccia (files/homeless_city/scripts/world/World.gd:1637-1647) — la global_position è interpolata e sarebbe caduta in un'altra cella da 16 px. Delta misurato fra le due: 0,000000 px.
  • Il default è stato tolto del tutto (PowerUpSystem.gd:640): senza, un chiamante futuro che si dimentica la posizione non compila, invece di sbagliare in silenzio. Aggiornata di conseguenza la sonda di SU-362, che chiamava con la sola chiave.
  • ✅ Invariati: l'ampiezza per livello (SU-362/SU-380) e il fatto che tutto sia locale — zero rete, nessun pacchetto e nessuna RPC. Due avversari diversi sbagliano ancora in direzioni diverse: 333,597 px di distanza fra gli scarti, 165,284° fra le direzioni.
  • ⚠️ Non provato in una partita online vera: le misure vengono da una sonda headless.

Entrati in banchina, i binari si chiudono alle spalle (SU-951)2026-09-12

Ivan, dal collaudo del 12/09: «quando arrivo sulla banchina del bonus stage non devo più tornare sui binari, blocco pre banchina». Si poteva camminare a sinistra e riscendere nel corridoio.

  • Il muro sinistro ora dipende dalla fase come già faceva il destro (files/homeless_city/scripts/world/BonusLevel.gd:2825-2830): _x_traguardo in Fase.BANCHINA, _x_partenza - 48 nel corridoio. Un clamp solo, nello stesso punto: il movimento orizzontale resta con un'autorità sola invece di dividersi in due strade. È anche ciò che l'enum prometteva già a parole — «da BANCHINA non si torna MAI in CORRIDOIO» — e che il codice non faceva.
  • Sonda deterministica, 180 frame (3 s) di sinistra a fondo per ciascuna fase (files/homeless_city/scripts/tools/probe_su951_banchina.gd): minimo in banchina 4210,000 px, esattamente _x_traguardo; minimo nel corridoio 112,000 px, esattamente _x_partenza - 48. Il corridoio non è stato ristretto per sbaglio: è la seconda misura a dirlo, non l'assenza di una lamentela.
  • ✅ Il salto di 47 px nell'istante dell'ingresso in banchina resta identico; _entra_in_banchina(), la pulizia dei topi e il tetto del corridoio non sono toccati (il patch vive fra le righe 2822 e 2836).
  • ⚠️ Non provato col gesto vero su device: la sonda pilota l'ingresso in banchina invece di arrivarci camminando. Resta a Ivan premere sinistra per tre secondi appena entrato.

Il timbro del controllore: dal quarto giro di metro si collezionano i cosmetici della Baracca (SU-782)2026-09-12

Il premio C di SU-768, aperto da Ivan oggi («A2 sì»). Ogni vittoria dal giro 4 del bonus metro aggiunge un timbro persistente; a 3, 7 e 15 timbri si apre un cosmetico esclusivo della Baracca, senza alcun effetto di gioco.

  • Il contatore è una chiave dentro counters (files/homeless_city/scripts/autoload/MetaProgress.gd), non un file nuovo: così persistenza, azzeramento da RESET PROGRESSI e fusione cloud col massimo arrivano gratis dal macchinario che c'era già.
  • I due cancelli — vittoria e giro ≥ 4 — stanno dentro MetaProgress, non nel chiamante: è questo che rende «una sconfitta non timbra» una misura e non una deduzione. Sonda: 15 vittorie dal tier 4 = 15 timbri; sconfitte ai giri 4/7/20 e vittorie ai giri 1/2/3 = zero timbri; il contatore riletto da un processo nuovo.
  • I tre cosmetici NON stanno in SKINS, e questa è la scelta portante: exists() è falso, quindi buy(), unlock(), unlock_all(), buy_golden() e select() li rifiutano alla prima riga, senza aggiungere un solo if all'economia a lattine e senza toccare gifted/paid di SU-935/SU-937. Provato: acquisti falliti col saldo di 5.000 lattine intatto, e il negozio «solo su invito» costruito per davvero espone 5 voci, nessuna del timbro.
  • ⚠️ Trovato dalla review: l'ordine delle due scritture era quello sbagliato dei due. Lo sblocco andava in skins.cfg prima che il contatore finisse in meta.cfg: un arresto nel mezzo lasciava un cosmetico posseduto sotto soglia, e la riconciliazione sa solo aggiungere — quindi l'incoerenza era definitiva. Invertito: ora la finestra produce l'incoerenza opposta (contatore avanti, cosmetico chiuso), che all'avvio si ripara da sé. La sonda ricostruisce la finestra di guasto e misura la riparazione.
  • ⚠️ Trovato dalla review: gli identificatori nuovi erano in italiano, contro la convenzione del progetto (testi UI e commenti in italiano, nomi nel codice in inglese). Rinominati tutti e undici. 📌 La chiave su disco resta timbri_controllore: ribattezzare la costante è innocuo, cambiare la stringa avrebbe fatto perdere i timbri già salvati in silenzio — la sonda ora la scrive per nome, così un domani quel cambio fallisce subito invece di far danni.
  • 📌 Limite noto dichiarato, non un dimenticanza: i tre riquadri rispondono a tocco e clic, non alla navigazione con pad o tastiera. Aggiungerli alla navigazione sposta il significato di «selected == ids.size()» in quattro punti del menu, e non si fa in coda a un lotto.
  • 📌 L'arte non c'è ancora: i posti sono pronti (stamp_cap, stamp_puncher, stamp_pass, stamp_mark) e stamp_icon() passa da ResourceLoader.exists() tornando null, quindi il riquadro resta vuoto senza un errore in console. I quattro sprite sono in generazione su Astra e li approva Ivan. Scatti: TMP/su782/scatti/.

La classifica in partita perde il cartone e passa sul nero (SU-878)2026-09-12

Deciso in chat da Ivan dopo la prova su device dell'11/09 («878 classifica funziona, per la grafica ci penso su»): *«toglierei il colore cartone e la farei sul nero o sul nero trasparente»*. Il ticket funzionava già: cambia solo la resa.

  • Nero traslucido al 65%, non nero pieno (LIVE_RANKING_BG_COLOR e lo StyleBoxFlat al posto della variazione di tema CardboardPanel, files/homeless_city/scripts/ui/HUD.gd:159 e :2627-2640). La scelta fra i due neri è stata fatta guardando lo scatto: sopra la città in movimento il 65% resta leggibile senza diventare un tassello opaco. Nessuno shader e nessuna sfocatura — un overlay traslucido a tutto schermo è un collo di bottiglia noto su mobile, e qui basta un rettangolo piatto.
  • ✅ La riga propria resta distinguibile senza toccare niente: il suo fondo blu (0.16,0.46,0.72 all'82%) contro il nero al 34% delle altre. Il testo era già chiaro e non è stato schiarito.
  • ✅ Invariati, come chiede il ticket: il tween dello scivolamento quando cambia l'ordine, i segni «in metro / fantasma / scappato», l'RPC e il payload, il timer, il punteggio grande, il tasto pausa e il banner spettatore che legge dalla stessa lista.
  • 📌 Il confronto prima/dopo è valido sul colore, non sulla geometria: lo scatto «prima» del telefono è uscito a 4288×2412 (risoluzione nativa) invece che a 844×390, mentre i due del tablet sono entrambi 1024×768. I rettangoli delle sovrapposizioni sono comunque misurati dalla sonda, e il layout non è cambiato — solo il colore. Scatti in TMP/su878/prima/ e TMP/su878/dopo/.

La scritta delle lattine rientra nel riquadro, il cambio nome non si apre più per abitudine, e il pannello del gate dice la verità (SU-944, SU-945, SU-940)2026-09-12

  • SU-944 — la premessa del ticket era sbagliata, e l'ha detto la misura. A sforare non è «LATTINE INSUFFICIENTI» (588,0 px contro una scatola di 583,7, +4,3) ma «SBLOCCA (150 LATTINE)» in francese: 671,0 px, +87,3 fuori — cioè il caso su cui il pulsante era considerato tarato. Rimedio: il font della sola _skin_confirm_lbl × 0,8 (files/homeless_city/scripts/ui/MainMenu.gd:10437-10443), non l'accorciamento del testo — la chiave di questo ticket era MENU_SKIN_SBLOCCA_NO, e accorciare quella non avrebbe chiuso niente. Dopo: peggiore 541,0 px, tutti e tre gli stati in tutte e 8 le lingue dentro il riquadro, col prefisso «▶ » attivo. Nome, descrizione e frecce non si spostano di un pixel (457,67 / 160,77 prima e dopo). Scatti in TMP/su944/ — il francese prima e dopo è quello che si vede a occhio.
    • 📌 La misura va letta PRIMA di scrivere il testo lungo nel nodo: un Control non scende sotto il suo minimo, quindi a posteriori largh_pulsante si autogonfia e il criterio risulterebbe soddisfatto anche sforando. Sta scritto in testa alla sonda files/homeless_city/scripts/tools/su944_misura_lattine.gd.
    • 📌 Un commento lasciato nel codice diceva «peggiore dopo: 536,8 px»: era il conto a mano 671 × 0,8, e il font si arrotonda a un intero di pixel invece di scalare linearmente. Corretto col numero misurato.
  • SU-945 — la schermata CAMBIA NOME non si apre più solo perché il telefono non ti conosce. _account_needs_public_name() (MainMenu.gd:8082-8130) guardava solo Settings.account_name_claimed, cioè lo stato locale: su un dispositivo nuovo si apriva anche quando nel registro il nome c'era. Ora decide sui cinque casi del ticket, e il più importante è il quinto: registro muto (rete assente, timeout) → la schermata NON si apre, si tiene il nome locale e si riprova al prossimo avvio.
    • Il messaggio dice il motivo (criterio 3), e questo mancava al primo giro: _open_account_nome() azzerava sempre _nome_notice_code, quindi si arrivava davanti al campo senza sapere perché. Ora il gate passa il motivo, riusando i testi già in 8 lingue — nessuna chiave nuova: «Troppo corto: da 3 caratteri in su» per la forma, «Questo nome l'abbiamo già guardato, e la risposta era no» per un nome bocciato.
    • ✅ Sonda files/homeless_city/scripts/tools/su945_gate_nome.gd, registro finto e zero rete: 7 controlli, KO=0, e zero claim nei casi 1, 4 e 5. Controprova rossa: col gate di prima il caso 1 su dispositivo nuovo apriva la schermata.
  • SU-940, parte UI — il pannello mandava a rifare un login che c'era già. Il motivo nuovo restart_required cadeva nel ramo else di _refresh_mp_gate_visuals(): l'utente leggeva «NIENTE FANTASMI IN STRADA / Entra con un account» e la voce apriva la schermata dell'account. Ora ha titolo, corpo e voce propri (MP_GATE_*_RIAVVIO, righe 983-985 di files/homeless_city/assets/translations/ui_menu.csv) e l'azione torna indietro come per la build vecchia, perché qui non c'è nessuna porta da aprire. Misurato sul testo reso, non sul rettangolo: in tedesco — la lingua più lunga — il corpo occupa 100,0 px su 184,3 di area. Scatti in TMP/su940/.
    • 📌 Il canale coperto dal primo giro (join_failed) non era quello che si vede: premendo MULTIPLAYER il menu MP non si apre affatto, quindi quel segnale non veniva nemmeno raggiunto.

Suonare fa salire di livello: le mance valevano zero XP, l'elemosina due (SU-946)2026-09-12

Segnalazione di Ivan: «suonando, il livello non sale come quando si raccolgono le monete da terra». Non era un bug di consegna: le mance del busking passavano da CoinDrop con xp_base = 0, mentre l'elemosina passa XP_BEG = 2. Lo zero era voluto (SU-247 metteva il +1 sul *ciclo*, per non lasciare a zero i cicli in cui nessuno dona), quindi qui si discuteva la quantità.

  • I due numeri che il ticket chiedeva, misurati su 12 semi a tier 0 nel quartiere neutro (sonda files/homeless_city/scripts/tools/su946_busk_xp.gd): PRIMA suonata 6 XP contro elemosina 30 XP — il 20%, cioè il difetto. DOPO 29,6 contro 30,0, il 99%. Il ticket chiedeva la pari con tolleranza 20%.
  • Strada scelta: l'XP sta dentro la mancia, 1 punto per dollaro (XP_BUSK_PER_DOLLAR e l'helper puro busk_tip_xp(), files/homeless_city/scripts/pickups/CoinDrop.gd), accreditato solo alla raccolta come per l'elemosina. Scartato il «+1 per ciclo che cresce»: sul client il numero di mance del ciclo si sa solo dopo, dall'host, e un bonus tardivo sarebbe stata la seconda consegna di XP per ciclo — il doppio conteggio che il criterio 2 vieta. Scartato anche l'XP fisso per mancia: l'host rimanda al client un totale in dollari per tutto il ciclo, quindi un fisso avrebbe pagato una mancia invece di tutte. Per dollaro invece la somma è esattamente lineare, e host e client prendono lo stesso numero comunque le mance siano raggruppate.
  • ⚠️ Trovato dalla review incrociata, non dal builder: l'XP finiva anche sulle donazioni del super COMIZIO IN PIAZZA, che riusa busk_donate() (NPC.gd:2280) e, sul client, la stessa consegna (World.gd:5301). Sarebbe stato un modo di salire di livello senza suonare, in un ticket che vieta di toccare l'economia. Ora il comizio passa xp_inside = false e resta a zero XP: misurato, 19 donazioni in 6 giri → $70 e 0 XP.
    • 📌 La distinzione non si poteva dedurre dai due argomenti che il pacchetto aveva già: total sono dollari, e col GATTO il cap delle donazioni è 2, quindi anche una suonata può produrre un exhausted vuoto identico a quello del comizio. Per questo il terzo argomento esiste.
    • ⚠️ _cl_busk_result cambia firma (terzo argomento con default): il nome della RPC non è nuovo, quindi nessun id RPC slitta, ma una build vecchia scarterebbe il pacchetto a tre argomenti del comizio. Alla 0.44 il minimo MP va comunque alzato — dal tag v0.43 ci sono 25 RPC nuove in World.gd — quindi non aggiunge costo. 📌 Da sapere: la guardia RPC di SU-934 vede le RPC nuove, non i cambi di firma di quelle esistenti.
  • Controprova rossa senza git stash (XP_BUSK_PER_DOLLAR a 0 e rimessa a 1 nello stesso comando): 6 contro 30, il 20%, ESITO KO — e il caso client con l'XP acceso falliva come deve.
  • ✅ Le altre spunte: un ciclo in cui nessuno dona vale ancora +6 XP (il pavimento di SU-247); i dollari non cambiano ($24 → [1,1,2,10,10] con XP 0 e con XP 24); l'XP accreditato è esattamente quello che le monete portavano dentro.
  • ⚠️ La sonda ha dovuto inchiodare tre variabili, o dà verdetti opposti sullo stesso codice (misurati 105% e 55% su due lanci): il tier sale col tempo di run e abbassa la generosità; il quartiere sorteggiato scala le due attività in direzioni opposte (Nottefonda busking +25% ed elemosina −20%, Collina elemosina ×2); il barbone rivale finito nel pubblico non dona mai e dimezza le mance degli altri (SU-89) — era lui il lancio da 55%.
  • ⚠️ Non provato: nessun collaudo a due peer (il lato client è analizzato sul codice e provato chiamando la consegna condivisa in locale), e niente misure a tier > 0, col CONCERTONE o col VIRTUOSO DELLA TROMBETTA — che ora alzano anche l'XP, conseguenza voluta ma non misurata. In un angolo molto affollato la suonata può arrivare a ~1,8× l'elemosina: la manopola è una riga, CoinDrop.XP_BUSK_PER_DOLLAR.

Lo splash di avvio scende a due secondi, e il menu era già pronto da un pezzo (SU-943)2026-09-12

Deciso in chat da Ivan: «schermata loading farla 2 secondi anziche' 3». Il valore vero non era 3 ma 2,6 s (SU-439 aveva già tolto la dissolvenza d'uscita da 0,4 s, quindi 2,6 era anche il totale).

  • SPLASH_MIN_SEC da 2,6 a 2,0 (files/homeless_city/scripts/ui/Boot.gd:83). Cronometrato sul Mac con una strumentazione temporanea poi rimossa, due run: cambio scena a 1897 ms e 1904 ms (bersaglio 2000 ±200).
  • Il criterio che contava non era il numero, era se a 2,0 s il menu fosse pronto: il caricamento in thread di MainMenu e il prewarm degli sfondi di SU-650 finiscono a 287-297 ms, cioè con 1,7 s di margine sui 2,0. I 0,6 s in meno non toccano niente, e non c'è stato bisogno di spostare il prewarm.
  • 📌 Se un giorno quel margine non basterà, il sintomo non sarà un fotogramma nero ma uno scatto: il cambio scena è un tween_interval(SPLASH_MIN_SEC) fisso che non attende il caricamento (Boot.gd:279-280), e _go_to_menu() chiama ResourceLoader.load_threaded_get() anche su un load IN_PROGRESS (Boot.gd:411-414) — che blocca fino alla fine invece di fallire. Sull'Honor 10, il device più lento, è la cosa da guardare.
  • 📌 Al cancello sono stati allineati due commenti che descrivevano ancora 2,6 s (Boot.gd:177, :262): li aveva segnalati l'istruttoria di Codex, ed erano l'unico rilievo vero delle sue undici righe — le altre dieci dicevano NON MISURATO perché su un lotto costruito da Claude cancello non vede il report del builder.
  • ⚠️ Non provato: il cronometro sull'Honor 10 e l'assenza di scatti oltre 100 ms al cambio scena, che vogliono il device di Ivan.

La release si accorge da sola quando le RPC sono cambiate (SU-934, SU-907)2026-09-12

  • Guardia RPC in prepare_release.sh (rg_guardia_rpc_mp in tools/release/release_git_lib.sh): confronta l'elenco delle RPC di ogni .gd fra l'ultimo tag di release e dev, e se è cambiato lo stampa accanto al comando del manifest, dicendo che la versione minima multiplayer deve diventare quella che esce. In Godot 4 l'id di una RPC è l'indice del suo nome in ordine alfabetico: una RPC in più sposta tutte quelle dopo, e due build diverse non giocano insieme senza dare nessun errore.
    • Misurato al primo giro: dal tag v0.43 (04/09) a dev ci sono 25 RPC nuove in World.gd (possessione, riccone, metro, branco di SU-907), 13 in NetworkManager.gd e una sonda. Quindi alla 0.44 il minimo MP va portato a 0.44.
    • Controprova: confrontando il tag con sé stesso la guardia non stampa niente.
  • WORKFLOW/04_RELEASE.md: il passo del manifest non era nella checklist, ora è la voce 8c del Tempo 1, con la regola della versione minima e il perché.
  • 📌 Pubblicati (chiesto da Ivan): version_stage.json in schema 2 a 0.43 (928da3f) e version_live.json in schema 2, lasciato a 0.41 per non annunciare un aggiornamento sul canale pubblico (1b04fee); in tutti e due la versione minima MP è 0.43, la stessa che stava in devices.json. version_dev.json resta schema 1, come da ticket.

Il branco si vede su tutti i telefoni, e da fantasma si cambia giocatore con X (SU-907, SU-942)2026-09-11

  • SU-907 — il branco arriva anche agli ospiti (9527375, builder Claude LW2, due giri al gate). È il quarto giro del ticket, ma il difetto è nuovo: i giri precedenti sistemavano audio e nuvola, e la sonda chiamava il puppet nello stesso processo. Il consulto Codex Astra D907 aveva visto che il percorso host→ospite non esisteva (net_dog_pack usciva sull'host, spawn_remote_dogs rifiutava i client). Confermato con una sonda a due processi Godot in ENet, files/homeless_city/scripts/tools/probe_su907_branco_rete.gd con tools/autotest/probe_su907_branco_rete.sh, rossa sul codice di prima. Ora l'host diffonde comparsa e fine del branco di chiunque, col super e con la carta.
    • ⚠️ RPC nuova _cl_dog_pack_fx: gli id delle RPC di World.gd slittano, e una build vecchia e una nuova non giocano insieme.
    • 📌 Dalla review e dal gate: con l'host collassato le richieste dei client venivano rifiutate (proprio il caso del collaudo di Ivan); fantasmi e spettatori non vedevano i branchi altrui; la fine anticipata da carta non arrivava agli altri; il lanciatore della sonda usciva 0 anche con un FAIL.
    • 📌 Trappola: il builder ha usato git stash per la controprova. Al gate lo stash era vuoto e il diff intatto, ma con altri builder in volo sullo stesso working tree avrebbe portato via il loro lavoro: il divieto ora sta nella capsula dei brief (WORKFLOW/CAPSULA.md).
  • SU-942 — da fantasma si cambia giocatore con X (9527375, stesso lotto). Vale anche per lo spettatore di GUARDA, deciso al brief per coerenza. Un rilascio breve di A non cambia più, e la tenuta di 3 s resta com'era. I banner prendono il prompt del tasto da InputManager: per questo è stato toccato HUD.gd, fuori dal brief. La X a schermo resta attiva per il fantasma: verificato leggendo VirtualControls.gd, non fotografato.
  • 📐 Collaudo a due peer (ENet) di SU-888, SU-907, SU-942, SU-941 e SU-933 lanciato a fine sprint: esiti in TESTLOG.md.

RIENTRI ATTESI (messi In revisione il 2026-09-11, Sprint 14, /sprint della sera): tredici chiavi — SU-779, SU-914, SU-933, SU-920, SU-934, SU-917, SU-888, SU-941, SU-939, SU-932, SU-935, SU-907, SU-942. SU-940 resta in Da fare, in attesa di una decisione di Ivan.

La tastiera di iOS si misura dentro la build, e la spilla solo su invito entra nel negozio (SU-932, SU-935)2026-09-11

  • SU-932 — secondo giro, misura più rimedio (ae756a3, builder Claude L935). Un consulto Codex Astra (D932) non attribuisce la causa senza una misura sul telefono; i candidati sono lo stato del campo, un'interferenza fra tocco, mouse emulato e gestione nativa, o il first responder di iOS.
    • La build porta una riga [SU-932] scritta con push_error, così arriva sul disco dell'iPhone: 5 s dopo l'apertura di CAMBIA NOME e 2 s dopo ogni tocco, al massimo 3. Riporta tocchi, provider, fuoco, editable, feature, richieste esplicite e altezza della tastiera.
    • Rimedio all'ipotesi più economica: fuoco ed edit() al RILASCIO del tocco, solo per il campo del nome; il campo del codice beta resta com'era.
    • Sonda probe_su932_tastiera.gd: 7 casi, con pressione e rilascio su frame veri. La sonda del primo giro li consegnava in fila, e quel caso non lo copriva.
    • 📌 Dalla review Codex: all'apertura si azzeravano solo le righe, non i contatori, e la diagnosi di un'apertura mai toccata avrebbe riportato i tocchi di quella prima.
  • SU-935 — la spilla «solo su invito» (ae756a3, stesso lotto). Variante 01 approvata su SU-919, a 1 lattina d'oro in ExclusiveSkinShop, persistita in Skins e scalata dal saldo in Inviti; nella Baracca un'icona in alto a destra del pannello, accanto al titolo, perché la lista degli upgrade era già piena. Raw in sprites_raw/UI/badge_invite.png, 32×32 in files/homeless_city/assets/ui/badge_invite.png.
    • 🐛 Trovato dal builder: buy_golden() assegnava il piccione a qualunque id diverso da «punk». Nessun chiamante lo faceva ancora, ma il badge sarebbe stato il primo.
    • 📌 Presi al gate: «1 LATTINE D'ORO» al plurale (chiave nuova SHOP_GOLD_PRICE_ONE, 8 lingue); il raw era finito nella radice di sprites_raw/; dalla review, il provino della Baracca nasceva col badge già posseduto e non provava la comparsa dopo l'acquisto.
    • ⚠️ Non provati: l'acquisto premendo davvero il bottone (la sonda compra da codice) e il telefono. Da decidere con Ivan: la posizione dell'icona nella Baracca. La lista della Baracca a 844×390 esce dal pannello già prima di questo lotto.

La possessione dei nemici di quartiere regge in partita, il quartiere lo sceglie l'host, gli accessi già collegati si vedono (SU-888, SU-941, SU-939)2026-09-11

  • SU-888 — possessione di yeti, pinguino e fenicottero (43a072f, architetto Claude L888, due giri al gate). La sonda del primo giro chiamava set_possession_state a mano ed era verde col gioco rotto. files/homeless_city/scripts/tools/probe_su888_gesture.gd passa dal gesto vero e ha trovato quattro cause:
    • la tenuta di A arrivava all'host un frame prima dei 3000 ms e veniva rifiutata senza messaggio, anche per gli NPC delle parti 1 e 2;
    • per il fantasma l'orologio dei foe era fermo, perché DifficultyManager.elapsed avanza solo con run_active;
    • un foe posseduto fuori dallo schermo dell'host si spegneva;
    • il banner SEI DENTRO cercava i foe fra gli NPC.

      Possessione 3/3 da host e da client, a 60 e 30 fps (prima 1/3). Nessuna RPC nuova.

    • 📌 Dalla review Codex, corretti al gate: il registro anti-doppione usava ancora l'orologio fermo, quindi il secondo abbattimento non arrivava allo spettatore; 150 ms di tolleranza non bastavano con un primo frame lungo su telefono, ora sono 400; lo spettatore entrato a partita iniziata partiva con l'orologio a zero, ora si aggancia a _cl_run_sync.
    • ⚠️ Residui: chi guarda parte fino a ~1 s dietro l'host, perché il sync è a secondi interi; un salto di debug dell'host non sposta DifficultyManager; il pupazzo di neve (ParkFoe.gd) si ferma ancora per il fantasma (una riga, fuori dal ticket). Due telefoni veri: non provati.
  • SU-941 — il quartiere lo decide l'host (b132cc4, builder Claude L941). _quartiere_available_for_all() guarda solo gli sblocchi dell'host. L'ingresso di chi non ha il quartiere era già libero, e NetworkManager.gd non è stato toccato. Sonda su941_lobby_quartiere.gd, anche con un peer ENet vero; run_mp.sh a due peer va al collaudo.
  • SU-939 — gli accessi già collegati non dicono più COLLEGA (b132cc4, stesso lotto). Le righe leggono accounts(puid) di EOSBridge per riflessione, senza modificarlo. La riga «✓ … GIÀ COLLEGATO» sta su una pillola verde spenta. Provini in TMP/sprint_0911s/L941/.
    • 📌 Preso al gate: la riga collegata aveva la stessa pillola bianca dei tasti, e lo diceva solo il testo. Dalla review Codex: la cache non era legata al PUID (dopo un trasloco poteva mostrare gli accessi di prima), le risposte in ritardo sovrascrivevano link e logout, e le sonde iniettavano la cache senza mai passare da accounts(). Corretti dal builder, sonda 21/21.
    • 📌 Trappola della harness: un sync_proj senza --import dopo un cambio di CSV rimette la cache di traduzione vecchia. Prima degli scatti serve ensure_import force.

La versione minima MP passa nel manifest del canale, e la coda dell'unione non si perde più a metà (SU-934, SU-917)2026-09-11

  • SU-934 — la versione minima MP sta nel manifest del canale (c093c3c, Codex Sol N934 e N934b). versione_multiplayer entra in version_<env>.json. BetaGate la legge dal manifest del canale di Env, fail-open, e scarica devices.json solo col gate beta desktop. genera_whitelist.py non la tocca più, e /release stampa il comando di pubblicazione dopo la conferma. Sonda probe_su903_version_gate.gd: con stage a 0.44 una 0.43 è bloccata, senza rete si entra.
    • 📌 Preso al gate: lo script di pubblicazione usciva prima del push se il manifest era già committato, poteva committare file già in stage di altri e non validava la versione. Corretto in N934b. I quattro casi git (normale, push recuperato, già allineato, indice sporco) li ha provati l'orchestratore su un remote bare locale, perché nella sandbox Codex git è vietato.
    • ⚠️ Manifest NON pubblicati: version_stage.json e version_live.json in schema 2 sono pronti in TMP/sprint_0911s/N934/ e aspettano l'ok di Ivan (push su street-university-beta-devices). Finché non escono, una build con questo commit non legge più la versione minima da devices.json e trova online i manifest vecchi (0.41, schema 1), quindi il gate MP resta aperto: vanno pubblicati prima della prossima build.
  • SU-917 — la coda dell'unione parte anche quando l'unione si interrompe (a4a6bbf, builder Claude L917 e Codex Sol N917b). La liberazione del nome e sync_survivor partivano solo nel percorso riuscito. Se l'unione si interrompeva dopo lo stacco del donatore rimasto orfano e poi si riparava con COLLEGA, non partivano più: è il quadro del KO, letto nel codice e da confermare coi log [SU-917] che ora ci sono a ogni passo. Sonda probe_su912_trasloco.gd: 86 controlli verdi, rossa sul codice di prima.
    • 📌 Dalla review Codex: il sync fallito ora si segnala come nel percorso normale; se il token del donatore è vuoto c'è un avviso ma l'unione non si ferma, perché su iOS la copia del token non è mai stata vista dal vivo; l'identificatore è rinominato in inglese.
    • ⚠️ Residui: la coda non sopravvive alla morte dell'app fra stacco e coda. divano_prime su stage resta prenotato finché non si lancia tools/firestore/libera_nome.py --esegui, con l'ok di Ivan.

Sprint della sera, prima onda: il salvataggio parte dalla fine partita vera, il nome si rilegge con ogni accesso, carte a 5 livelli e bonus metro fermo a 400 (SU-920, SU-914, SU-933, SU-779; diagnosi di SU-940)2026-09-11

Sprint 14, /sprint delle 19:15. Quattordici ticket aperti (sette nuovi, sette bocciati) e dieci lasciati fermi per decisioni già scritte (SU-870, SU-871, SU-872, SU-781, SU-783, SU-790, SU-795, SU-461, SU-701, SU-782).

  • SU-920 — il salvataggio parte dalla fine partita vera (6dc16eb, builder Claude). La scrittura era agganciata a RunManager.run_ended, che non ha emettitori dal 20/07 (7c5e08c). Ora ProgressSync e Inviti si agganciano a GameState.game_over, in differita. write_progress() non guarda run_active, quindi l'ordine «segnale prima di run_active=false» non blocca niente. Sonda files/homeless_city/scripts/tools/probe_su920_fine_partita.gd col mondo vero e trigger_game_over; i 37 controlli del primo giro restano verdi. Il vecchio aggancio resta, perché lo usa probe_su791_inviti.gd: se run_ended tornasse vivo la scrittura partirebbe due volte, senza danni.
  • SU-914 — il nome del registro vince anche con Apple e Google (3fcbe24, Codex Astra N914). claim_provider_handle usciva per Apple e Google prima della rilettura fresca, che valeva solo per Epic. Sonda probe_su914_rilettura_nome.gd: 121 controlli verdi, 20 falliti sul codice di prima.
  • SU-933 — carte a 5 livelli, superpotere e Giullare al 6 (a6acdbb, Codex Sol N933 e N933b). Effetti ×9/5 in get_mult, lettori del cap allineati, testi in 8 lingue.
    • 📌 Preso al gate: la CALAMITA è additiva e get_add() non riscalava. Col cap a 5 sarebbe scesa da 225 a 125 px: riscalata anche lei, scelta dichiarata sul ticket.
    • 📌 Preso al gate: il testo del BRANCO in ui_meta.csv diceva ancora «al 9», perché il grep del builder cercava solo «10».
    • Residuo: il commento di World.gd ~4805 dice ancora liv. 9 (il file era in mano al lotto di SU-888).
  • SU-779 — dal giro 4 incasso massimo 400 (9cbf792, Codex Sol N779). valore_moneta() torna a $2 dal quarto giro; i giri 1-3 non cambiano. Due commenti su «×2 a ogni giro» corretti al gate.
  • 🔍 SU-940 — diagnosi, nessun codice (consulto Codex Astra D940, dai binari del plugin): il mediatore P2P di EOSG registra le notifiche una volta sola, sul PUID del primo accesso, e set_local_user_id non è esposto nel binding (le cinque chiamate di EOSBridge vengono saltate). Sul ticket la decisione chiesta a Ivan: ricompilare il plugin, o riconoscere il cambio account e chiedere di riavviare.
  • 🔍 Consulti per la seconda onda: SU-907 (D907, il branco dell'host verso gli ospiti non viene mai inviato) e SU-932 (D932, causa non attribuibile senza una misura sul telefono: si prepara una riga diagnostica decisiva).
  • ⚠️ Non provati: Player Data Storage e registro veri, i giri Honor↔iPhone, il Giullare in multiplayer, un tunnel del bonus giocato davvero.

L'ordine non conta: collegare l'accesso di un altro giocatore li unisce, e resta chi ha accessi non spostabili (SU-921)2026-09-11

Decisioni di Ivan del 2026-09-10, scritte su SU-912: un solo giocatore per Apple, Google ed Epic; progressi sempre uniti; resta il giocatore con accessi non spostabili da quel dispositivo, altrimenti quello col salvataggio più recente.

  • La scelta di chi resta sta in una funzione pura di EOSBridge, sui quattro casi del ticket. Gli accessi esterni di P e di X si leggono da EOS Connect mentre si è entrati in ciascuno.
  • Il trasloco inverso. Quando resta X, è l'accesso con cui si è entrati che passa a lui:
    • copia del token di P, lettura e fusione dei suoi progressi;
    • unlink da P, nuovo token, link a X;
    • liberazione verificata del nome di P (SU-917);
    • identità, nome (SU-914) e salvataggio (SU-920) passano a X.

      Il trasloco diretto legge e fonde anche i progressi di X prima dello stacco.

  • Il dialogo dice chi resta, col nome: «Resta Divano! Le chiavi traslocano e i progressi si uniscono nello stesso zaino». Se restano chiavi sull'altro dispositivo lo dice, e un accesso dello stesso canale già presente non viene promesso. Testi nelle 8 lingue.
  • Dalla review di Codex, tre difetti veri corretti nel lotto UNI2:
    • chiudendo il gioco durante il trasloco EOS si spegneva a metà; ora l'uscita aspetta, con un timeout;
    • se l'unione falliva prima del primo link, i progressi dell'altro restavano fusi su disco; ora i cfg tornano com'erano;
    • due accessi dello stesso canale non stanno sullo stesso giocatore, e il dialogo non lo promette più.
  • 🐛 ui_menu.csv era rotto da SU-679 (e421551): due copie orfane di testi su più righe davano righe logiche da 1 e 8 colonne. Le ha trovate UNI2 e sono tolte in un commit a parte (9563fb3). Dopo: 0 righe malformate.
  • 📌 Due trappole dell'harness, costate un giro ciascuna:
    • la sonda accettava la HOME solo dentro TMP/su_UNI: corretto;
    • il provino calcola la TMP della radice dal progetto, quindi la copia di prova deve stare direttamente sotto TMP/.
  • 📐 Prove:
    • sonda files/homeless_city/scripts/tools/probe_su912_trasloco.gd: 77 controlli verdi, controprova --red rossa, rilanciata al gate;
    • provino TMP/su_UNI/shots/: 4 PNG, parole lette nello scatto. A misura telefono il testo resta piccolo.
  • ⚠️ Non provati: EOS vero (accessi esterni, link e unlink inversi), Player Data Storage vero, il giro iPhone↔Honor. La lista delle prove per domattina è su SU-912. Lotti Codex Astra (UNI, UNI2).

Il collaudo della mattina: il reset che regge alla nuvola, i punk nel codice beta, le ombre a mano (SU-937, SU-927, SU-938)2026-09-11

Ivan ha provato sui telefoni la build stage 275e1c4 e ha chiuso 17 ticket; da tre KO e due decisioni sono nati SU-937 e SU-938, e SU-927 è rientrato con un requisito nuovo.

  • SU-937 — RESET PROGRESSI riblocca quartieri e personaggi, e la nuvola non li riapre. Due guasti, riprodotti con la sonda files/homeless_city/scripts/tools/probe_su937_reset.gd (30 KO su 49 prima del fix): il pulsante in MainMenu.gd non chiamava Skins.reset_all(), e la prima fusione di ProgressSync.gd, che unisce gli sblocchi, riapriva i quartieri dalla nuvola (0/7 dopo il reset, 7/7 dopo la fusione). Ora il reset lascia in meta.cfg un segno datato per il PUID dell'account, e la fusione toglie all'altro lato solo ciò che il reset ha azzerato; fuori dal reset la fusione di SU-787 è identica. Il segno si scrive prima degli azzeramenti: se non si salva non si azzera niente, e il pulsante dice «RESET NON RIUSCITO». Conferma ed esito nominano quartieri e personaggi, in 8 lingue. Lotto architetto-opus Z927b, due giri.
    • Al gate, dalla review Sol, confermati e chiusi al secondo giro (sonda 97/97, 33 KO sul codice del primo giro): il segno di reset su una chiave globale, che un altro account avrebbe ereditato; un salvataggio in volo che riscriveva i dati di prima del reset; il fallimento del segno ignorato dalla UI; l'ordine dei reset legato all'orologio (ora il segno vale almeno il più alto già visto più uno).
    • Terzo giro, dalla seconda review (sonda 109/109, 5 KO sul codice del secondo giro): ogni giro di sincronizzazione fissa il PUID all'inizio e si scarta se l'account cambia durante l'attesa di EOS. La finestra c'era già da SU-920 con EOS vero: prima i dati di un account potevano finire in locale o sulla nuvola di un altro. E un segno paid che non torna indietro vince su gifted, così una copia vecchia della nuvola non rimborsa più i punk pagati.
    • Non provati: la nuvola EOS vera e due telefoni. Rischio che resta: un dispositivo che ha giocato da ospite o offline e si sincronizza dopo un reset dell'account perde quel progresso.
  • SU-927, rilavorato — il codice TUTTIPERSONAGGI apre anche i punk. Decisione di Ivan: «unlock all sblocca anche i punk, ma se non usi il codice vanno sbloccati col sistema delle lattine d'oro». Skins.unlock_all() apre punk_m e punk_f (il piccione resta fuori) e li segna come regalati (Skins.gifted): la spesa di lattine d'oro in files/homeless_city/scripts/autoload/Inviti.gd conta solo quelli pagati, quindi il saldo non scende. Sonde: SU-927 70 OK in headless, SU-793 44/44, SU-920 37 verde.
    • 📌 Il «flag che sblocca tutti i personaggi» non esiste: sul profilo di Ivan erano aperti per sblocchi passati, uniti fra dispositivi dalla sincronizzazione.
    • Default presi, che Ivan può cambiare: i punk aperti dal codice non scalano l'oro; il testo del reset nomina quartieri e personaggi.
  • SU-938 — le ombre le corregge Ivan a mano. Scelta di Ivan, dopo i KO di SU-905 e SU-929: «proviamo la A» «e la B per i camioncini». Lotto architetto-opus Z938, due giri.
    • A: nel pannello F1, in partita singola e da build di debug, SPOSTA OMBRA prende l'ombra più vicina e la sposta, allarga e allunga per fascia oraria (mattina, mezzogiorno, sera); SALVA scrive files/homeless_city/assets/data/ombre_correzioni.json, che ogni build legge. Le correzioni valgono per sprite (palazzi) o per tipo (camioncini, fontane, panchine), si sommano all'ombra calcolata e si interpolano col sole. Codice in files/homeless_city/scripts/world/WorldGenerator.gd e files/homeless_city/scripts/autoload/DebugPanel.gd.
    • B: per ogni tipo di camioncino e fascia, un PNG disegnato in files/homeless_city/assets/sprites/ombre/ prende il posto dell'ombra calcolata, con dissolvenza fra le fasce. I sei modelli da cui partire, col camioncino in trasparenza, l'ombra di oggi e un .txt con misura e pivot, sono in sprites_raw/ombre/modelli/.
    • Prove: senza file le ombre di palazzi e fontane sono identiche a HEAD (0 pixel su 18 scatti); una correzione sposta dei pixel scritti solo le ombre di quello sprite o tipo (palazzo 9/9, camioncini 2/2, fontana a raso e fontanile 2/2, le altre ferme); il PNG di prova sostituisce l'ombra del camioncino.
    • Al gate, dalla review Sol, corretti al secondo giro: la zona che scurisce i personaggi restava quella di nascita (ora si riregistra quando cambia una correzione: 9 centri su 9 in ombra nella sagoma nuova); i valori del JSON non erano controllati (ora gli stessi limiti del pannello, e le voci malformate si scartano con una riga di log); nomi del codice in italiano. Scartati: il «bug certo» delle fontane (smentito con la sonda) e la pausa globale (il divieto vale in MP, lo strumento funziona solo in partita singola).
    • Non provati: lo strumento con i tasti veri e il pannello a schermo, e il JSON dentro un export mobile.
    • Terzo giro, dal collaudo di Ivan («sposta ombra non me lo fa fare per la vineria, per i bidoni dell'immondizia»): dei 12 punti di WorldGenerator.gd in cui nasce un'ombra del sole lo strumento ne copriva 5 (palazzi, camioncini, fontane, panchine). Ora prende anche vineria, bidoni e altri arredi, bancarelle, bagni, lampioni e pensilina. Restano fuori le macchie che seguono chi si muove (piccioni, nemici, torcia), che non sono ombre del sole. Prove: 76 controlli, con bidone e vineria spostati dei pixel scritti e le altre ombre ferme; senza file 0 pixel diversi su 36 confronti.
    • Chiesto da Ivan nello stesso giro: STATS FERME nel pannello F1 ferma il calo di fame, energia e igiene e le morti per stats (un flag letto da stats_drain_suspended() in files/homeless_city/scripts/autoload/GameState.gd), spento a ogni avvio, solo sul barbone di chi lo accende. Sonda 8/8. Non provato con un'ondata di calamità in corso.
    • Nel commit ci sono anche le prime 4 correzioni fatte da Ivan con lo strumento (palazzi del centro, mattina) in files/homeless_city/assets/data/ombre_correzioni.json.
    • Quarto giro, dal KO di Ivan sullo strumento («quando le riattivo si spostano», «un offset di partenza ombra tra l'ombra la mattina e quelle la sera», «mancano le ombre per rifugio, banca, chiesa e per la metro»). Riprodotti con la sonda probe_su938_ko4.gd sul codice di prima: uscendo dallo strumento l'ombra saltava di 2,1-6,4 px, perché lo strumento bloccava il sole all'alba con la correzione al 100% e il gioco la applicava al 54%; e con correzione zero il punto di partenza cambiava fra le 6 e le 19 (26 px sulla fontanella a raso), per scarti tarati su un'ora sola.
    • Rimedio: le fasce sono tre terzi del giorno (6:00-10:20, 10:20-14:40, 14:40-19:00), la correzione vale uguale dentro la sua fascia e passa all'altra in ±20 minuti di gioco. Lo strumento non blocca più il sole e mostra l'ora vera (col tasto F un'ora della fascia scelta): salto 0,00 px. Bidoni, arredi, fontanile e fontanella a raso partono dal piede della sagoma a ogni ora (0,00 px), e gli scarti tarati di SU-736 e SU-929 sono tolti; senza correzioni cambiano quindi, per scelta, le ombre di questi oggetti, mentre palazzi e camioncini restano identici.
    • Ombre nuove: banca e chiesa quando stanno da sole, metro col cartello, rifugio (la sagoma dell'edificio aperto, senza il cartello OPEN/CLOSED), tutte correggibili con lo strumento.
    • Le 8 correzioni già salvate da Ivan sono convertite al sistema nuovo con tools/autotest/converti_su938_ko4.py: su 127 ombre il vecchio codice con le vecchie correzioni e il nuovo con le convertite coincidono entro 0,005 px. Il builder non aveva il permesso di sovrascrivere il file di Ivan; l'ha sostituito l'orchestratore, dopo aver verificato che fosse ancora identico alla copia delle 15:40.
    • Da rifare probabilmente: la correzione del fontanile di mattina, fatta all'alba nello strumento vecchio, che tiene l'ombra 27 px sopra il piede.
    • Quinto giro, dal secondo KO di Ivan («i bidoni dell'immondizia, ancora spostate, e la chiesa che ha l'ombra della fabbrica»). Bidoni: il codice era giusto (su 34 bidoni l'ombra parte dal piede, strumento e gioco coincidono, SALVA scrive sulla chiave trash.png). A staccarli era la voce trash.png del file di Ivan, uscita dalla conversione del giro 4, che ricopiava il compenso fatto sul difetto vecchio. La voce è stata tolta dall'orchestratore dopo aver verificato che il file fosse invariato: le altre 11 chiavi sono identiche. I forzieri hanno una chiave loro (bin_closed.png).
    • Chiesa e banca: difetto vero. Il codice che a città finita garantisce una sola banca e una sola chiesa cambiava lo sprite del landmark dopo che l'ombra era nata, e l'ombra restava quella di partenza (al Mercato la chiesa aveva i comignoli della fabbrica e la banca il campanile). Ora, quando lo sprite cambia, l'ombra rinasce sulla sagoma giusta (_respawn_landmark_shadow in files/homeless_city/scripts/world/WorldGenerator.gd). Sonda probe_su938_ko5.gd; i giri 1-4 restano verdi, e senza file 0 pixel diversi su 36 confronti.
    • Correzioni salvate da Ivan con lo strumento dopo il giro 5, tutte sulla mattina, in files/homeless_city/assets/data/ombre_correzioni.json: cestini (trash.png: 4 px più su, 1 a sinistra, larghezza 0,9, lunghezza 1,6), forzieri (bin_closed.png: 3 px più su, larghezza 0,95), fabbrica (landmark_factory.png: 3 px più su). Il KO dei bidoni al mattino dopo il giro 5 non si riproduceva sul codice (0,00 px su 34 bidoni con una sonda indipendente): era una questione di gusto sulla mattina, chiusa da Ivan a mano. Trovato per strada: la sonda del giro 4 misurava ancora e piede con lo stesso calcolo, quindi il suo 0,00 non provava niente.

Lo sprint di notte: il consulto torna a Sol, e due decisioni istruite coi numeri (SU-922, SU-930, SU-928)2026-09-11

Ivan è andato a dormire lasciando le scelte a Claude: i default presi sono scritti sui ticket. Un'altra sessione lavora in parallelo su SU-921 (EOSBridge.gd, ProgressSync.gd, MainMenu.gd, ui_menu.csv): i lotti di stanotte quei file non li toccano.

  • SU-922 — il modello Codex si sceglie compito per compito. In tools/codex_lotto.py il default di consulto passa da Astra a Sol, e la docstring ha una sezione QUALE MODELLO: luna per letture, cancelli e istruttorie; sol per lotti, consulti ordinari e review; astra solo a mano, per sistemi profondi, KO al 2º giro e sprite. La stessa regola sta nella tabella del roster e in «Dove va Astra, in pratica» di ORCHESTRAZIONE.md, e nella riga dei due piani di CLAUDE.md.
    • Prova: un consulto lanciato senza --modello (T922) parte gpt-5.6-sol/xhigh (TMP/codex_lotti/T922/AVVIO.json); tools/sprite/ resta su MODELLO_SPRITE:-gpt-6-astra.
    • Lotto tuttofare-haiku. Al gate l'istruttoria Luna ha notato che la regola stava solo nel commento sopra DEFAULT e non nella docstring: completata dall'orchestratore.
    • Ipotesi scartata al gate: «criterio 1 non misurato». L'istruttoria aveva letto l'AVVIO.json del cancello, non quello del consulto di prova.
  • 📌 SU-930 — (DESIGN) il tetto dei power-up. Misurato sulla telemetria 0.43, 128 run, livelli ricostruiti dai card_picked: nessuna carta arriva al superpotere; al 9 ci arrivano 2 run, al 6 sei, al 5 otto. Proposta sul ticket: tetto 5, Giullare al 6, opzione A (effetto per livello ×1,8). Anche così il Giullare lo vedrebbe una run su 16: se lo scopo è mostrarlo, serve anche un mazzo che riproponga le carte già prese. Decide Ivan; nessun codice. Istruttoria Codex Luna I930.
  • 📌 SU-928 — (DESIGN) la lista beta e il canale live. Proposta sul ticket: la versione minima MP dentro version_<env>.json (già uno per canale, sullo stesso repo, già scaricato da ogni build), devices.json solo al gate desktop, pubblicazione al passo del manifest di /release. Istruttoria Codex Luna I928. Due guasti muti trovati per strada, nessuno toccato:
    • tools/release/genera_whitelist.py riscrive devices.json da zero senza versione_multiplayer: lo 0.43 online è stato messo a mano, e il prossimo /lista-beta --pubblica spegne il gate di SU-903 senza errori;
    • version_stage.json e version_live.json online sono fermi a 0.41 e allo schema 1, mentre il gioco legge lo schema 2 (files/homeless_city/scripts/autoload/Env.gd:148): oggi vengono ignorati.
  • SU-923 — gli scatti di giorno sull'Honor erano le ombre di panchine e camioncini. Misurato con la sonda per-frame (files/homeless_city/scripts/dev/Su911Perf.gd, modalità su923_frames, accesa solo dalla feature di export omonima): di giorno, col barbone fermo, un frame lungo ogni 0,837 s, cioè 2 minuti di gioco, da 50-100 ms di cui ~84 di script, contro 18,7 ms di mediana. Colpevole _aggiorna_ombre_panchine in files/homeless_city/scripts/world/WorldGenerator.gd: a ogni spostamento del sole rifaceva le texture d'ombra testando lo sprite pixel per pixel. Il fix riempie l'ombra riga per riga e analizza lo sprite della panchina una volta sola. Sull'Honor, stessa base e 33-34 °C: p99 di giorno 88,9 → 32,0 ms, frame oltre 2× la mediana 228 → 22, oltre 4× 106 → 3, fps 47,2 → 51,3; notte 37,2 → 38,2 fps. Script di misura in tools/android/ (esporta_apk_su923.sh, misura_scatti_su923.py, strumenta_ombre_su923.py, fix_ombre_su923.py). Lotto architetto-opus Z923; il fix, preparato fuori dal repo perché WorldGenerator.gd era di un altro lotto, è stato applicato al gate dalla patch misurata.
    • Non è una regressione: la build aba6588, prima di SU-918, ha gli stessi scatti. Il codice viene da SU-736.
    • Di notte gli scatti spariscono perché con le ombre spente la funzione esce subito, non perché il frame è già lento.
    • ⚠️ Il fix cambia 740 pixel in 326 delle 3.609 texture d'ombra del camioncino (al massimo 24 in una: la tolleranza del test di Godot sul lato lungo); la panchina è identica al pixel. Log in TMP/su_Z923/fix/equivalenza_E2.log.
    • Restano 22 frame oltre 2× sparsi, non alla cadenza, e un frame da 420-590 ms verso le 15:00 di gioco, lato render, da ticket a parte. Tutte le build di misura sono uscite da git archive, mai dal working tree; export_presets.cfg identico.
  • SU-926 — da ospite si guarda una partita già iniziata, e si gioca la prossima. In files/homeless_city/scripts/autoload/NetworkManager.gd l'host accetta chi entra in una partita in corso e non piena e lo marca spettatore: chiave spect solo nel suo registro, ingresso con la _cl_game_starting già esistente, fuori da living_ids e dalla classifica viva, chiave tolta in start_game e nel rematch. In files/homeless_city/scripts/world/World.gd il suo barbone non nasce sugli altri schermi e sul suo è invisibile e fermo; la camera segue il primo della classifica, un tocco o un clic passa al successivo, e a fine partita chiede da solo il TORNA IN LOBBY di SU-876. Banner «STAI GUARDANDO» in HUD.gd. La ricerca automatica sceglie con choose_search_target: prima una lobby aperta, una partita in corso solo se non ce ne sono, le piene mai. La lista mostra «IN CORSO 5/8 · GUARDA» e «IN CORSO 8/8 · PIENA» (MainMenu.gd), perché al via l'host non toglie più la lobby dalla ricerca ma pubblica SU_RUN (EOSBridge.gd, supera SU-54). In RunRecorder.gd lo spettatore non apre una run. Nessuna RPC nuova (World 105, NetworkManager 24): la versione minima MP non si alza. Lotto architetto-opus Z926, due giri.
    • Sonde a 3 peer ENet (tools/autotest/probe_su926_mp.sh): scenario A, spettatore con host e giocatore vivi, 10/7/22; B, giocatore già fantasma, 10/7/19 al primo giro; C, partita conclusa e due peer in fila, 13/13. Regola della ricerca 10/10, SP 4/4, lista 16/16 in 8 lingue (riga più larga 523 px su 529).
    • Al gate, dalla review Sol, confermati e corretti: a partita conclusa il secondo peer in fila finiva spettatore in una città già finita; se il bersaglio scelto a mano usciva, la camera andava sull'id più basso invece che sul primo; lo stacco di scena a metà dissolvenza; nomi nuovi in italiano. Dagli scatti: righe della lista che perdevano il quartiere, e le partite piene sopra quelle in cui si può entrare.
    • ⚠️ Cambia il comportamento: le partite pubbliche già iniziate tornano visibili a chi cerca, e chi entra col codice in una partita in corso diventa spettatore invece di essere rifiutato.
    • Non provati: EOS vero (stato pubblicato, ricerca, posti contati con uno spettatore dentro), il tocco su un telefono, i pulsanti LISTA e RAPIDA premuti davvero, uno spettatore mentre qualcuno è nel tunnel; lo scenario B non è stato rilanciato dopo il secondo giro.
    • Terzo giro, dopo il congelamento delle 02:50, dalla seconda review Sol (R926b), confermati e corretti. (1) Se gli ultimi vivi collassavano mentre nasceva il mondo dello spettatore, lui restava nella città finita: riprodotto con lo scenario D, ancora in città dopo 30 s. Ora, appena la sua città è pronta, l'host gli rimanda i punteggi finali con la RPC già esistente (World.gd), e in 308 ms è in lobby (D 3/3/2, A 10/7/22, C 5/2/3/3). (2) Una seconda ricerca EOS vuota svuotava l'elenco delle lobby unibili, e l'ingresso restava solo P2P: ora una ricerca vuota non tocca l'elenco precedente (EOSBridge.gd, sonda a lobby finte 3/3). Resta un caso: se l'host torna in lobby entro ~1,5 s dalla fine, lo spettatore aspetta nella sua città fino al via successivo.
    • ⚠️ Rischi residui dichiarati dalla terza review Sol (R926c), da provare su EOS con due telefoni: se l'host è già tornato in lobby quando lo spettatore finisce di caricare, i punteggi non arrivano e lo spettatore aspetta il via successivo; e il P2P parte senza aspettare l'esito di join_async (così era già prima di SU-926), quindi con una lobby sparita nel frattempo si può entrare fuori dalla lobby EOS. Nessun quarto giro: sono casi limite che solo EOS vero mostra.
  • SU-932 — CAMBIA NOME su iPhone e iPad: il tocco apre davvero la scrittura. In files/homeless_city/scripts/ui/MainMenu.gd il campo del nome ha virtual_keyboard_enabled e virtual_keyboard_show_on_focus esplicite e un handler del tocco, che prende il fuoco e, se il campo non è in scrittura, chiama edit(). Da Godot 4.4 fuoco e scrittura sono due stati distinti: senza edit() la tastiera non sale e i tasti si perdono. Se dopo 2 frame l'altezza della tastiera è ancora 0 e il dispositivo ne ha una, parte DisplayServer.virtual_keyboard_show(). Log [SU-932] a righe singole, per leggere il giro dall'iPhone. Sonda probe_su932_tastiera.gd 20/20: desktop invariato, «iOS» con altezza 0 → una chiamata, «Android» con 300 px → nessuna, tocco più clic emulato → un giro solo. Lotto architetto-opus Z932; diagnosi di partenza dal consulto Codex Sol D932.
    • 📌 Resta la scelta «niente autofocus su touch» entrando nella schermata: il motivo è scritto nel codice (la tastiera coprirebbe mezzo schermo prima che si legga).
    • ⚠️ Non provati: la tastiera vera su iPhone e iPad, il riposizionamento del pannello con l'altezza vera, l'Honor. Il campo INVITA UN AMICO (SU-792) potrebbe avere lo stesso difetto: non toccato.
  • SU-927 — i codici della beta, solo dove c'è la beta. In IMPOSTAZIONI un cartello CODICE apre la schermata CODICE BETA (MainMenu.gd, chiavi CODICI_BETA_* in ui_menu.csv, 8 lingue). TUTTIPERSONAGGI apre i 14 personaggi, TUTTIMONDI i 7 quartieri, senza lattine né esclusivi; maiuscole e spazi non contano, e ogni errore dà lo stesso «Codice non valido.». Il gate non è un tag di export (i preset sono gli stessi per stage e live) ma Env: credenziali stage o dev, fonte non di default, mai credenziali live, mai demo; le credenziali live battono qualunque override. Il segno cheat sta in user://beta_codes.cfg, non torna mai falso, e files/homeless_city/scripts/autoload/RunRecorder.gd lo mette in run_start solo quando è vero. Sonda probe_su927_codici.gd: 70 controlli verdi, compresi il giro live (nessun cartello, codici rifiutati, niente cheat) e il riavvio. Stesso lotto Z932.
    • Al gate, dalla review Sol: se il segno non si salva lo sblocco non parte (fail-closed, stesso messaggio neutro), provato con un salvataggio fatto fallire davvero dalla sonda.
    • ⚠️ Il segno NON viaggia con ProgressSync, gli sblocchi sì: un secondo dispositivo sincronizzato riceve personaggi e quartieri senza il segno. Da decidere se portarlo in meta.cfg.
    • Ipotesi scartata al gate: «il messaggio di successo sparisce» (in Godot 4 assegnare text da codice non emette text_changed, e lo scatto lo mostra). Limiti noti, non corretti: il cartello non si raggiunge col pad, e su desktop il primo ESC libera solo il fuoco del campo.
    • Il criterio 2 (provino dell'export live) è sostituito dal giro live della sonda con l'override di Env.
  • SU-924 — in multiplayer il rifugio non tocca più l'orologio. In files/homeless_city/scripts/world/Interactable.gd, _use_shelter() dà stats, XP e suono e poi, in MP, esce prima del risveglio alle 07:00. Sonda probe_su924_rifugio.gd (tools/autotest/probe_su924_mp.sh, ENet a due peer): sul codice vecchio host e client saltavano alle 07:00 del giorno 2 (+1390 min); dopo, host 08:19→08:19 e client 08:15→08:15, 24 letture in 12 s senza salti, stats a 100, XP +3, cooldown riavviato. SP invariato. Lotto architetto-opus Z924.
    • 📌 In MP il cooldown del rifugio è 300 s, non i 600 del ticket: è la scelta di SU-30 (files/homeless_city/scripts/world/WorldGenerator.gd:2440-2449), non toccata.
  • SU-925 — quando l'host esce dal menu, gli ospiti lo sanno subito. In files/homeless_city/scripts/autoload/NetworkManager.gd il leave() dell'host manda un congedo HOST_LEFT sulla RPC già esistente _cl_join_denied, stacca il peer da multiplayer senza chiuderlo e lo tiene vivo finché gli ospiti chiudono (1,5 s al massimo). Il client mostra MP_HOST_USCITO, chiave nuova in ui_gioco.csv in 8 lingue. Misurato su ENet: avviso a schermo in 4 ms; host ucciso con kill -9, «disconnesso» dopo 27,8 s come prima. Nessuna RPC nuova (24 prima e dopo), quindi la versione minima MP non si alza. Stesso lotto Z924.
    • Scartate: un timer prima di _quiet_close (MainMenu richiama leave() durante il cambio scena e il pacchetto muore), e flush più chiusura immediata (ENet scarta la RPC che arriva nello stesso giro della disconnessione).
    • ⚠️ Trovato: i 30 s di PEER_TIMEOUT li usa solo l'host per gli ospiti muti. Il client, se l'host sparisce, aspetta il timeout del trasporto: portarlo a 10 s non accorcerebbe niente, serve un controllo lato client (non fatto).
    • Ipotesi scartata al gate: la review Sol segnalava un «emoji» nell'avviso (files/homeless_city/scripts/ui/HUD.gd:3781). È preesistente, e U+26A0 è l'icona pixel triangolo_avviso di StreetU-Icons.ttf.
    • Non provati: EOS, 60 s su rete mobile, chiusura della finestra con ⌘Q, un ospite già tornato in lobby (SU-876).
  • SU-929 — al Mercato l'ombra della fontanella a raso torna sotto i getti, e l'ingombro è il suo. In files/homeless_city/scripts/world/WorldGenerator.gd il ramo raso ha scarto e scala propri (OMBRA_FOUNTAIN_RASO_OFFSET, OMBRA_FOUNTAIN_RASO_SCALE) e raggi propri in _add_interactable(): collisione 13 invece di 22, prompt 24 invece di 36. Il fontanile resta la riga di sempre. Lotto dev-sonnet Z929, due giri.
    • Misure: ombra da +33,8 px di gioco sotto la base a −1,48 px (tre marker magenta piazzati nel mondo, tools/autotest/misura_su929.py sugli scatti di shot_su929_ombra.gd); il barbone arriva a 13,6 px dal bordo, 21,6 dal centro, col prompt visibile (probe_su929_ingombro.gd); scatti del fontanile prima e dopo identici byte per byte.
    • ⚠️ _blocked_spots non è cambiato: è un elenco di soli punti che decide dove NON nascono gli altri oggetti in generazione (non ferma il barbone), letto da quattro funzioni con margini diversi. Un raggio per punto cambierebbe la densità di bidoni, lampioni, bagni e bancarelle ovunque.
    • 📌 Al gate: lo scarto non era archiviato (il cancello Luna lo dava NON MISURATO), quindi lo script è stato rilanciato sugli scatti prima/dopo. Nel primo giro il builder aveva lasciato fuori i raggi credendo _add_interactable() fuori perimetro; i .uid delle due sonde li ha copiati l'orchestratore dalla copia importata.
    • ⚠️ Trappola della sonda: la finestra esce 4288×2412 invece di 844×390, e la scala vera è ~30,5 px di schermo per px di gioco. Per questo si misura coi marker nel mondo, non con la trasformazione della camera.
  • SU-931 — il gioco sull'iPhone Duo, misurato dal viewport. Provini nuovi: files/homeless_city/scripts/tools/shot_su931_partita.gd (HUD in partita vera e mappa), shot_su931_menu.gd (menu, lobby 8/8, negozio) e la sonda probe_su931_resize.gd (2670×1878 → 2034×1398 → 2670×1878 sulla stessa istanza del mondo), coi lanciatori in tools/autotest/. Canvas logico 1091×768 e 1117×768: nessun taglio né sovrapposizione negli elementi misurati, resize senza restart, safe area ricalcolata a ogni passaggio. Nessun file di gioco toccato. Lotto dev-sonnet Z931.
    • ⚠️ Trappola: il Mac riporta la finestra oltre schermo alla forma 16:9 in modo asincrono, dopo ~90 frame; un controllo a 4 frame non se ne accorge, quindi la sonda riafferma la misura prima di ogni scatto.
    • ⚠️ Il Duo vero oggi non mostrerebbe queste misure: sul Mac ci sono Xcode 26.6 e l'SDK iOS 26.5, e secondo le note Apple citate dal lotto un'app compilata prima dell'SDK 27 gira in un riquadro da iPhone con le bande nere. Per lo schermo pieno servono Xcode 27.1 e un Info.plist senza UIRequiresFullScreen: decisione di Ivan, nessun file toccato.
    • 📌 Scartati al gate, dopo l'istruttoria Luna I931 e gli scatti guardati: il banner «largo 1341 px fissi» (la larghezza la calcola files/homeless_city/scripts/ui/HUD.gd:5278-5302 dal viewport, e nello scatto il testo non è tagliato: probabile residuo del clamp del Mac) e il titolo lungo di una carta che va a capo (riquadro fisso di 156 px in files/homeless_city/scripts/ui/LevelUpScreen.gd:45-46, uguale a ogni risoluzione: non è del Duo). Il cancello ha notato che la scansione delle sovrapposizioni copre coppie scelte, non tutti i Control: compensato guardando i PNG.

RIENTRI ATTESI (messi In revisione il 2026-09-11, Sprint 14, sprint di notte da CLI): undici chiavi — SU-922, SU-923, SU-924, SU-925, SU-926, SU-927, SU-928, SU-929, SU-930, SU-931, SU-932.

Epic fa da collante: un solo salvataggio sincronizzato, e il server che libera i nomi funziona davvero (SU-920, SU-917, SU-921)2026-09-11

Ivan in chat, dopo la prova su device: «in sostanza per me epic deve fare da collante e diventare l'unico gestore di salvataggio, che va sincronizzato tra i dispositivi», e «se io mi collego prima con apple e poi faccio collega epic dovrebbe fare la stessa cosa». Tre decisioni, date a domanda e scritte su SU-912:

  • l'ordine dei collegamenti non conta, alla fine c'è un solo giocatore;
  • i progressi si uniscono con la regola di SU-787;
  • resta il giocatore con accessi non spostabili da quel dispositivo, e se nessuno dei due ne ha, quello col salvataggio più recente.
  • SU-920 — il salvataggio si sincronizza davvero. Due difetti letti nel codice: la nuvola si leggeva solo entrando a mano dalla schermata Account, e ogni scrittura sovrascriveva quella remota, quindi vinceva l'ultimo che scriveva. Ora:
    • ProgressSync legge, fonde e poi scrive; se la lettura fallisce rinvia, non sovrascrive;
    • rilegge una volta per avvio quando la sessione torna viva da sola, con l'avviso di SU-788.

      La review di Codex ha trovato altro, corretto nel lotto NSY2:

    • letture e scritture partivano anche senza la sessione dell'account viva (col PUID dell'ospite);
    • la fusione si applicava a partita in corso;
    • la riga ULTIMO SALVATAGGIO restava ferma;
    • il segnale di rientro stava nel ramo sbagliato;
    • la sonda sovrascriveva i cfg veri.

      Scartato: due dispositivi che scrivono nello stesso istante. Player Data Storage non ha scritture condizionate, e chi perde la corsa rifonde i suoi progressi alla scrittura dopo.

      Sonda files/homeless_city/scripts/tools/probe_su920_progress_sync.gd: 37 controlli verdi, controprova rossa, cfg della HOME di prova rimessi con le stesse impronte. Lotti Codex Sol (NSY, NSY2).

  • SU-917 — il server dal vivo, tre intoppi uno dietro l'altro.
    1. Scope mai autorizzati. La prova su device rispondeva «identita non verificata» anche con token veri. Eseguendo _rvsJwks dall'editor: «You do not have permission to call UrlFetchApp.fetch». Il doGet di prova non l'aveva mai fatto notare, perché non usa servizi nuovi.
    2. Il 403 senza motivo. Dopo l'autorizzazione, «firestore 403». Il .gs ora riprova senza l'header del progetto quota e riporta il motivo di Google: «Request had insufficient authentication scopes». Il test del verificatore sale a 53/53.
    3. Consenso parziale. Nella schermata di Google non erano spuntati tutti i permessi, e l'editor non ripropone più il consenso. Tolto l'accesso dall'account Google e riautorizzato spuntando tutto, il server risponde {"ok":true,"esito":"gia libero"} a un token vero di dev.

      I permessi IAM dell'account di Ivan li ha provati la stessa lettura col token di gcloud: 404, con e senza header. Le Script Properties le ha provate un confronto di impronte, senza stamparle.

      Dal log del gioco preso dall'iPhone (xcrun devicectl device copy from): il primo trasloco della notte era quello nel verso sbagliato che Claude aveva proposto prima di capire l'obiettivo (Epic spostato da Divano al giocatore Apple). Nessun nome si è liberato, grazie proprio all'intoppo 1.

  • 🔄 SU-921 — l'ordine non conta. Lotto in costruzione su Codex Astra (UNI). Si installa sui device insieme a SU-920.
  • ⚠️ Non provati: una liberazione vera su stage, e il giro dei salvataggi Honor↔iPhone. Servono la build con SU-920 e SU-921, e un COLLEGA sull'iPhone.

Il negozio a lattine d'oro: le punk a 10, un piccione che ti segue a 5 (SU-793)2026-09-10

Sprint delle 20, seconda onda: lotto Codex Astra N793, più il giro F793 (Sol) sui rilievi della review. Le decisioni di Ivan sono già sul ticket: scala 1/5/10, niente cappellino, piccione visibile anche agli altri in multiplayer. Default dell'orchestratore, dichiarato: un solo acquisto da 10 sblocca tutte e due le punk.

  • La sezione «solo su invito» nel negozio delle skin (files/homeless_city/scripts/ui/ExclusiveSkinShop.gd, aperta da files/homeless_city/scripts/ui/MainMenu.gd): saldo di lattine d'oro, coppia PUNK a 10, PICCIONE a 5, interruttore «il piccione mi segue». L'ospite vede saldo 0 e non può comprare. Testi in 8 lingue.
  • Le punk escono dal negozio a lattine normali (files/homeless_city/scripts/autoload/Skins.gd): non si comprano più coi 150, ma chi le aveva già sbloccate le tiene. Nessun altro prezzo cambia.
  • Il piccione (files/homeless_city/scripts/player/CosmeticPigeon.gd) segue il barbone con le pose del piccione normale: non entra nel gruppo "piccioni", non ha collisione, non dà XP né soldi. In MP viaggia come suffisso |p nella stringa della skin già replicata (NetworkManager.gd, Player.gd): nessuna RPC nuova, stringa massima 24 byte.
  • ⚠️ Tre rilievi della review R793, chiusi da F793:
    • la spesa d'oro stava in un contatore locale che la fusione fra dispositivi non vede, mentre gli sblocchi si uniscono: con due dispositivi offline si spendeva due volte lo stesso saldo. Ora il saldo si ricava da ciò che si possiede (oro guadagnato meno i prezzi degli esclusivi posseduti, mai sotto zero), e il contatore oro_speso di Inviti.gd non c'è più;
    • pigeon_enabled stava nel file fuso per OR, quindi spegnere il piccione non resisteva alla fusione. Ora vive in user://inviti.cfg, che non si fonde;
    • il pannello riceveva le pressioni ripetute di un tasto tenuto premuto; ora le ignora.
  • ⚠️ Al gate, a occhio: nel provino il titolo e le frecce della scelta personaggio si leggevano dietro «SOLO SU INVITO». Correzione in _open_exclusive_shop: la schermata skin si nasconde finché il negozio è aperto, e la chiusura la ricostruisce.
  • Prove rilanciate qui:
    • probe_su793_negozio.gd: 44 verifiche su 44, compresi i due dispositivi offline e la preferenza locale contro un remoto acceso;
    • probe_su791_inviti.gd e probe_su792_inviti.gd ancora verdi;
    • compile-check 10 su 10;
    • provino files/homeless_city/scripts/tools/shot_su793_negozio.gd, scatti in TMP/su793_negozio/.
  • ⚠️ NON FATTO: il badge da 1 lattina d'oro, che aspetta l'approvazione di SU-919. NON PROVATO: il piccione visto da un secondo giocatore, in una partita vera a due processi con EOS.

RIENTRI ATTESI (messi In revisione il 2026-09-10, Sprint 14, seconda onda dello sprint delle 20): quattro chiavi — SU-918, SU-794 (riaperta e rimessa In revisione), SU-919, SU-793.

  • Tutte e quattro sono nate, o si sono sbloccate, da decisioni prese da Ivan in chat alle 22.
  • 📌 Tre difetti presi al gate e non dai builder: il pannello del negozio che lasciava vedere la schermata sotto; la spesa d'oro che si poteva fare due volte; la scadenza dei riscatti che il client poteva spostare.
  • ⚠️ Due comandi in background uccisi dal sistema per memoria bassa, con tre copie Godot importate più export, provini e quattro codex exec: l'A-B-A di SU-918 è rimasto a metà, e la sonda era rimasta accesa sull'Honor. Il telefono è stato rimesso sulla 0.43.
  • 📌 I commit non sono stati pushati: git push origin dev aspetta l'ok di Ivan.

Sprint delle 20, seconda onda: la notte senza il ciclo sulle pozze, gli inviti con l'impronta al posto del PUID, il badge da approvare (SU-918, SU-794, SU-919)2026-09-10

Ivan in chat, con la settimanale di Claude al 90% e il reset alle 23: «se ci sono altri task da chiudere facciamolo». Nessun ticket dello sprint si poteva lavorare senza una sua decisione, e le ha prese tutte insieme: SU-918 entra nello sprint, su SU-794 prima si pseudonimizza, il piccione di SU-793 si vede in multiplayer. Le decisioni sono scritte sui ticket. Tutto costruito su Codex.

  • SU-918 — luce e tinta delle pozze calcolate una volta per cella (files/homeless_city/scripts/world/World.gd, lotto Codex Astra N918). Una passata GPU a risoluzione di cella scrive intensità e tinta in una texture piccola, e la composita la legge NEAREST invece di ciclare sulle sorgenti per ogni frammento. Tetto 24, lanterna, vignetta, lampo, calura e gelo intatti; la sonda Su911Perf.gd spegne anche il viewport delle celle nella fase «tinta spenta».
    • Look: provino bash tools/autotest/shot_su918_notte.sh prima e dopo nello stesso punto (via, lampione, neon). Nessuna differenza visibile; la differenza pixel sta fra lo 0,7 e l'1,9% ed è concentrata sugli NPC che si muovono; nessun SHADER ERROR. Scatti in TMP/su918_notte/prima/ e TMP/su918_notte/dopo/. Il builder dichiara una differenza che a occhio non si vede: la tinta quantizzata a 8 bit, al massimo 1/255 per canale.
    • ⚠️ Fps: l'A-B-A a build appaiate si è interrotto. Il Mac ha fermato i processi per memoria bassa a metà della misura della build nuova, e la run «prima» era partita più fredda (29 °C contro 35).
    • La prova che regge sta dentro la stessa run: pozze accese contro spente valeva +6,4 e +5,5 fps nella build vecchia, circa 0 nella nuova (38,4 contro 38,2); il salto giorno→notte scende da 15-17 a 11 fps. Grezzi in TMP/su911/misura_20260910_223409_683332/ (prima) e TMP/su911/misura_20260910_223926_098964/ (dopo, parziale).
    • Review Codex R918: nessun problema concreto.
  • SU-794 (b) — i riscatti degli inviti senza il PUID in chiaro (lotto Codex Sol N794c; files/homeless_city/scripts/autoload/Inviti.gd, tools/firestore/firestore.rules):
    • l'id del documento è l'impronta SHA-256 di su-inviti: più il PUID;
    • il codice invito deriva dall'impronta e non rivela più un pezzo del PUID;
    • il campo scade, a 365 giorni, è obbligatorio;
    • l'anti-autoinvito resta nelle regole, confrontando il codice con la coda dell'impronta.

      Le sonde probe_su791_inviti.gd e probe_su792_inviti.gd, rilanciate qui, confermano: path pseudonimo sì, PUID in chiaro no, ESITO=OK.

    • Review Codex R794c. Tre rilievi «alti» sono scartati perché nessuna build con gli inviti è uscita: codici vecchi che non contano più, riscatti con id=PUID non aggiornati, client vecchi rifiutati dalle regole nuove. Corretto il rilievo vero: creato veniva dall'orologio del client, che poteva così spostare avanti la cancellazione; ora le regole lo vogliono entro 10 minuti da request.time.
    • ⚠️ NON FATTO, e tocca a Ivan in quest'ordine:
      1. pubblicare le regole insieme alla build che contiene il commit, non prima;
      2. attivare il TTL sul campo scade. Il comando suggerito da Codex è NON VERIFICATO: gcloud firestore fields ttls update scade --collection-group=riscatti --enable-ttl --project=<progetto> --database='(default)';
      3. solo dopo, l'informativa nelle due copie (TMP/su794/privacy_index.html) e i moduli degli store (TMP/su794/moduli_store.md), bozze già riscritte sul sistema nuovo.

        I 12 mesi di conservazione sono un default dell'orchestratore, da confermare.

  • SU-919 — il badge «solo su invito», tre varianti da approvare (lotto Codex Astra G793): spilla smaltata a lattina dorata con VIP, medaglia di cartone col nastro, toppa rammendata con due lattine che brindano. Scheda in TMP/su_G793/su793_badge/scheda_approvazione.png. La Baracca oggi non ha un riquadro dove esporre uno sprite, perché gli acquisti sono righe di testo: 32×32 è una proposta. Il Fatto di SU-919 è l'approvazione di Ivan, e SU-793 monta il badge solo dopo.

La notte sull'Honor misurata: la composita del WorldTint costa 12 fps, e il fix è SU-918 (SU-911)2026-09-10

Sprint delle 20, secondo lotto. Sonda e strumenti li ha costruiti Codex (Astra N911, Sol F911); la misura è stata fatta sull'Honor 10 collegato al Mac, in due run.

  • La sonda di misura files/homeless_city/scripts/dev/Su911Perf.gd parte SOLO con la feature di export su911_perf (o in debug con SU911_PERF=1). Entra da sola in città (singolo, Il Centro, seme 911001), tiene fermo e immortale il barbone e fa nove fasi da 6+26 s: GIORNO_A, NOTTE_A, NOTTE_POZZE_SPENTE, NOTTE_TETTO_12/6/1, NOTTE_TINTA_SPENTA, NOTTE_B, GIORNO_B. Giorno e notte li forza dall'orologio di GameState, quindi il look normale non cambia.
    • Ganci di poche righe in Boot.gd, TestBot.gd e GameState.gd; in EOSBridge.gd, OnlineLeaderboard.gd e RunRecorder.gd le guardie che spengono login EOS, classifica e telemetria.
    • ⚠️ Gli argomenti da riga di comando non arrivano alla build Android (SU-414): per questo serve una feature di export, non un flag.
    • Prima di installarla sul telefono di Ivan, istruttoria Codex I911: nessuna scrittura su Firestore, Apps Script, classifica, telemetria o EOS. Restano la lettura di devices.json e l'iscrizione al topic FCM, che la 0.43 fa già.
  • Gli strumenti. tools/android/esporta_apk_su911.sh esporta l'APK firmato con la feature e rimette il preset (verificato identico). tools/android/misura_notte_su911.py legge il contatore Huawei [disp 0] frame: in tre finestre da 8 s per fase e scrive risultati.md coi confronti A/B.
  • I sei rilievi della review R911, chiusi da F911:
    • controllo che l'APK sul telefono sia quello atteso;
    • fase non valida se fps_motore scende sotto la metà degli fps del compositore;
    • le nove fasi controllate per nome e ordine;
    • preset controllato anche in staging;
    • strumenti eseguibili;
    • soprattutto, una guardia nel pre-volo di tools/release/export_all.sh: se export_presets.cfg contiene su911_perf, l'export si ferma. Senza, un export di misura ucciso a metà avrebbe lasciato nel preset una feature che fa partire la sonda da sola nelle build dei tester.
  • 📊 I numeri (fps del compositore, run 1 → run 2):
    • GIORNO_A 49,9 → 49,4; NOTTE_A 32,8 → 33,8;
    • pozze spente 39,2 → 39,2;
    • tetto 12/6/1: 36,3/36,7/38,7 → 36,7/37,4/38,2;
    • tinta spenta 45,3 → 46,3;
    • NOTTE_B 31,4 → 31,3; GIORNO_B 46,2 → 40,4.

      Grezzi e risultati in TMP/su911/misura_20260910_205529_014360/ e TMP/su911/misura_20260910_210348_522244/.

  • ⚠️ NON VALIDO alla lettera il criterio delle due A del giorno. Nella run 1 la differenza è 3,6 fps (batteria da 37 a 40 °C). Nella run 2 è 9,1, per un crollo transitorio nella prima finestra di GIORNO_B: 28,8 fps, poi 46,3 e 46,0. Le due NOTTE sono valide in entrambe le run (1,4 e 2,4). Una terza run identica non è stata fatta, perché fallirebbe per la stessa causa nota: se serve il numero stretto va cambiata la sequenza (una corta, solo GIORNO/NOTTE/GIORNO), non ripetuta.
  • 📌 Il colpevole è la passata composita del WorldTint: spegnerla dà +12,5 fps in tutte e due le run. Il costo cresce poco col numero di pozze: da 22 a 12 si recuperano 2,9-3,5 fps. La scomposizione del consulto di Codex Astra (D911), da prendere come ipotesi: circa 5,7 ms legati alle pozze, circa 3,4 ms fissi con la sola lanterna, circa 1,2 ms di resto, sotto la deriva. Proposta: luce e tinta calcolate una volta per cella light_pixel in una texture, invece del ciclo per pixel.
  • 📌 Ticket di fix creato in backlog: SU-918, coi numeri, la proposta, i criteri A-B-A a build appaiate e il NON TOCCARE del look di SU-16.
  • 📌 L'Honor ha di nuovo la 0.43 che aveva (stessi 181.778.001 byte, stesso certificato): la sonda non resta sul telefono.

RIENTRI ATTESI (messi In revisione il 2026-09-10, Sprint 14, sprint delle 20 da CLI): due chiavi, SU-794 e SU-911.

  • Perimetro di partenza: 11 ticket in «Da fare»/«In corso», nessun bocciato. Lavorabili stasera 3: SU-911, SU-794, SU-795 (solo la bozza). Gli altri sono fermi per decisione scritta, hardware (il Redmi) o dati (la 0.44 non ha ancora un tag).
  • ⚠️ Tasso di rientro del giro precedente: 0 su 7 (SU-687, SU-707, SU-907, SU-787, SU-792, SU-902, SU-788). È ancora uno zero vero per costruzione.
  • 📌 Due difetti trovati al gate e non dai builder: il regolamento di SU-794 che andava a capo sotto i bottoni mentre le misure delle Label erano giuste, e l'export di misura che poteva lasciare la sonda nelle build di release.
  • 📌 Il giro si è fermato con la settimanale di Claude al 90%, a ticket lavorabili chiusi.
  • 📌 I commit non sono stati pushati: git push origin dev aspetta l'ok di Ivan.

Il regolamento degli inviti in due righe, e un'informativa che così non si può pubblicare (SU-794, SU-795)2026-09-10

Sprint delle 20 da CLI, costruito tutto su Codex: la settimanale di Claude era all'89%, quella di Codex al 54%.

  • SU-794 punto 3 — due righe di regolamento nella schermata «Invita un amico», sempre visibili, anche all'ospite: «Premi solo estetici: al massimo 10 lattine d'oro» e «Nessun valore reale e nessun legame con le recensioni». Chiavi ACCOUNT_INVITI_REGOLA_COSMETICA e ACCOUNT_INVITI_REGOLA_VALORE in 8 lingue (files/homeless_city/assets/translations/ui_menu.csv); bottoni, campo del codice e avviso scendono per far posto, in _build_account_inviti_screen (files/homeless_city/scripts/ui/MainMenu.gd). Lotto Codex Sol (N794b).
  • ⚠️ Il builder aveva la misura giusta e il testo sbagliato. Riportava ogni riga a 564×16 px e «nessuna sovrapposizione geometrica», vero per i rettangoli. Il provino al gate ha mostrato la seconda riga andare a capo in italiano e in russo al telefono, con la parola di sotto nascosta dietro COPIA/CONDIVIDI. Il testo è stato accorciato in tutte le lingue a quello che il ticket chiede: il «né soldi né lattine normali» era un'aggiunta del brief, non del ticket.
  • Un provino che risponde alla domanda in numeri: files/homeless_city/scripts/tools/shot_su794_regole.gd eredita da quello di SU-792, conta le righe di ciascuna Label del regolamento e fallisce se una va a capo. bash tools/autotest/shot_su794_regole.sh: 22 misure su 22 a una riga (8 lingue a 844×390; it, fr, ru a 1024×768), scatti in TMP/su794_regole/.
  • ⚠️ NON FATTO: informativa e moduli degli store. Le bozze ci sono (TMP/su794/privacy_index.html, TMP/su794/privacy.diff, TMP/su794/moduli_store.md, TMP/su794/dati_inviti.md; lotto Codex Sol N794a), ma non si pubblicano. L'istruttoria ha trovato che:
    • i riscatti di SU-791 sono leggibili da chiunque senza autenticazione (tools/firestore/firestore.rules, righe 263-266: allow read: if ambiente_noto(env), che vale anche per le query);
    • l'id di ogni documento è il PUID completo di chi è stato invitato, e il codice deriva dagli ultimi 8 caratteri del PUID di chi invita;
    • non esistono né scadenza né cancellazione.

      Scriverlo onestamente vuol dire togliere dall'informativa «l'unica cosa che diventa pubblica» e «cessione di dati: mai». La scelta fra pubblicare così e prima pseudonimizzare l'id (hash del PUID) con un periodo di conservazione è chiesta a Ivan sul ticket: nessuna build con gli inviti è ancora uscita, quindi cambiarlo adesso non costa migrazioni.

  • 📌 SU-795 — bozza della richiesta al supporto Epic sulla Web API delle Stats, in inglese, in TMP/su795/richiesta_epic.md. La manda Ivan: il canale non è scritto nel repo.

Dopo la prova del trasloco: il nome resta a chi ce l'ha, e quello del vecchio giocatore si libera solo con la firma di Epic (SU-914, SU-915, SU-916, SU-917)2026-09-10

Ivan in chat, dopo l'esito della prova su device: «fai tutto ciò che è da fare per risolvere», e subito dopo «intendo il discorso qui sopra sulla transizione di account … solo quelli di pertinenza». Le due decisioni rimaste aperte hanno preso le strade raccomandate, scritte su SU-912.

  • SU-914 — entrare con un canale non rinomina più nessuno. Sull'iPhone l'ingresso con Epic aveva trasformato «Divano» in «ITABlackwind_000», perché claim_provider_handle guardava solo lo stato locale, che su un dispositivo nuovo non sa niente del nome. Ora legge prima la riga players fresca:
    • nome suo (riserva vuota) → adottato, nessuna claim;
    • prestito o riga assente → come prima (SU-505);
    • lettura fallita → muto, nessuna claim.

      Sonda files/homeless_city/scripts/tools/probe_nome_esistente.gd: 4 casi su 4, rilanciata al gate con HOME nuove; controprova rossa senza la lettura (2 claim, nome diventato ITABlackwind_565). Lotto Codex Sol (NEX).

  • SU-915 — il PUID mascherato mostra le ultime 4 cifre. Le prime erano «0002» per tutti: nei log dell'Honor il vecchio e il nuovo giocatore erano indistinguibili. Correzione a mano, una riga.
  • SU-916 — l'export boccia una EncryptionKey assente o sbagliata, sul file dell'ambiente prima di esportare e sul cfg impacchettato dopo, senza mai stamparla. tools/release/export_all.sh --solo-chiave <file> la prova da sola: vuota, 63 caratteri e non esadecimale bocciate, valida passa, chiave mai nell'output; i quattro file di credenziali veri passano. Lotto Codex Sol (EKY).
  • SU-917 — il nome del vecchio giocatore lo libera un server che verifica chi lo chiede. La prova su device aveva mostrato la liberazione rifiutata dalle regole Firestore, e il rifiuto è giusto: le chiamate al registro non portano identità, e in stage e in live un delete da client vorrebbe dire nomi portati via da chiunque. Le regole restano come sono.
    • La prova d'identità è l'ID token Connect del vecchio giocatore, firmato da Epic: il client ce l'ha solo durante il trasloco, entrato nel vecchio giocatore e prima dell'unlink. Resta in una variabile locale, mai nel log né su disco.
    • Il server è l'Apps Script del registro: azione release_verificato. Firma RS256/RS512 verificata a mano con BigInt contro le chiavi pubbliche di Epic, poi sub, scadenza, emittente e pfdid dell'ambiente. Solo dopo, da amministratore, cancella la prenotazione se è intestata a quel PUID e svuota la riga players, in una commit con precondizione.
    • Il client: EOSBridge copia il token prima dell'unlink e a trasloco riuscito chiama NicknameRegistry.release_verified. Se fallisce lo scrive nel log, e il trasloco vale lo stesso, come deciso su SU-912.
    • Per gli orfani che non possono più entrare: tools/firestore/libera_nome.py, col service account; senza --esegui dice solo cosa farebbe.
    • Prove:
      • test del verificatore tools/firestore/prova_release_verificato.mjs: 51 su 51, controprova rossa;
      • un ID token vero di EOS dev, preso sul Mac con la sonda files/homeless_city/scripts/tools/probe_su917_id_token.gd senza stamparlo, passa con le chiavi di Epic ed è rifiutato cambiando PUID o deployment. I nomi dei claim confermano pfdid;
      • sonda offline files/homeless_city/scripts/tools/probe_su912_trasloco.gd: 12 casi su 12, controprova rossa, token mai nel log.
    • Rilievi di Codex: una verifica di sicurezza sull'Apps Script (Sol) non ha trovato modi di impersonare un altro PUID. Ha trovato due problemi, corretti nel lotto RVS2:
      • il manifest senza lo scope di PropertiesService, che avrebbe rotto anche le azioni di oggi;
      • un token falso con kid inventato che forzava una ricarica delle chiavi a ogni richiesta, tenendo il lock. Ora si verifica prima del lock e si ricarica al massimo una volta ogni 10 minuti.

        Ipotesi scartata della review del server: la riga con nome vuoto e nome_lower pieno, che le regole vietano ai client (tools/firestore/firestore.rules, righe 242-243); gestita comunque per le scritture da amministratore.

    • Review del client (Codex Sol), due rilievi.
      • *Alta, corretto a mano:* release_verified() passava da _call(), che segue l'override dell'endpoint salvato sul dispositivo e accetta anche http. Un override dimenticato avrebbe ricevuto il token firmato. Ora il token va solo a ENDPOINT_URL, compilato nel gioco, e solo se è https.
      • *Media, scartato:* la guardia del path nella sonda del token si aggira con un symlink sotto TMP/. È uno strumento che lancia solo Claude, con il path scritto da lui.
    • ⚠️ BETATESTING/ è fuori da git: il .gs e il suo manifest non si committano, come gli altri Apps Script. Lotti Codex Astra (RVS), Sol (RVS2, RVC).
  • ⚠️ Non provati:
    • su device: SU-914 (iPhone che entra con Epic), SU-915 (log di una build release), SU-917 (trasloco con il nome liberato davvero);
    • l'Apps Script dal vivo (autorizzazione, scrittura da amministratore su Firestore): serve prima il deploy di Ivan.

Il trasloco: un accesso che è già di un altro giocatore si sposta qui, invece di essere rifiutato (SU-912, SU-913)2026-09-10

Ivan in chat: «mi serve poter resettare anche il mio account o in generale cancellare un account o sovrascriverlo in epic, secondo me la prova è proprio quella che serve e anche l'implementazione». Tre scelte sue, fatte a domande singole e scritte tutte su SU-789:

  • prima la sovrascrittura, poi la cancellazione;
  • il nome del vecchio giocatore torna libero;
  • ticket e lancio subito.

Il lotto l'ha costruito Codex Astra (TRS1), il correttivo Codex Sol (TRS2).

  • EOSBridge.relocate_provider(): il trasloco. Su un accesso che è già di un altro giocatore X:
    1. rifà l'accesso al canale;
    2. login Connect, cioè entra in X;
    3. unlink_account(X);
    4. secondo login, che ora risponde InvalidUser;
    5. link_account sul giocatore corrente;
    6. NicknameRegistry.release(X);
    7. identità rimessa sul giocatore corrente.

      Vale per tutte le righe COLLEGA. Il cuore, _relocate_with_ops(), riceve le operazioni EOS come oggetto: così la sonda percorre anche il ramo riuscito, che era il rilievo della review sul lotto di SU-789.

  • ⚠️ Il segnale connect_interface_unlink_account_callback non c'è nel GDScript del binding. Come per il link, si controlla a runtime; se manca, ci si ferma prima di chiedere qualunque token.
  • Il dialogo «TRASLOCO IN VISTA»: TRASLOCALO QUI (doppia conferma entro 3 secondi) o LASCIA STARE. Sono 6 chiavi nelle 8 lingue, in files/homeless_city/assets/translations/ui_menu.csv.
  • 📌 Il gate ha trovato quattro difetti che il report non diceva, corretti nel lotto TRS2:
    • le righe nuove del CSV avevano virgole non quotate nello spagnolo, e le colonne slittavano. Ora ogni riga ha 9 campi su 9, controllati con csv di Python;
    • il dialogo riusato mostrava il titolo «SEI GIÀ DENTRO», che con la domanda non c'entrava;
    • il dialogo poteva aprirsi sopra un'altra schermata, se il giocatore lasciava ACCOUNT durante l'attesa (rilievo della review). Ora si apre solo con _current_screen() == "account";
    • il provino non sapeva fallire: gli scatti «844×390» uscivano a 4288×2412 e venivano salvati lo stesso. Ora controlla la misura prima di salvare, esce con 1 se non torna, e scrive solo sotto la TMP/ di radice.
  • 📌 Rilievi della review scartati, col perché:
    • «release fallito = trasloco riuscito» è il design approvato: il nome resta occupato e finisce nel log;
    • le costanti ERR_TRASLOCO_* in italiano sono coerenti con le vicine ERR_LINK_*.
  • Prove rifatte in locale:
    • compile-check: FALLITI 0;
    • sonda probe_su912_trasloco.gd: verde su 11 casi, rossa in entrambe le controprove (release prima del link; ramo «a metà» tolto);
    • audit emoji: 0;
    • provino files/homeless_city/scripts/tools/shot_su912_trasloco.gd: solo parziale.
      • Prima del correttivo: 4 scatti guardati. Testo e pulsanti leggibili, ma col titolo sbagliato e i «844×390» fuori misura.
      • Dopo il correttivo: un solo scatto valido, TMP/su_TRS1/shots/trasloco_it_844x390.png, a misura giusta e col titolo nuovo. A 844×390 il corpo è piccolo e il font pixel si sgrana: è lo stile del gate esistente.
      • Gli altri scatti non sono ripresi. Dopo il primo il provino si ferma senza errori: il secondo menu si carica e i frame non avanzano più. Con ogni probabilità è App Nap sulla finestra non in primo piano, non il gioco.
    • Sul provino anche un difetto del correttivo, corretto qui: un ternario con due Array letterali assegnato a un Array[String] dà SCRIPT ERROR a runtime, e il compile-check non lo vede.
  • ⚠️ NON PROVATO: unlink e link veri, il callback nativo dell'unlink, la liberazione del nome sul server, e la prova di Ivan (Honor con Google → COLLEGA EPIC col suo Epic → TRASLOCALO QUI → iPad con Epic, stesso PUID). C'è anche il Mac: il suo Device ID può essere ancorato al vecchio giocatore, e lì forse serve uscire e rientrare una volta.
  • 📌 SU-913 (DESIGN) è nel backlog: la cancellazione account completa, con l'inventario dei dati per giocatore raccolto dall'istruttoria Codex I789. Resta per una sessione nuova, perché il contesto di questa ha passato la soglia.

Epic si collega a un account Google o Apple: il pezzo che mancava al salto Android-iPhone (SU-789)2026-09-10

Ivan in chat: «cosa serve per collegare epic a google e android? ormai è un punto fondamentale», poi «ok e lancia». Lotto costruito da Codex Astra (settimanale Claude all'86%, Codex al 50%). Spunta e review su Codex; compile-check, sonda e controprove rifatti in locale.

  • EOSBridge.link_provider("epic") esiste. Epic entra in LINK_PROVIDERS, e _external_credential() ha il suo ramo:
    • su iOS riusa il token del flusso web di SU-502 (EOS_CRED_EPIC_ID_TOKEN);
    • su Android e desktop fa il solo login Auth dal portale e copia il token con copy_user_auth_token.

      La riga «COLLEGA EPIC» del menu, finora grigia, si accende da sola.

  • ⚠️ Il login Epic non si poteva riusare, e il ticket non lo sapeva. Dopo l'Auth il plugin passa da solo a Connect (files/homeless_city/addons/epic-online-services-godot/heos/hauth.gd:196). Se l'Epic non ha ancora un giocatore, ne crea uno vuoto (riga 259 dello stesso file): il collegamento si sarebbe bruciato al primo tentativo, e quell'Epic sarebbe rimasto «di un altro giocatore» per sempre. Qui si entra con auto_connect_account = false, rimesso com'era su ogni uscita.
  • ⚠️ Niente PersistentAuth nel collegamento. Sull'Honor è salvato il token Epic di Ivan del 17/08, e avrebbe scelto lui quale account collegare.
  • ⚠️ La trappola più grossa l'ha mostrata hauth.gd, non il ticket: la sessione Epic lasciata aperta dopo un rifiuto.
    • Alla scadenza di Connect il plugin rientra col token Epic (_on_connect_interface_auth_expiration_connect_account_async). Chi aveva provato a collegare un Epic altrui sarebbe diventato quel giocatore dopo un'ora, in silenzio.
    • Ora, su ogni rifiuto, si chiude il solo Auth (AuthInterface.logout con l'id appena acquisito) e si rimette l'epic_account_id di prima.
    • ha.logout_async() no: chiude anche Connect e butta fuori il giocatore Google.
    • Sul successo la sessione resta aperta, perché quell'Epic ora porta allo stesso PUID.
  • 📌 user_login_info va solo ai tipi di CRED_TYPES_CON_NOME, anche nel collegamento. Prima lo passava a tutti, e con Epic sarebbe tornato l'InvalidParameters (10) di SU-502. Le opzioni si costruiscono in _link_login_options(), una funzione pura.
  • 📌 Un errore non si spaccia più per «account di un altro giocatore». Prima qualunque risposta diversa da InvalidUser finiva in link_altrui, col PUID vuoto. Ora:
    • Success con PUID → «già tuo» o «di un altro»;
    • InvalidUser → si collega;
    • il resto → link_fallito, con numero e nome del result.
  • 📌 Review Codex R789, due rilievi.
    • Applicato: gli errori del portale non passavano da _watch_eos_login_errors. AppNotFound e ScopeNotFound finivano in un «riprova» generico e il numero di EOS non arrivava al log. Ora escono col codice che usa il login; annullo e rifiuti restano link_fallito.
    • Vero ma non rifatto: la sonda è offline e non percorre il ramo riuscito (copia del token, link accettato). Quel ramo lo prova il device, ed è elencato qui sotto come NON PROVATO.
  • Sonda files/homeless_city/scripts/tools/probe_su789_link_epic.gd, con una HAuth finta: 41 verifiche, verde in locale. Diventa rossa in entrambe le controprove: togliendo il ripristino di auto_connect_account, e togliendo il guardiano di user_login_info. Compile-check locale: 3 file, FALLITI 0.
  • ⚠️ NON PROVATO: la pagina Epic vera, copy_user_auth_token riuscito, il logout del solo Auth, link_account accettato o rifiutato dal servizio, e il criterio del ticket (Google su Android + COLLEGA EPIC → Epic su iPad, stesso PUID). Serve un account Epic mai entrato in Street University: quello di Ivan ha già il suo PUID del 17/08 e verrebbe rifiutato. Proprio quel rifiuto è la terza prova da fare.
  • 📌 Aperto, per decisione di Ivan: chi ha già due giocatori separati (uno Epic, uno Google) riceve il rifiuto di SU-451. L'unica via dell'SDK è UnlinkAccount, che fa perdere al PUID abbandonato l'accesso e i progressi.

La nuvola dei cani, la città di notte, e tre animali che significavano già qualcosa (SU-907, SU-911, SU-872, SU-793)2026-09-10

Mattina dopo il giro notturno: Ivan ha risposto a quattro domande in chat, e ognuna è finita sul ticket prima di diventare lavoro.

  • SU-907, terzo giro — la nuvola torna su host e client, ed è dei cani. Il secondo giro aveva tolto l'audio del gatto dal morso, ma audio e nuvola viaggiavano nello stesso corredo soppresso: con l'audio era sparita anche la nuvola sul puppet remoto. Ora hanno due interruttori separati, play_cat_audio e use_dog_cloud: il morso passa (false, true), la GATTLING (true, false).
  • ✅ Asset nuovo dogfight.png, gemella esatta di catfight.png (192×80, 4 frame da 48×80), generata da Astra: due cani marrone e grigio scuro, musi allungati, orecchie pendenti, nessun arancione. Misurato con la sonda che verifica il nome della texture, non solo visible: morso → audio gatto 0, dogfight.png su host e puppet; GATTLING → audio gatto 1, catfight.png su entrambi. Una sonda su visible sarebbe passata anche mostrando il gatto sul morso del cane.
  • 📌 World.gd è uscito dal perimetro del brief, ed era il posto obbligato: la replica ai puppet passa dai relay RPC. Due parametri con default, nomi delle RPC invariati — e siccome in Godot 4 gli id RPC seguono l'ordine alfabetico dei nomi, non slitta nessun id.
  • ⚠️ L'arte non è ancora approvata da Ivan: è In revisione proprio per questo, e se non va il PNG si sostituisce in place senza toccare il codice.
  • SU-872 punto 2 chiuso come «causa nota, non conviene» (Ivan). Niente fix sulla pausa. Aperto SU-911 per la città di notte, dove Ivan vede scendere il framerate, collegato a SU-872.
  • 📌 SU-911 nasce con un sospetto forte, e con l'ordine di non fidarsene: la notte non è fatta con PointLight2D (strada scartata di proposito, World.gd ~7677) ma con uno shader fullscreen sul WorldTint che riceve le pozze di luce dei lampioni come uniform. Uno shader a schermo pieno che cicla sulle luci per pixel è il candidato naturale su una GPU mobile — ma di notte cambiano anche lampioni, insegne e NPC, e il ticket chiede il numero prima della colpa.
  • SU-793 — Ivan ha scelto la scala 1/5/10: un badge in Baracca per 1 lattina d'oro, le skin punk maschio e femmina per 10, niente cappellino (il gioco non ha uno slot che si veda in partita).
  • ⚠️ La torcia per 5 è stata fermata prima di diventare un KO: esiste già in gioco ed è funzionale — è l'oggetto di Brina che si raccoglie e si lancia (SU-812/813). Come premio da inviti avrebbe rotto il vincolo scritto in testa al ticket, «esclusività senza vantaggio».
  • 📌 Anche l'alternativa di Ivan, «un animale che ti segue», andava verificata, e tre animali su quattro significavano già qualcosa: il topo è un nemico del bonus metro (TOPI_PER_GIRO), il gatto è una meccanica (GameState.has_cat cambia le donazioni del busking), il cane è il superpotere del branco. Il piccione no: Pigeon.gd si dichiara «non colpibile, non dà XP, non dà soldi, zero economia». Raccomandato quello — escluso il piccione d'oro, che è un'entità di gioco a sé (SU-673).

La notifica arriva davvero, il cane smette di miagolare, e una guardia che non guardava (SU-687, SU-707, SU-907, SU-787, SU-792)2026-09-10

Giro notturno con l'Honor 10 collegato al Mac: Ivan lo ha attaccato e autorizzato disinstallazione e reinstallazione senza chiedere, il che ha fatto cadere in un colpo il cancello di due ticket fermi da giorni.

  • SU-687 + SU-707 — la push arriva sull'Honor col gioco chiuso. Build di release esportata da dev (--env stage), installata al posto della 0.43 di Play. Topic su_beta, scelto da PushNotifiche.gd::topic() che dà TOPIC_PUBBLICO solo se Env.is_live(). Prova: TMP/su687_push/notifica_honor_v2.png.
  • ⚠️ La trappola che è costata un giro intero: adb shell am force-stop BLOCCA le push. L'app finisce nello stato Android «stopped» e il broadcast FCM viene scartato dal sistema, non dal gioco (GCM: broadcast intent callback: result=CANCELLED). Si chiude con am kill, che equivale allo swipe dai recenti. Il primo giro è fallito esattamente così, e sembrava un difetto di configurazione Firebase. Vale anche per i tester: chi usa «Arresta forzatamente» dalle impostazioni non riceve più push finché non riapre l'app.
  • SU-707: nell'APK non resta traccia del vecchio plugin (SUPushPlugin/SUPushService), né nell'indice zip né nelle stringhe dei tre classes*.dex; a runtime carica GodotxFirebaseMessaging. Resta solo SUPushAutoInit, che è il ponte previsto.
  • ⚠️ Il criterio «il token nasce solo a permesso concesso» NON è chiudibile sull'Honor: Android 10 è API 29, sotto la soglia POST_NOTIFICATIONS (33), quindi il pannello non compare per design e l'iscrizione parte a permesso implicito del SO. Serve un Android 13+, che hanno i tester e noi no.
  • SU-907 (KO di Ivan: «quando azzanna sento ancora il gatto») — aveva ragione, e la causa non era il file audio. _dog_bite riusava i verbi del gatto (stun_by_cat/hit_by_cat), che si portano dietro play_sfx_cat_fight sul Player e il broadcast ai puppet: il morso del cane suonava già bene, sopra ci suonava il gatto. Il primo giro aveva misurato nove cose giuste sul morso e nessuna su *cos'altro* suonava insieme.
  • ✅ Rimedio: verbo dedicato hit_by_dog_bite, così la soppressione viaggia insieme al colpo. Un flag globale non avrebbe fermato il broadcast, e in MP il client avrebbe sentito il gatto lo stesso. Misurato: per morso cani_morso=1 e audio gatto =0, su civile e su polizia; GATTLING intatta (play_sfx_cat_fight=1, broadcast 1, nuvola presente).
  • ⚠️ Da decidere: la nuvola catfight.png ora resta sull'host ma sparisce sul puppet remoto, perché il broadcast gatto è soppresso. Il ticket parlava solo di audio, quindi non è stata toccata.
  • SU-787 (637d) — al primo accesso i progressi si fondono. read_progress() faceva «scarica e sovrascrivi», cioè proprio ciò che il ticket vieta: venti ore da ospite e poi il login cancellavano tutto. Fusione generica sui campi realmente presenti (insiemi in unione, numeri al massimo, bool in OR), non una lista di nomi a mano — quella avrebbe perso in silenzio il primo campo aggiunto domani.
  • 📌 Un'eccezione motivata: abbandoni_run_oneste_streak si fonde col minimo, perché col massimo cambiare dispositivo avvicinerebbe l'azzeramento del malus, cioè premierebbe la cosa che deve scoraggiare.
  • ✅ Misure: record fuso 999, guadagnate max(500,450)=500, spese max(20,120)=120, saldo 380; chiave per chiave meta 24+22 → unione 33 → fuso 33, perse 0; idempotenza 965/965 byte identici. MetaProgress.gd e Skins.gd non toccati.
  • SU-792 (641c) — la schermata «Invita un amico», dentro ACCOUNT: codice con copia e condividi, conteggio, saldo di lattine d'oro. L'ospite la vede ma non riscatta, così resta un regalo e non un cancello. Le lattine d'oro non passano da MetaProgress: coppia monotona in Inviti, saldo max(guadagnate − spese, 0) col tetto 10. 24 chiavi in tutte e 8 le lingue, audit emoji 0.
  • ⚠️ Tre maschere sovrapposte prima di ottenere due provini veri, e la prima è un difetto vecchio: in MainMenu._ricostruisci_testi c'era var lbl: Label = voce.get("lbl") con sotto la guardia is_instance_valid(lbl). Quella guardia non ha mai protetto nulla: l'assegnazione a una variabile tipizzata fallisce da sé su un'istanza liberata, *prima* che la guardia venga valutata — e l'errore non ferma l'esecuzione, quindi il cambio lingua lasciava i testi vecchi in silenzio. Difetto di SU-196, emerso solo ora perché la schermata inviti si costruisce e distrugge. Corretto togliendo il tipo, e nello stesso giro si espellono le voci morte: la lista cresceva a ogni ricostruzione.
  • ⚠️ Seconda maschera: il provino cambiava solo TranslationServer, ma i testi passano da I18n.t(), che legge la lingua dall'autoload. Terza: il .csv non era stato reimportato, quindi le chiavi nuove non stavano nei .translation compilati e t() ripiegava sul fallback, che è scritto in italiano — le voci vecchie cambiavano lingua, le nuove no.
  • 📌 Nel mezzo i due PNG hanno differito per il solo fondale (il menu lo cambia da sé): un cmp diceva «diversi» e stava guardando il cielo. Su un provino multilingua la prova è leggere le parole, non confrontare i byte.
  • SU-902 — il fantasma segue la skin. I 15 fogli approvati su SU-901 affettati e importati; Skins.gd guadagna ghost per tutte e 17 le skin, Player passa da path fisso a scelta per skin col barbone come ripiego, e in MP il puppet remoto prende il fantasma della skin di quel giocatore. Riusato frames_variant_path(id, kind), il meccanismo che già regge cat/drunk/heatstroke, invece di aprirne uno nuovo.
  • ⚠️ La trappola del lotto erano gli uid: generare i 15 .tres copiando player_ghost_frames.tres avrebbe copiato anche il suo uid — che sta nell'intestazione del file — e Godot ne avrebbe risolto uno solo. Tutti i fantasmi uguali, con l'aria di un bug del codice. Misurato: 15/15 uid presenti e distinti, 0 duplicati globali.
  • ✅ Griglia 5×5 su 15/15 raw (gutter minimo 7 px), 375/375 celle affettate non vuote, 15/15 sheet con hash distinto. 17 scatti in TMP/su902_ghost_shots/. m1 e f1 ripiegano sul barbone: nei due scatti si vede lo stesso fantasma, e differiscono solo per il frame dell'animazione — il che è anche la prova che il criterio del fallback funziona.
  • ⚠️ Non provato: i due peer reali. Lo scatto dal lato B è bloccato da App Nap. I raw sono entrati in LFS come puntatori (15 file, 45 righe in git).
  • SU-788 (637e) — la fusione smette di essere silenziosa. SU-787 l'ha resa corretta ma invisibile: chi entrava su un dispositivo nuovo vedeva i progressi cambiare senza sapere perché. Due avvisi big_notify glielo dicono, e N e M nascono dentro read_progress() (ProgressSync.gd:158), calcolati sui payload reali — ricalcolarli fuori sarebbe il modo di farli divergere dalla fusione che raccontano. La riga «ultimo salvataggio» usa query_metadata().last_modified_time, non un timestamp inventato. Caso vuoto provato, non asserito. 4 provini it/en in TMP/su788_avvisi/, 5 chiavi × 8 lingue, audit emoji 0.
  • 📌 Le trappole di stanotte sono finite in WORKFLOW/CAPSULA.md, non nel brief: erano già riapparse su un secondo lotto mezz'ora dopo. Nel brief sarebbero morte con la sessione. Il builder di SU-788 le ha ricevute e ha prodotto i quattro provini al primo colpo, dove i due lotti precedenti avevano speso tre giri.
  • SU-872 punto 2 — misurato il menu di pausa sull'Honor, e due premesse del ticket sono cadute. A-B-A nello stesso punto: gioco 44,7 → pausa 42,6 → gioco 44,2 fps, deriva 0,5 fps (sotto la soglia di 3, confronto valido). Metodo: delta del contatore vendor di dumpsys SurfaceFlinger su finestre di 8 s — gfxinfo e --latency non servono, il layer di GodotAppLauncher è una SurfaceView fuori dalla pipeline che tracciano.
  • ⚠️ La pausa non ha né sfocatura né pioggia: disegna 1 ColorRect full-rect senza shader, 2 Label, 3-5 Button. _build_cat_rain vive solo in MainMenu.gd — la pioggia è del menu del titolo, e il ticket l'attribuiva alla pausa.
  • ⚠️ E l'overlay non è il colpevole: costa 1,8 fps, il 4%. Il sospetto storico (overlay traslucidi = collo di bottiglia mobile, 23→34 fps) non regge per questo overlay, che è uno solo e senza shader. Il colpevole vero è che get_tree().paused ferma la logica ma non il rendering: la città continua a essere disegnata per intero sotto l'overlay.
  • 📌 Il ticket di fix non è stato aperto: il fondo del problema è la fps di città sul Mali-G72 (~40-45), non la pausa, e i 55 fps non si raggiungono intervenendo solo lì. Domanda posta a Ivan sul ticket.

RIENTRI ATTESI (messi In revisione nella notte fra il 09 e il 10/09/2026, Sprint 14, giro da CLI): sette chiavi — SU-687, SU-707, SU-907, SU-787, SU-792, SU-902, SU-788. Perimetro di partenza: 20 ticket in «Da fare»/«In corso» (query paginata, nextPageToken verificato vuoto). ⚠️ Tasso di rientro del giro precedente, misurato PRIMA dei lotti: 0 su 8 (SU-779, 780, 784, 785, 786, 791, 877, 892 — nessuna tornata in «Da fare») — ma resta uno zero vero per costruzione: Ivan non le ha ancora provate su device, ed è esattamente ciò da cui questa riga dovrebbe difendersi. L'unico rientro vero lavorato stanotte è SU-907, più i due bocciati storici SU-687 e SU-707, sbloccati dall'Honor collegato. 📌 Il cancello ha smentito i builder tre volte, e in tutte e tre il report diceva il vero mentre la prova mancava: SU-792 dichiarava i provini fatti (la sandbox Codex è headless: i PNG non c'erano), SU-902 idem per i 17 scatti, SU-788 li ha invece dichiarati mancanti da solo — l'unico dei tre a cui il brief aveva detto «non fingere di averli fatti». 📌 Restano fermi e non per dimenticanza: SU-871, SU-870, SU-781, SU-783 (misure o decisioni di Ivan), SU-790, SU-461, SU-701 (fermi per decisione scritta), SU-872 punto 1 (serve il Redmi collegato), SU-789 e SU-795 (richiedono prove a due device o una richiesta a Epic), SU-782 e SU-793 (aspettano che Ivan scelga i tre cosmetici). 📌 I sei commit non sono stati pushati: git push origin dev aspetta l'ok di Ivan.

Le regole degli inviti sono attive, e adesso è il database a dire di no (SU-791, SU-785)2026-09-09

Ivan ha lanciato tools/firestore/pubblica_regole.sh e ha generato la chiave EOS. Due criteri che stamattina erano NON PROVATO adesso hanno un numero.

  • Regole attive: ruleset c461f729-5a78-4551-a248-e167e6160164, release cloud.firestore aggiornata (HTTP 200). L'attivazione la lancia Ivan: la PATCH della release è l'azione che il classificatore nega a Claude, la validazione no.
  • Provato su stage, con le regole vive (tools/firestore/prova_inviti_stage.sh, nuovo): tre PUID riscattano il codice di A (200/200/200), due finiscono una partita (200/200), e il conteggio di A è 2 — esattamente il numero che chiedeva il ticket. A che riscatta il proprio codice: 403 PERMISSION_DENIED. La scrittura diretta del conteggio: 403 — è la prova negativa del criterio 3, quella che senza regole pubblicate non si poteva nemmeno tentare.
  • 📌 Il secondo riscatto dello stesso PUID è rifiutato con 409, non 403: lo ferma currentDocument.exists=false prima che le regole entrino in gioco. Il risultato è quello voluto (create-only), ma la causa è diversa da quella che si presumerebbe leggendo le regole, e vale la pena saperlo se un giorno quel 409 cambia.
  • ⚠️ I documenti di prova restano su stage: allow delete: if false è voluto — un riscatto cancellabile sarebbe un riscatto ripetibile. I PUID di prova iniziano con FEED per distinguerli dai dati veri.
  • SU-785, criterio 1 finalmente provato: con la chiave vera nel cfg la sonda dà interfaccia_ottenuta=true per la strada EOS.PlayerDataStorage.PlayerDataStorageInterface -> IEOS.playerdatastorage_interface_*, e la controprova col metodo inventato esce rossa con exit 1. La chiave è stata generata da CLI e scritta nel file senza passare dalla chat; il file è gitignored.

Il servizio degli inviti su Firestore: codice, riscatto, conteggio, tetto (SU-791)2026-09-09

Deciso da Ivan il 05/09 su SU-641: il premio è solo cosmetica esclusiva, quindi la frode dell'auto-invito non si impedisce, si rende inutile — chi bara si regala un cappello.

  • ✅ Nuovo autoload Inviti.gd: collezione inviti/{env}/riscatti/{puid} create-only con codice, creato, run_finita=false; l'update porta solo run_finita da false a true; il conteggio si legge con runAggregationQuery, il gioco non lo scrive mai (Inviti.gd:63-167).
  • ✅ Il codice è derivato dal PUID (8 caratteri dagli ultimi esadecimali, con 0 → Z e 1 → Y per togliere le ambiguità): invertibile, quindi verificabile senza tabella, ed è ciò che permette alle regole di rifiutare l'auto-invito. Misurato: 1.000 PUID danno 1.000 codici unici, zero collisioni, zero caratteri ambigui.
  • ✅ Tetto a 10 premiati: misurato, 12 riscatti validi danno 10 premiabili. Regole in tools/firestore/firestore.rules:90-297, accanto a quelle del registro nickname.
  • Secondo giro: al cancello il servizio risultava essere solo API, senza un call-site — nessuno lo chiamava, quindi la marcatura di fine partita chiesta dal ticket non sarebbe mai avvenuta. Ora è agganciato a RunManager.run_ended nella stessa forma di ProgressSync, con l'ospite escluso prima di qualsiasi update e sentinelle contro le ripetizioni (Inviti.gd:43-52). Misurato: con login 1 sola richiesta di update su due fini-partita, da ospite 0.
  • ⚠️ Non provato su stage, ed è la parte che conta: i tre riscatti col conteggio a 2, l'auto-invito rifiutato, il doppio riscatto rifiutato. Il lotto non aveva rete.
  • 📌 Le regole le deve pubblicare Ivan: finché firestore.rules non è pubblicato, il criterio «le regole rifiutano la scrittura diretta del conteggio» non è vero sul database, qualunque cosa dica il file nel repo.
  • 📌 La registrazione dell'autoload Inviti in project.godot è finita nel commit di SU-785/786, perché il file è lo stesso e i due lotti lo condividevano.

I progressi sull'account: la chiave di cifratura, e un trasporto che chiamava un metodo inesistente (SU-785, SU-786)2026-09-09

  • SU-785: encryption_key arriva davvero alle PlatformOptions insieme alla CacheDirectory sotto user:// (EOSBridge.gd:209-214,339-347); chiave assente o non di 64 esadecimali = sincronizzazione spenta con una riga di log, gioco identico a prima, nessun errore a schermo. Documentata in RICOSTRUZIONE_DA_ZERO.md:102-106 come credenziale da custodire per sempre.
  • 📌 La chiave non sta sul portale Epic e non è recuperabile: la genera Ivan una volta (openssl rand -hex 32) e la conserva. Persa o cambiata, ogni salvataggio già caricato diventa illeggibile.
  • SU-786: ProgressSync.gd nuovo, con scrittura, lettura a chunk e metadati di progressi.json, agganciato a run_ended e all'uscita ordinata — mai durante la partita, mai senza login. Serializzazione di meta.cfg e skins.cfg col round-trip byte per byte (2.323 B → 3.141 B).
  • ⚠️ Il difetto trovato al cancello, e perché era il tipo peggiore: il trasporto chiamava platform_interface.call("get_player_data_storage_interface"), un metodo che nel binding EOS di questo progetto non esiste — non c'è nessun get_*_interface in addons/epic-online-services-godot/eos.gd. Siccome il modulo è fail-open, non dava nessun errore: semplicemente non avrebbe mai scritto né letto niente, in silenzio, per sempre. E la sonda era verde.
  • Cura: l'interfaccia si prende per riflessione da EOS.PlayerDataStorage.PlayerDataStorageInterface, che è l'involucro ufficiale delle chiamate IEOS.playerdatastorage_interface_*, col fallback Engine.get_singleton("IEOS")/root/IEOS — la stessa forma di OnlineLeaderboard._ieos(). Guardia has_method() sui quattro metodi realmente usati, e ogni fallimento scrive quale passo è saltato, non un generico «spento» (ProgressSync.gd:233-275).
  • La sonda non è più fail-open, e lo dimostra: interfaccia_ottenuta=true con la strada usata scritta nel log, e una modalità negativa (--metodo-inesistente) che diventa ROSSA con exit 1. È la prova che adesso vede il difetto che prima le sfuggiva.
  • ⚠️ Non provato: scrittura e lettura cloud vere con un account autenticato su stage, le tre scritture consecutive e la data dei metadati. Servono credenziali EOS e rete, che il lotto non aveva.

Il bonus metro si ferma a 400/800, e dal quarto giro paga in carte e vite (SU-779, SU-780)2026-09-09

Deciso da Ivan il 05/09 su SU-768: dopo il tier 3 il bonus si ferma — massimo 400 $ di biglietto e 800 $ di incasso — e dagli zombie in poi il premio non deve essere denaro.

  • ⚠️ Il tetto sulla sola soglia NON bastava, ed è la trappola che il ticket aveva già visto: _giro_premio_banca() ricavava il giro *dividendo la soglia*, quindi fermando la soglia a 400 i tier 4+ sarebbero spariti. La cura è separare le due cose: bank_bonus_wins avanza a ogni vittoria, la soglia si ferma (GameState.gd:1705-1733,1911-1966; Interactable.gd:2984-2988).
  • Misurato: la sequenza vinta-vinta-vinta-vinta-persa-vinta dà prezzi 100/200/400/400/400/400 e giri 1/2/3/4/4/5. Una sconfitta non fa avanzare il giro. Venti tunnel dal tier 4 con raccolta completa: incasso massimo 800. Tier 1-3 identici a prima (200/400/800).
  • ✅ Monete con esponente massimo 2 — 1, 2, 4, 4… — così con 200 monete i lordi sono 200/400/800/800 (BonusRewards.gd:1199-1200).
  • Dal giro 4 la cattedra abusiva: l'esito è sempre pick_card dal ventaglio completo invece della roulette (BonusRewards.gd:1788-1824), e la revisione del gatto restituisce una vita col tetto di 9, tramite restore_lives(n, source) estratta dalla strada del rifugio (GameState.gd:489-506).
  • Misurato: 100 consegne dal tier 4 danno 100 ventagli completi, zero esiti monetari e 99 vite effettive (la centesima partiva già a 9: ricevuta sì, vita no). Nei giri 1-3 la roulette resta 56,5/23,1/16,0/4,4% contro il 58/22/15/5 atteso — scarto massimo 1,5%, dentro il 3% preteso — e zero vite in regalo. Chi perde non riceve né carta né vita.
  • Secondo giro, sul punto 5 che il primo aveva saltato: i commenti di BonusLevel.gd:41,1805 dicevano ancora che il giro si ricava «dallo scaglione della banca (100 = 1, 200 = 2, 400 = 3, 800 = 4, 1600 = 5)», e la sonda probe_su666_giro3.gd era costruita sulla stessa premessa. Corretti entrambi; rg sul progetto dà 0 occorrenze residue. La lezione di SU-624 scritta lì sopra è stata conservata, non cancellata.
  • La telemetria era già a posto, e adesso c'è la prova invece della speranza: Interactable.gd:2988 rende bank_bonus_wins + 1, BonusLevel.gd:1880-1927 lo conserva ed emette, RunRecorder.gd:1275 scrive proprio quel giro. È ciò che permetterà a SU-781 di misurare i tier 4+.
  • ⚠️ Non provati: il provino della cerimonia sul telefono e il gesto di selezione (il lotto girava headless), e la risalita reale con carta al cap o pool sotto le 3 opzioni — è simulata col contratto open_full_fan=false.
  • 📌 Una premessa del ticket non è più vera: dice «in multiplayer il bonus oggi rifiuta l'ingresso», ma BonusLevel.gd:1824-1840 non lo rifiuta più (cambiato da un ticket precedente). Nessuna RPC né pausa globale è stata aggiunta, quindi il vincolo sostanziale è rispettato — ma la frase va rivista.

In online la terza pagina del giornale È la classifica, e chi è scappato ci resta (SU-877)2026-09-09

Ivan, l'8/9: «la terza pagina non c'era». I casi veri erano tre, non i due scritti sul ticket, e il terzo l'ha trovato il provino.

  • ⚠️ (a) chi esce a metà partita spariva del tutto: non entrava mai in session_scores e l'host lo cancellava da players. Restato solo, la pagina mostrava una riga.
  • ⚠️ (b) chi collassa per primo usciva dal ramo show_gameover_after = false: nessuna terza pagina, la classifica arrivava solo alla fine condivisa.
  • ⚠️ (c) trovato dal provino, non dal ticket: anche alla fine condivisa _finish_gameover_presentation riscriveva il testo *dopo* il call_deferred("_mp_show_ranking"). La classifica compariva solo se per caso arrivava un altro punteggio finale dopo. Misurato: 1 riga in pagina prima, 9 dopo.
  • Le righe nascono dalla fusione di session_scores con NetworkManager.live_ranking (la fotografia di SU-878 che l'host già pubblica ogni 2 s): tiene i punteggi vivi, marca da sé chi esce, e non si azzera alla chiusura sessione. Zero RPC nuove (HUD.gd, NetworkManager.gd:215-2009).
  • 📌 Scartata la strada scritta nel criterio 2 del ticket («l'host tiene l'uscito in session_scores»): session_scores si riempie solo via RPC broadcast, quindi sarebbe servita una RPC in più — e in Godot 4 gli id delle RPC sono alfabetici, cioè una aggiunta ne fa slittare altre e rompe i peer di versione diversa. Mancava solo il nome per intero, che la fotografia manda a tre lettere: risolto con session_names, fotografato una volta in _cl_game_starting.
  • ✅ Pagina provvisoria al proprio collasso con altri vivi, con «ANCORA IN STRADA» e una sola voce per diventare fantasma; pagina definitiva alla fine condivisa con le due voci; chi è uscito resta in classifica marcato «SCAPPATO». Tre chiavi nuove in 8 lingue.
  • La testata diceva ancora «PAGINA 2» sopra la pagina che Ivan cercava come terza — corretto all'orchestrazione: in online HUD_GO_PAGINA3, in single resta pagina 2 davvero (SU-475), stessa guardia _was_mp_run del corpo (HUD.gd:3958).
  • ✅ Difetto emerso dagli scatti: le gemelle della riga verde (SU-446) restavano al corpo del font precedente dopo un cambio di forma, e si vedevano come una seconda copia sfalsata del testo. Corretto dove il corpo cambia.
  • 📌 Da guardare: nella pagina *definitiva* i due bottoni lasciano meno altezza e la lista scorre — in forma telefono si leggono 3 righe su 4 senza scorrere. Nella provvisoria, con una voce sola, si vedono tutte e quattro.
  • ⚠️ Non provato con due peer veri: App Nap blocca il secondo scatto sullo stesso Mac (il percorso di rete è esercitato via call_local). World.gd non è stato toccato.

Il busking suona la skin: sedici numeri, uno per personaggio (SU-892)2026-09-09

Ivan ha approvato i sedici suoni generati (SU-891 a «Fatto»), e si sono montati. Prima il tasto del busking suonava sempre lo stesso busking.wav, chiunque fosse in scena: adesso il suono segue la skin, come già faceva la striscia dell'animazione (SU-852).

  • ✅ Gemello audio di busk_strip(): Skins.busk_sfx(id) con la stessa forma e il ripiego su busking.wav (Skins.gd:76-77,106-325,443-457). Avvio, rilancio in loop e super leggono la skin invece dell'export fisso della scena (Player.gd:2606-2740, Player.tscn:313-314).
  • In multiplayer il puppet sente la skin dell'altro, non la propria: AudioStreamPlayer2D sul bus SFX esistente (Player.gd:1561-1577). Misurato: locale f1, puppet remoto rapper_f.
  • Misurato sul path, non a orecchio: per tutti e 16 gli id, SfxBusk.stream.resource_path è il file di quella skin — tabella completa nel commento del ticket. Con un id senza file si ripiega su busking.wav, zero SCRIPT ERROR.
  • La trappola che questo ticket poteva pagare, e non ha pagato: un AudioStreamWAV in loop senza loop_end è muto dopo cinque frame e non dà nessun errore. Verificato su tutti e 16 gli import: loop_mode=1, loop_begin=0, loop_end=197568, e al secondo giro (misurato a 5,0 s) playing=true.
  • ✅ I sedici file in files/homeless_city/assets/sounds/busk/ e i raw omonimi in Sounds_raw/ (LFS): SHA-256 identici fra sorgente, asset e raw; tutti PCM16 mono 44,1 kHz, 4,48 s.
  • 📌 m1 resta sul busking.wav storico: i file approvati sono sedici, le skin diciassette. È il ramo di ripiego, ed è esercitato.
  • ⚠️ Non provato: l'ascolto umano del secondo giro (il «niente click» è misurato sui limiti del loop, non con l'orecchio) e una sessione EOS reale a due processi — il ramo puppet è stato esercitato headless con due istanze.

Le lattine diventano due contatori che salgono soltanto (SU-784)2026-09-09

È il gate di tutta la sincronizzazione dei progressi sull'account: un saldo che sale e scende non si può fondere fra due dispositivi — il massimo regala lattine, il più recente le perde. Due contatori monotoni si fondono col massimo, sempre.

  • lattine_guadagnate e lattine_spese al posto del saldo (MetaProgress.gd:257-260); get_lattine() è la differenza, saturata a zero (:717-718). add_lattine(n>0) somma ai guadagni, n<0 e spend_lattine() alle spese (:723-741).
  • Migrazione timbrata dopo il caricamento di contatori e timbri (:1139-1144,1172-1180): il vecchio saldo passa una volta sola nelle guadagnate, la chiave legacy sparisce, e al secondo avvio non rigira.
  • Misurato, operazione per operazione: 30 operazioni miste (guadagni, spese, spese rifiutate) danno 30/30 saldi identici a prima, e in nessuno dei 30 passaggi un contatore scende. Finale: saldo 17, guadagnate 435, spese 418.
  • ✅ Migrazione da un meta.cfg con lattine=250: guadagnate 250, spese 0, saldo 250, timbro presente; riaperto una seconda volta, i cinque valori non cambiano.
  • ✅ Baracca e skin comprano e rifiutano come prima, misurato alle soglie: 24 rifiuta / 25 compra (fondo cassa), 34 rifiuta / 35 compra (gate skin).
  • 📌 Decisione presa dal builder e da tenere d'occhio: add_lattine(n<0) registra solo la sottrazione *effettiva* (saturata al saldo), mentre lattine_run continua a ricevere n intero — è il comportamento di prima, ma è il punto in cui i due numeri possono divergere.
  • ⚠️ Non provata l'integrazione con la UI delle skin: è stato verificato il solo gate comune spend_lattine(), perché il lotto aveva un file solo di perimetro.

L'ombra dei camioncini tarata sul disegno di Ivan, e il rettangolo che mentiva (SU-905)2026-09-09

Ivan ha disegnato a mano l'ombra che vuole, sulle tre tavole preparate apposta (zoom 2x con griglia), e i suoi tratti rossi sono diventati numeri: isolati per differenza dalla tavola originale — così il rosso del suo pennarello non si confonde con il rosso del camioncino — e riportati in coordinate dello scatto.

  • Ritarati i tre moltiplicatori (WorldGenerator.gd:9236-9238): mattina 0,75 → 0,4 (più corta), pomeriggio 3,5 → 6,5 e 1,05 → 2,0 (la lingua che sale dietro il mezzo, ~1,9× più lunga). RAPPORTI_ATTESI della sonda aggiornato di conseguenza (1,03/1,44/1,50 → 0,90/2,15/2,28): quella sonda insegue il codice, non lo comanda.
  • Il criterio che conta è dove arriva la punta, ed è centrato: y minima 282 contro 275 voluti alle 15, 203 contro 215 alle 17 — scarti di 7-12 px su un'ombra di oltre 200. Alle 8, dove Ivan la voleva bassa e larga, il rettangolo è 194×130 contro 208×124 chiesti.
  • ⚠️ Il confronto sul rettangolo TOTALE era fuorviante, ed è la trappola di misura di questo giro: il rettangolo dell'ombra prodotta risultava 353×310 contro i 245×220 «voluti», e sembrava un peggioramento. Ma il corpo del van arriva a y=509 e il tratto di Ivan si ferma a y=496: lui ha disegnato la lingua che si allunga, non l'attacco alle ruote. Confrontare un bbox che include l'attacco con uno che non ce l'ha fa apparire sbagliato un risultato centrato. 📌 È la quarta volta in due giorni che uno strumento verde o rosso guarda nel posto sbagliato: qui è bastato misurare dove finisce davvero il camioncino.
  • 📌 Resta una scelta che spetta a Ivan e non è stata presa: con l'attacco fissato alle ruote (giustamente, era il suo primo KO), *arrivare in alto* e *stare dentro l'inviluppo disegnato* non possono essere veri insieme. È stata privilegiata l'altezza, che era la lamentela esplicita («deve solo comparire dietro a destra dove si allunga»). Se preferisce più corta, si accorcia.
  • ✅ Sonda probe_su796_ko_ombre.gd rigirata: problemi=0 su tutti gli 8 semi. Sovrapposizioni con il contorno voluto in TMP/su905_L14/sovrapposizioni/.

La query dello sprint rendeva una fetta e non lo diceva (tools/jira_leggi.sh)2026-09-09

Scoperto a fine giro, controllando cosa restasse da fare: la stessa identica query che al mattino aveva reso 18 ticket ne rendeva 27, e i mancanti non erano arrivati nel frattempo — SU-707 era stato aggiornato alle 12:58, SU-687 alle 12:54, SU-871 e SU-872 alle 08:59, tutti prima della lettura delle 14:14.

  • ⚠️ Causa: /rest/api/3/search/jql è paginata con nextPageToken, e maxResults è solo un *suggerimento* — il server può renderne meno senza che la risposta sembri anomala. Il campo che lo dice è isLast, che non guardavamo. E la nuova API non ha più il total della vecchia /search: senza seguire la paginazione, una fetta è indistinguibile dal tutto.
  • chiavi_e_titoli() in tools/jira_leggi.sh ora chiede le pagine successive finché isLast non è vero. Verificato: 27 ticket contro i 18 di prima.
  • 📌 Conseguenza su questo giro, detta senza attenuanti: la riga RIENTRI ATTESI di oggi dichiara «perimetro di partenza: 18 ticket», ed è sbagliata — erano di più. I 15 lavorati restano lavorati e chiusi bene, ma non erano «tutto lo sprint»: erano quello che la query aveva reso.
  • 📌 Il segnale che avrebbe dovuto insospettire: SU-701 e SU-461, che i changelog dei giorni prima davano esplicitamente nello sprint («fuori per decisione di Ivan»), nella query del mattino non comparivano affatto. Quando il perimetro non torna con la memoria storica dello sprint, è la query a mentire.

Il branco adesso si sente: il cerchio mentre gira attorno, il morso quando azzanna (SU-907)2026-09-09

Ivan ha ascoltato i due file generati stamattina e li ha approvati in chat («cani per ora mettiamo queste»), sbloccando il montaggio.

  • Cerchio e morso in gioco (SuperPower.gd:732,747,924,945,1404): il tappeto parte all'apparizione dei cani — da super, da carta e per i branchi remoti — e si ferma solo quando non resta più nessun branco, così due richiami sovrapposti non generano copie sfasate. Il morso suona una volta per morso valido, dentro il rientro di 0,8 s che il codice già rispettava. Un solo player per suono, entrambi sul bus SFX, nessun volume scritto a mano: seguono i cursori delle opzioni come gli altri effetti.
  • ⚠️ loop_end impostato, che è la trappola vera di questo ticket: 132300 campioni (3,00 s × 44100 Hz), fissato sia nell'.import sia sul duplicato a runtime. Un AudioStreamWAV in loop senza loop_end ha un loop di lunghezza zero, ammutolisce dopo pochi frame e non dà nessun errore — si sarebbe scoperto solo giocando.
  • Misurato, non dedotto: stream a runtime AudioStreamWAV con loop_mode=1, loop_end=132300, durata 3,000 s, bus SFX; spawn → suono in 2 ms, despawn → stop in 0 ms con posizione a 0,000 (nessuna coda); richiamo mentre è in corso → una sola istanza; a 10,15 s il cerchio suona ancora e la posizione avanza (0,477 → 0,755), che è la prova del loop_end; dieci morsi consecutivi → dieci riproduzioni, dieci riavvii puliti, un player solo.
  • ✅ I raw in Sounds_raw/ (LFS, aggiunti per cartella), i .wav importati in assets/sounds/. super_branco.wav, il suono dell'attivazione, non è stato toccato.
  • 📌 Non provato: la sessione EOS vera a due peer, l'ascolto in cuffia e il trascinamento manuale dei cursori del volume. Il ramo remoto è coperto headless.

L'ombra del camioncino inseguiva l'insegna sul tetto (SU-905, KO di Ivan)2026-09-09

Il ticket è rientrato con quattro difetti: *«alle 8 è troppo larga (esce dai lati), troppo lunga per un camion (non è alto come un palazzo) e parte da metà camioncino, deve partire dal fondo dove ci sarebbero le altre ruote; alle 15 compare dal lato sinistro, deve solo comparire dietro a destra»*. Tre difetti su quattro avevano una causa sola.

  • ⚠️ La causa: _ombra_piede_obliquo inseguiva l'insegna HOT DOG montata sul tetto. È un residuo isolato che nello sprite (120×84 nativi) sta a y=14, lontanissimo dalle ruote: l'appoggio finiva a y=60,5 invece che alla base vera (y=83). Da lì l'ombra attaccava a metà mezzo. 📌 È la stessa famiglia di trappola già pagata altrove nel progetto — il bbox di uno sprite può contenere scarti staccati, e chi lo misura ci casca.
  • ⚠️ E la base era centrata sul canvas, non sul camioncino: centro tela 59,5 px contro centro reale 67 (bbox opaco x 24-109) — ~7 px di sbordo strutturale sul fianco sinistro a ogni ora, che è esattamente l'ombra «che compare a sinistra» delle 15. Risolti insieme da _van_bbox_opaca() (WorldGenerator.gd:10013), che calcola il rettangolo dei pixel davvero opachi e lo usa per appoggio e lunghezza (:10077-10081).
  • I numeri, prima → dopo: attacco all'ombra a 0,50 px dalla base (era mezzo mezzo più su); larghezza dell'ombra alle 8 da 94,4 a 69,6 px contro un van largo 68,8 — cioè ora dentro l'ingombro; bordo sinistro da 498 (fuori, a sinistra del van) a 504-506, sempre a destra del bordo del mezzo.
  • Accorciata come un mezzo basso, non come un palazzo: _ombra_camioncino_sole usava la stessa altezza sbagliata (60,5) come «quanto è alto il mezzo»; corretta a 82 px e ricalibrati i tre moltiplicatori sul rapporto della panchina, l'unico riferimento approvato e stabile. Rapporto area ombra/sprite: h08 1,64 → 0,49 (panchina 0,48), h15 0,90 → 0,39 (0,45), h17 1,37 → 0,54 (0,48).
  • ⚠️ Il criterio di SU-796 è stato aggiornato, non aggirato: RAPPORTI_ATTESI passa da 1,817/2,366/2,944 — tarati su un'ombra da palazzo — a 1,03/1,44/1,50. 📌 Da notare: la sonda passava problemi=0 anche prima, e continua a passarlo ora. Era il criterio a essere vecchio, non il codice: una misura può essere verde e misurare la cosa sbagliata.
  • ✅ Quota delle ruote invariata; panchina, lampione e ogni altro prop identici prima e dopo sugli stessi semi; VAN_DRAW_SCALE = 0.8 non toccato.
  • 📌 Trovato di rimbalzo: probe_su796_ko_ombre.gd:107 salvava le texture isolate con un path res://../../TMP/… non globalizzato — errore=0 e nessun file scritto, la trappola scoperta ieri su un altro provino. Corretto.
  • 📌 Non provato: il rendering su device vero (solo scatti dalla harness) e i quartieri diversi dal centro. Il rapporto con la panchina resta vicino ma non identico (0,39-0,54 contro 0,45-0,48): le due forme sono diverse.

Se l'host cade la partita si annulla, e da oggi è scritto che è una scelta (SU-908)2026-09-09

Il ticket nasceva dall'altra faccia della strada 2 di SU-899: mettere sull'host dei fatti autoritativi significa che quei fatti muoiono con lui. Chiuso con l'opzione A, che è anche il comportamento già in vigore.

  • Scelta A — la partita si annulla, scritta in testa a _on_server_disconnected() (files/homeless_city/scripts/autoload/NetworkManager.gd:1782) con la data e il perché, così chi legge non la prende per una dimenticanza. Il giocatore vede la stringa che c'è già.
  • 📌 Perché non B (l'host si trasferisce): obbligherebbe a mandare a tutti, mentre accadono, i fatti che oggi l'host tiene per sé — chi ha attraversato il tornello (SU-899) e il giro condiviso del tunnel (SU-881) — perché chi eredita non può ricostruire ciò che non ha mai visto. Finché nessun erede esiste, quei fatti stanno sull'host senza rischio, e la strada 2 non ha bisogno di niente altro. Chi volesse la migrazione parte da lì: il commento lo dice.
  • 📌 Nessun codice di migrazione scritto: il ticket dice esplicitamente che qui non si scrive logica nuova.

La classifica in partita online, in alto a destra, con le righe che scivolano (SU-878)2026-09-09

Durante la partita online l'HUD non mostrava niente degli altri: i punteggi vivi arrivavano già all'host ogni 2 s per le carte dispetto, ma non tornavano a nessuno.

  • Una sorgente sola (NetworkManager.gd:193,218,572,582,2576 + World.gd:2286,5292,5755,5768): l'host raccoglie da punteggio live, session_scores e stato metro, e replica ogni 2 s. Il banner spettatore («nº PROVVISORIO») legge la stessa lista, come chiedeva il criterio — non una copia che può divergere.
  • Il pannello (HUD.gd:149,2610,2685,2746,3063,3670): fino a 4 righe sotto il timer, nome a tre lettere della lobby, ordinate per punti, la propria riga evidenziata, il tasto pausa che scende, safe-area rispettata. Stati «in metro», «fantasma», «scappato» in 8 lingue (ui_hud.csv:41), e chi lascia resta con l'ultimo punteggio.
  • Il sorpasso si vede: le righe scivolano con un tween di ~0,3 s invece di riordinarsi di colpo (Ivan, chat dell'8/9). Misurato il percorso, non l'intenzione: y da 40,00 a 23,56 a 4,00, arrivo al bersaglio in 0,3 s, e il pacchetto periodico successivo non contrasta la riga — cioè l'animazione sopravvive all'aggiornamento che arriva mentre è in corso.
  • Payload 104 byte a 4 peer contro il limite EOS di 1170. Rettangoli misurati e zero sovrapposizioni con giorno, ora, punteggio, timer e pausa, sia a 844×390 sia a 1024×768, col pannello dentro la safe-area in entrambe. Nascosta in single, nel tutorial e con l'HUD di corsa spento.
  • ⚠️ Il provino dichiarava errore=0 e non scriveva nessun PNG. Image.save_png() con un path res://../../TMP/… non salva e non lo dice: corretto globalizzando il path e aggiungendo un FileAccess.file_exists() che dice scritto=. 📌 E i file sono poi ricomparsi in un posto inatteso — /TMP/su_L11/…, cioè /private/tmp — perché le sonde girano su una copia rsync del progetto in /tmp, quindi res://../.. esce dal repo. Gli scatti buoni sono stati copiati in TMP/su878_scatti/.
  • 📌 Da guardare: nello scatto i punteggi sembrano formattati in modo incoerente («DAN 2000» accanto a «BOB 1.200»). Non è fra i criteri del ticket e non l'ho toccato.
  • 📌 Non provato: il trasporto remoto vero multi-processo/EOS. Gli handler sono stati esercitati con call_local su World e HUD reali.

La possessione arriva ai nemici di quartiere: yeti, pinguino e fenicottero (SU-888)2026-09-09

Terza e ultima parte, sulla rete propria dei foe di quartiere: questi tre non stanno sul canale degli NPC, hanno foe_net_id e il loro _cl_foe_sync.

  • Stessi gesti e stessi punti delle parti 1 e 2, con l'evidenza rossa e la guida via _srv_foe_input(foe_id, dir_code) (World.gd:10666): payload applicativo 8 byte contro il limite EOS di 1170, e gli id di possessione dei foe sono disgiunti da quelli degli NPC senza toccare foe_net_id (World.gd:1974, ID_COLLISION=NO). Ogni colpo a segno su un giocatore vale 120: misurato su tutti e tre, con la vita che cala davvero.
  • Ciascuno colpisce come sa fare: lo yeti carica al contatto (Yeti.gd:119, guida a 55 px/s, posa e ricarica invariate), il pinguino fa male solo scivolando (Penguin.gd:100,111: innocuo altrimenti, il tocco arma 2,5 s a 160 px/s — SLIDE=true solo per lui), il fenicottero becca (Flamingo.gd:87, 100 px/s).
  • Il gatto scaccia sempre lo spirito, yeti compreso — che da libero è immune (Yeti.gd:218, Flamingo.gd:140): misurato CAT_EJECTED=true con YETI_REMAINS=true, cioè lo yeti resta e il fantasma esce; il fenicottero fugge e rientra come sempre (FLED=true RETURNED=true).
  • La caccia dello yeti si sospende e riprende, misurata sullo spostamento: libero FREE_DX=11.0, posseduto -5.5 (va dove lo porta il fantasma), dopo l'uscita di nuovo 11.0.
  • La lezione della parte 2 è stata applicata (DistrictFoe.gd:424): i 120 punti partono solo dopo il calo di vita reale, senza nessuna euristica su vite altrui — che nella parte 2 era il difetto che regalava punti senza colpo.
  • 📌 Non provato: due processi reali in Brina e Collina. La sonda copre entrambi i set di foe nello stesso World ma non due peer veri; il ripiego su ENet è stato rifiutato dalla guardia della harness e non è stato aggirato.

Nel menu multiplayer entra solo chi ha l'ultima versione accettata (SU-903)2026-09-09

Il blocco c'era già ma arrivava tardi ed era relativo: SU-896 confronta la versione del client con quella dell'host, quindi due giocatori fermi alla stessa versione vecchia giocavano insieme lo stesso, e chi ospitava scopriva di essere indietro solo quando qualcuno provava a entrare.

  • Una fonte della versione accettata, senza servizi nuovi: il campo versione_multiplayer in BETATESTING/devices.json — il file che Ivan pubblica già su GitHub e che BetaGate.gd scarica. Lo schema resta 1 e i client vecchi ignorano il campo (BetaGate.gd:65,254). ⚠️ Il file va ripubblicato, altrimenti il gate legge «sconosciuto».
  • Il confronto è per componenti numeriche, non fra stringhe, e la prova che serviva è nel test: 0.9 < 0.10. In ordine alfabetico sarebbe il contrario.
  • Il caso «non si sa» è un requisito, non un ripiego (BetaGate.gd:154): campo assente, non numerico o richiesta fallita = versione sconosciuta e si entra, con la decisione scritta nel codice e datata. Un aereo non deve mangiarsi il multiplayer locale.
  • ✅ La voce MULTIGIOCATORE resta visibile e apre il pannello del motivo, riusando quello esistente (MainMenu.gd:10180,11700): «Tu hai la 0.43, per il multiplayer serve almeno la 0.44. Aggiorna dallo store e riprova», in 8/8 lingue con due segnaposto per lingua. Il bypass è lo stesso interruttore del DebugPanel di SU-896 (MainMenu.gd:11594), non un secondo. NetworkManager.gd non è stato modificato.
  • ⚠️ Il ticket ha preso due giri per colpa di un falso rosso mio. Rilanciando io la sonda sull'albero vero uscivano 3 fallimenti su 7 — versione allineata, senza rete e col bypass finivano tutti nel gate — e sembrava che il blocco scattasse sempre. Nessuna delle quattro ipotesi che avevo scritto era giusta: la sonda incontrava il gate dell'account (SU-447) *prima* di quello della versione, perché il mio rilancio non passava il flag che il suo lanciatore usava, e il pannello che leggevo era no_account. Il gate della versione era corretto fin dal primo giro.
  • Rimediato nella sonda, non nel gate (probe_su903_version_gate.gd:22,132,185): ora prepara il login, misura separatamente versione, account e pannello, e rifiuta i flag che maschererebbero i controlli — così non può più mentire in nessuna delle due direzioni. 7/7 sull'albero vero, exit 0.
  • 📌 Non provato: lo scaricamento HTTP reale da GitHub e l'autenticazione vera (niente rete nella sandbox), il click sul DebugPanel, e il rifiuto in lobby di SU-896 a due peer.

Il colpo del posseduto, i 120 punti, e tre giri per accorgersi che il compile-check mentiva (SU-887)2026-09-09

Il ticket è entrato con un report verde e si è chiuso al terzo giro. Le due cose che l'hanno tenuto aperto non le ha viste nessun builder: le ha viste la sonda, rilanciata sull'albero vero.

  • ⚠️ Il compile-check passava verde mentre OGNI run del gioco moriva. Due righe identiche var arch := str(n.get("archetype_id")) if "archetype_id" in n else "" (World.gd, in due punti): l'inferenza := da un ternario su Variant è un warning che il progetto tratta come errore, e check_files.sh non lo vede. Il sintomo era altrove — un altro lotto trovava generator a null e la sua sonda moriva su Parse Error — ed è stato quel lotto a portare la diagnosi. Ora le due righe hanno il tipo esplicito e il commento che dice perché.
  • ⚠️ Difetto 1: il colpo non aveva ricarica e nessun tiro veniva registrato. Misurato: FIRST_CD=0.0 SECOND_CD=0.0 SHOTS=0, e due nodi creati a ogni pressione — il tasto sparava a raffica e, non risultando alcun tiro, il filtro non trovava mai un colpo cui attribuire i punti (HIT_DELTA=0). Riparato armando le ricariche già esistenti dei lanci condivisi (ModularNPC.gd:969,1009,1249,1409) e registrando l'istanza davvero lanciata (World.gd:10644,10656), senza inventare timer nuovi. Ora: FIRST_CD=5.0, un tiro per pressione, e la seconda pressione entro la ricarica crea 0 nodi.
  • ⚠️ Difetto 2: i 120 punti si prendevano senza il colpo. L'accredito guardava un calo generico di vite: bastava che il giocatore perdesse una vita mentre c'era un posseduto in giro. Misurato prima: UNRELATED_LIFE_LOSS_DELTA=120. Riparato agganciando l'accredito alla causa reale (World.gd:10680,10707, con l'impatto autoritativo nei tre proiettili) e buttando via il confronto delle traiettorie. Ora: UNRELATED_LIFE_LOSS_DELTA=0, e NORMAL_SHOT_DELTA=0 contro OWNED_SHOT_DELTA=120.
  • Verde sull'albero vero, non nella sandbox: per tutti e tre gli archetipi con un'arma (tossico, spazzino, gattara) il colpo a segno vale +120 con la riga nel breakdown; a vuoto, su un NPC e su un fantasma vale 0; i contatti di ronda (bully, rivale) pagano 120 per colpo anche togliendo tre vite; e il ri-invio di _cl_final_score non rifà né la notifica né la ghostificazione (1 → 1 su entrambe).
  • 📌 La sonda del primo giro «passava» perché non provava niente: girava in un processo solo, iniettava a mano il tiro e non esercitava né l'azione vera né il colpo a vuoto. Ora costruisce gli NPC da World, con volo e collisioni reali e nessuna iniezione.
  • 📌 Non provato: la misura vera a due peer e il trasporto EOS/ENet. session_scores e il reinvio sono verificati in locale.

La scoreggia scende in metropolitana, e sotto terra si sente anche nel vagone accanto (SU-882, SU-883)2026-09-09

Sotto terra la scoreggia non era mai esistita: il tasto lanciava il gatto alla pressione proprio per quello. Ora segue lo schema della città — tenuta = nuvola d'area, tocco = gatto — e in multiplayer arriva addosso agli altri discesi.

  • Una macchina sola, non due (files/homeless_city/scripts/player/Player.gd:2272-2405): sotto terra è la stessa _handle_fart della città a decidere tenuta o tocco, dietro il gate bonus_level_active; la superficie resta invariata. Raggio, durata e fattore di rallentamento sono accessori alle costanti cittadine e la ricarica è quella già armata da _do_fart: nessun numero ricopiato, che era il criterio del ticket.
  • Nuvola e suono duplicati a runtime dai nodi del Player (BonusLevel.gd:1952-1958,4110-4269): nessun asset nuovo. Nel corridoio i topi nel raggio fanno dietrofront, in banchina la folla si scosta e gli zombie restano fermi per la durata, i treni non ne risentono.
  • SU-883, il tanfo dal vagone accanto (NetworkManager.gd:621-627,2485-2516): il client chiede senza payload — 0 byte applicativi — l'host estrae il mittente e manda _cl_tunnel_stink solo agli altri discesi, riusando tunnel_underground_ids. ⚠️ Nessuna seconda lista dei discesi: due liste che si credono entrambe autorevoli sono il modo più caro di rompere il multiplayer.
  • Misurato sull'albero vero, non nella sandbox del builder (13 controlli, 0 KO): mp_farts 0→1, misdemeanor fart 0→1, 2/2 topi invertiti con la x che diminuisce, folla da 0,0 a 45,0 px, zombie fermo a 0,000 px durante la nuvola e 3,598 px dopo, gatto al tocco 0→1. E la prova che le costanti sono davvero riusate: il rallentamento remoto sotto terra fa 40,49 px in 1 s contro i 40,50 della città. Il tanfo non tocca né chi è in superficie né chi l'ha fatto.
  • ✅ Regressione SU-643 (tenuta e tocco in città): 5/5.
  • 📌 Non provato: l'ascolto e la resa a finestra, e la prova a tre peer — 0/3 entrati, EOS ha risposto UnexpectedError sul Device ID e la sandbox ha rifiutato il ripiego su ENet. Nessun aggiramento tentato. Non aperta la scena del tutorial, anche se il percorso senza bonus è coperto dalla regressione.

L'ombra dei camioncini: il difetto stava nella sonda, e la leva ovvia era codice morto (SU-905)2026-09-09

Il ticket chiedeva di misurare prima e correggere poi. La misura ha risposto tre volte, e ogni volta smentendo chi la faceva — il codice del gioco non è stato toccato: git diff su WorldGenerator.gd è vuoto, e la decisione che resta è di Ivan.

  • ⚠️ Primo giro: il difetto era nella sonda, non nel gioco. _trova_ombra prendeva la prima ombra dello stesso tipo incontrata in DFS, non la più vicina — e il seme 796001 genera due camioncini hot dog (796007, due gelatai): la sonda misurava l'ombra dell'altro furgone, 122-191 px più in là, e la faceva sembrare staccata dalle ruote. Corretta con lo stesso nearest-match che _trova_visual usava già. Con la sonda giusta lo scarto di posizione è ≤1 px su entrambi gli assi, e la quota delle ruote è identica alle tre ore.
  • ⚠️ Secondo giro: «sarà l'alfa» era falso, e i numeri lo dicono: opacità media 1.000 per van, panchina e lampione — la texture è una maschera binaria e l'alfa a schermo lo mette uno shader condiviso. Lo scarto vero è altrove: la densità dell'ombra del van crolla a 16,4% (15h) e 20,1% (17h) contro il 28-29% costante della panchina e il 32,6% del lampione, e il rapporto area-ombra/area-sprite è 0,90-1,74 contro lo 0,45-0,48 fermo della panchina — il criterio che SU-796 cita per nome. Il van proietta una striscia stretta (1/5 della propria larghezza), la panchina una sagoma piena: sono due strategie diverse, non un numero mal tarato. La lunghezza invece è quella approvata da Ivan, verificata: 1,817 / 2,400 / 2,883 contro attesi 1,817 / 2,366 / 2,944, problemi=0.
  • ⚠️ Terzo giro: la leva ovvia non era una leva. larghezza in _tex_ombra_camioncino (WorldGenerator.gd:10021) non ha alcun effetto alle tre ore del ticket — provato per algebra e poi per misura, portandola a 13,6× e ottenendo numeri identici bit per bit: base[2]/base[3] (:10040-10041) cancellano quel termine, che conta solo a mezzogiorno. Non è un difetto: è la correzione voluta da Ivan a un KO precedente di SU-796 («un solo parallelogramma, senza cuneo interno»), scritta nel commento a :10030-10034.
  • 📌 Tre varianti da guardare, nessuna applicata: TMP/su905_varianti/su905_foglio_contatto_UNICO.png mette attuale, C e B nella stessa scena alle 15h, coi due camioncini del seme e la panchina in inquadratura. Densità 16,4% → 23,2% (C, +10 px) → 28,9% (B, +20 px, praticamente la panchina), ma il rapporto di lunghezza sale del +8,5% e +14,8%: allargare aiuta la densità e non è gratis sulla lunghezza già approvata. Entrambe restano dentro la banda ±25% della sonda di SU-796 su 8 semi, quota ruote invariata.
  • ✅ Committata solo la sonda (files/homeless_city/scripts/tools/shot_su796_camioncini.gd): nearest-match, misura di densità, rapporto d'area e opacità per van, panchina e lampione. Serve a chiunque tocchi le ombre da qui in avanti.
  • 📌 Domanda aperta, fuori dal perimetro del ticket: alcuni semi generano due camioncini dello stesso tipo nella stessa città. Se capitano vicini, l'ombra di uno si legge come se fosse dell'altro — ed è un candidato concreto per il «sbagliata rispetto al mezzo» del collaudo. È generazione, non geometria: non toccato.

Quindici fantasmi, uno per personaggio, e un prompt approvato che contraddiceva il montaggio (SU-901)2026-09-09

Il fantasma era uno solo — quello del barbone — e lo indossava chiunque morisse, qualunque personaggio avesse. Generati da Astra i quindici che mancavano, in TMP/su_ghost_skins/, da guardare prima del montaggio (che è SU-902, bloccato).

  • 15 fogli, uno per skin, con tools/sprite/su901_ghost_skins.sh (nuovo, sul modello di su864_ritocchi_skin.sh): riusa GHOST_AI_PROMPT.md cambiando solo la descrizione del personaggio, e passa ad Astra due riferimenti — il raw di partenza della skin e il raw del fantasma m1 come righello di stile. Da guardare: TMP/su_ghost_skins/confronto_ghost_skins.png, 15 bande «partenza | fantasma frontale | fantasma di profilo».
  • ⚠️ Trovata una contraddizione dentro il prompt approvato di SU-31, ed è il genere di cosa che si ripaga a ogni giro finché non è scritta: GHOST_AI_PROMPT.md chiama la riga 4 «SE, verso destra», ma la pipeline di montaggio (files/homeless_city/tools/import_chatgpt_sprites.py, SOURCE_ROW_DIRS = [s, n, e, sw, ne]) vuole quella riga disegnata SW, verso sinistra, e la specchia lei in SE. Chiedendo «SE» il primo giro usciva con la riga 4 quasi identica alla 5. Il prompt non è stato riscritto — è quello con cui sono nati i fogli già approvati — ma ora porta l'avviso in testa alla riga.
  • 📌 La direzione non si ottiene chiedendo una direzione: la riga 4 usciva quasi frontale su tutte e 15 (difetto presente anche nel riferimento m1 già approvato). Si è chiusa sostituendo l'istruzione con un test geometrico verificabile — «solo l'orecchio sinistro visibile» — tecnica già documentata in tools/sprite/LEGGIMI.md per un difetto gemello. ⚠️ Resta debole su breaker e punk_m: da giudicare a occhio, eventualmente un giro mirato solo su quei due.
  • 📌 Celle di bordo «sporche» (sconfinamenti lievi, nessuna barra a tutta misura): da 0/25 su breaker, juggler_f e punk_m fino a 25/25 su rapper_m. È lo stesso difetto delle skin di settembre e si chiude al montaggio col rimontaggio dei raw, non rigenerando.
  • 📌 player_skin_f1_ghost_sheet.png, che esisteva già e nessuno script carica, è stato guardato e regge: lasciato com'è.

Il giro del tunnel diventa dell'host, e l'host smette di credere al client (SU-881, SU-899)2026-09-09

Due ticket in un lotto solo perché il primo chiude metà del secondo: portando il contatore di giro sull'host, round_number non lo dichiara più nessun client. La strada l'ha scelta Ivan in chat: strada 2 — l'host non conosce il saldo, valida il gesto.

  • Un solo giro per partita, tenuto dall'host (files/homeless_city/scripts/autoload/NetworkManager.gd:244,587,2025,2039): request_tunnel_entry/_srv_tunnel_enter passano da 3 argomenti a UNO (solo la fermata). Giro e «sto aprendo io» non si dichiarano più. Online la soglia di banca è 100×2^(giro−1) (GameState.gd:1740-1817), e il raddoppio in single non cambia di una riga.
  • SU-899, il gesto al posto del saldo (NetworkManager.gd:2218-2280): l'host segna ogni 0,25 s chi vede davanti allo sportello (110 px, TTL 180 s) e concede l'apertura solo a chi ha quel gesto, consumandolo. ⚠️ Fallisce aperto se la banca non si trova: altrimenti un mondo non ancora pronto murerebbe il tunnel per tutti.
  • Il tabellino CAPOLINEA (NetworkManager.gd:2099-2166,2184): ogni disceso manda il suo risultato alla risalita, e a lista svuotata parte il banner ordinato per monete — banner e non pannello, nessuno è in pausa. Chi si sconnette sotto esce dalla lista e non viene aspettato; una rete di sicurezza a 5 s copre il client piantato.
  • ⚠️ Due decisioni che cambiano un comportamento, e vanno provate da Ivan: (1) online sparisce il «rigioca lo stesso giro» di chi si fa prendere dal treno se un compagno ha vinto — è la conseguenza voluta del giro unico, ma è un cambio di sensazione; (2) il cambio di giro a premio acceso si rimanda a bank_take_gold_debt(), perché alzarlo subito avrebbe reso quel biglietto impagabile per sempre.
  • Payload misurati (limite EOS 1170): _cl_tunnel_ranking 116 byte a 4 peer e 232 a 8 con nomi da 12 caratteri, _cl_tunnel_round 16, _srv_tunnel_enter 16, _srv_tunnel_done 24. Nel tabellino viaggiano i nomi e non i peer id: chi si sconnette dopo aver mandato il risultato non è più in players e sarebbe diventato «???».
  • 41/41 prove della sonda nuova (files/homeless_city/scripts/tools/probe_su881_899.gd), e SU-880 non regredisce: chiamata 19,9 s, ingresso gratis a 19,2 s, rifiuto a 21,4 s, 4 arcobaleni accesi e spenti.
  • 📌 Cosa resta scoperto, e per costruzione: nel tabellino monete e «vinto» li dichiara ancora il client, perché quello che succede sotto terra l'host non lo vede — è la stessa frontiera della strada 2. E il gate del gesto non ferma chi trucca il client e fa davvero la passeggiata banca→fermata.
  • 📌 Non provato: la prova vera a due peer. Un SceneTree ha un solo multiplayer peer, quindi la sonda entra dal punto in cui sbucano le RPC dopo la lettura del mittente: il pacchetto che viaggia davvero resta da provare su due macchine.

La segnalazione in classifica passa da un popup, e finalmente si può togliere (SU-900)2026-09-09

Scorrendo la classifica online col dito capitava di armare SEGNALA e confermarla senza volerlo: il nome spariva dietro *** per sempre, perché report_name() scriveva e nessuna funzione toglieva.

  • Il gesto non segnala più: apre un popup con SEGNALA e ANNULLA (files/homeless_city/scripts/ui/MainMenu.gd:7671), e la conferma a doppia pressione è sparita (:8340). Su una riga già segnalata il popup gemello offre RIMUOVI SEGNALAZIONE. ANNULLA è la voce preselezionata all'apertura, ed è anche ciò che fanno ESC e il tasto indietro: un gesto involontario finisce senza conseguenze.
  • La strada di ritorno che non esisteva: unreport_name() in files/homeless_city/scripts/autoload/OnlineLeaderboard.gd:2139 toglie il nome dai bloccati e tutte le sue vecchie voci dalle segnalazioni, e salva subito su disco. Aggiunta anche is_reported_name() (:2131) per distinguere una segnalazione locale da un nome oscurato dal filtro generale — ⚠️ is_blocked() non è stato toccato: resta il punto unico che legge, e questo ticket cambia solo chi ci scrive dentro.
  • ⚠️ Il difetto nasceva col dito, e il rimedio è lì: le righe ora distinguono il tocco dal trascinamento al rilascio, con soglia 18 px (MainMenu.gd:7758), e la conferma resta disabilitata per 0,18 s contro il click emulato del touch. Misurato: dieci scorrimenti rapidi della lista, zero segnalazioni, segnalazioni_count() 0→0 e la riga selezionata ferma (1→1) col popup aperto.
  • ✅ Segnala → conteggio 0→1, nome ***, nome presente nel file riletto da disco; rimuovi → 1→0, nome assente dal file e di nuovo visibile; ANNULLA su entrambi i popup lascia conteggio e byte del file identici. Cinque chiavi I18n nuove in tutte e 8 le lingue, audit emoji 0.
  • 📌 Cosa deve provare Ivan, e non è provato qui: il dito su un device vero — è dove nasce il difetto, e la sonda simula i trascinamenti su una Label sola. E il ritorno del nome dopo un riavvio completo del gioco: la sonda rilegge il file nello stesso processo, che è una prova più debole.
  • 📌 Da guardare (ipotesi di una lettura di controllo, non verificata): a MainMenu.gd:7821 il bottone di conferma è disabilitato per 0,18 s, ma l'attivazione da tastiera non controlla disabled — freccia su e INVIO entro quella finestra potrebbe passare lo stesso.

La sonda dello scudo dava 0, poi 5, poi 1: era lei che usciva e rientrava dal tornello (SU-898)2026-09-09

Il conteggio degli arresti sul gesto HOLD cambiava a ogni esecuzione sullo stesso codice — 0/10, 2/10, 5/10, 1/10 su tre alberi diversi. Una misura così non può né bocciare né promuovere un lotto, e ogni ticket che tocca il tornello ci passa.

  • La causa è stata provata, non ipotizzata: a seme fisso, prima del rimedio, gli arresti erano 3, 2, 5, 5, 1 su cinque giri — e coincidono esattamente con le uscite/rientri spuri dall'area del trigger, 3, 2, 5, 5, 1. Il barbone usciva e rientrava dal trigger durante il hold, e il conteggio dipendeva da quanti frame cadeva dentro o fuori: una corsa fra il passo del personaggio e quello della sonda.
  • Rimediato dentro la sonda, senza toccare il gioco (files/homeless_city/scripts/tools/probe_su727_scudo_metro.gd:26,132,248): seme fisso 727898 stampato nell'esito, contatori body_exited/body_entered separati per il solo HOLD, e reset fisico a due fasi — prima fuori dal trigger per un physics frame, poi dentro fino all'overlap confermato.
  • Dieci esecuzioni, dieci volte lo stesso numero: HOLD 0/10 ×10, TOCCO 0/16 ×10, uscite/rientri 0/0 ×10. Il criterio HOLD resta nei criteri invece di essere tolto, perché ora è una misura vera.
  • ✅ Nuovo lanciatore tools/autotest/probe_su727_scudo_metro.sh con SU727_RUNS, HOME dedicata rimossa e ricreata a ogni replica — senza quello, dal secondo giro la sonda esce rossa con un codice sanitario, che è una trappola già pagata in questo progetto.
  • 📌 Non misurato: che la dispersione non fosse una regressione di SU-879. Il ticket lo dava per verificato («c'è identica sull'albero a HEAD») e il lotto non l'ha rimisurato.

A Solleone gli spazzini non esistono più, e nessun `if` sul nome del quartiere (SU-904)2026-09-09

Ivan, in chat: «solleone non deve avere gli spazzini, solo i cactus». Il tetto «uno spazzino a parco» c'era già, ma lo spazzino nasceva lo stesso — perché nessun quartiere sapeva dire «questo archetipo da me non esiste».

  • Un'esclusione dichiarativa per quartiere, npc_esclusi in files/homeless_city/scripts/autoload/Quartieri.gd:25,451-454,865-868 — stessa forma dei cactus, vuota dappertutto e valorizzata solo a Solleone. Lo spawner la legge prima di far nascere l'NPC (files/homeless_city/scripts/world/DistrictSpawner.gd:105-134,412-415), e il vecchio flag di tetto per parco lì è stato tolto perché non serviva più. Nessun if quartiere == "solleone" nel generatore, che era il vincolo del ticket.
  • Numeri, non stime: tre semi (904091/904173/904257), spazzini a Solleone 15/13/12 prima → 0/0/0 dopo, e zero anche fra gli NPC vivi dello spawner. Gli altri sette quartieri invariati sul pool (Gattopoli 25/19/15, gli altri già a zero), e Fumarola conserva il suo spazzino guardiano: 1/1/1 sui tre semi — era il rischio vero, perché lì lo spazzino è il nemico di casa.
  • ⚠️ Il determinismo è stato risolto filtrando il pool, non ripescando, ed è la decisione che tiene in piedi il multiplayer: files/homeless_city/scripts/autoload/NPCDatabase.gd:816-853 toglie gli esclusi prima dell'unico randf(). Misurato: 24/24 pesche consumano esattamente un tiro, e 24/24 sequenze sono identiche fra due peer simulati. Una ripesca avrebbe consumato un numero di tiri dipendente da chi guarda, e due peer sullo stesso seme avrebbero generato due città diverse.
  • 📌 Cosa NON è provato: tre sessioni reali da almeno 3 minuti e due processi EOS veri. La sonda (files/homeless_city/scripts/tools/probe_su904_spazzini.gd) comprime 180 tentativi di spawn e simula i due peer in un processo solo.

I due suoni del branco, e super_branco.wav che stava al limite del formato (SU-906)2026-09-09

Il branco di cani aveva un solo suono, quello dell'attivazione. Generati con ElevenLabs i due che mancavano — il cerchio mentre girano attorno, il morso quando azzannano — e lasciati in TMP/su_cani_audio/ per l'ascolto di Ivan, che è chi mette Fatto.

  • cani_cerchio.wav 3,00 s e cani_morso.wav 0,80 s, mono 44,1 kHz, picco −5,70 dBFS entrambi. I PCM grezzi in TMP/su_cani_audio/_raw_pcm/, i numeri e le ragioni in TMP/su_cani_audio/LEGGIMI.txt. Nessun file entrato in files/homeless_city/assets/: il montaggio è SU-907, oggi bloccato.
  • ⚠️ Il criterio «volume confrontabile con super_branco.wav» è stato disatteso di proposito, e il perché è una misura: super_branco.wav sta a 0,00 dBFS, cioè al limite del formato. Non è il metro del progetto, è l'anomalia — throw.wav è a −5,7 dBFS e i 16 suoni di busking di SU-891 stanno fra −1,2 e −1,7. Allinearsi a 0,0 avrebbe messo un tappeto in loop sopra ogni altro effetto del gioco. Scelto −5,70: «non più forte» resta vero con 5,7 dB di margine, e se all'ascolto è troppo piano si rialza di un numero.
  • 📌 Il morso è la seconda presa, e la prima è stata scartata su un numero, non a orecchio: montava invece di scattare — picco a 535 ms su 800, contro i 15 ms di attacco di fart.wav. La seconda ha il picco a 63 ms. La scartata resta in TMP/su_cani_audio/varianti_scartate/.
  • 📌 Il cerchio invece è la PRIMA presa, ed è la scelta contro-intuitiva: la seconda aveva più energia ma 9,3 dB di dislivello fra i primi e gli ultimi 250 ms (−20,3 contro −29,6), cioè in loop avrebbe pulsato a ogni giro; la prima ne ha 2,5 (−38,1 / −35,6). Salto di giunzione fra ultimo e primo campione a −52 dBFS: nessuno scalino udibile.
  • ⚠️ La conversione fa il downmix a mono PRIMA di normalizzare. È la trappola pagata su SU-891 il giorno prima: il PCM di ElevenLabs è stereo interleaved a 0 dBFS, e calcolare il guadagno sullo stereo per poi sommare i canali lascia il livello finale sotto il bersaglio (busk_magician.wav era finito a −5,40 contro i −1,2/−1,7 degli altri quindici).
  • 📌 Cosa non è provato: non li ho ascoltati, e il loop non è stato provato in Godot. Le misure dicono durata, livello, attacco e tenuta della giunzione — non se il cerchio suoni «cani» né se il morso faccia ridere. Quella parte è l'approvazione di Ivan, e il loop_end è di SU-907 (⚠️ un WAV in loop senza loop_end ammutolisce dopo pochi frame senza dare errore).

Una scheda di giudizio per ambiente, e il primo bocciato vero: Divano sparisce e torna (SU-509, SU-511)2026-09-09

Deciso da Ivan in chat: strada (b), una scheda per ambiente invece di una colonna env. Poi, col suo stesso nickname, la prima prova vera del giro di ritorno.

  • giudizio diventa giudizio_{env} in tutti e due gli script: PREFISSO_SCHEDA + una funzione scheda(env), e l'ambiente passa alle funzioni che toccano il foglio. La scheda esistente rinominata giudizio_stage, coi 13 verdetti gia' scritti dentro. ⚠️ Il perche' sta scritto nella docstring di entrambe le funzioni: le righe non portano l'ambiente con se' e tools/firestore/applica_giudizi.py applica ogni riga giudicata all'ambiente di --env, quindi in una scheda sola un «bocciato» di stage lanciato con --env live avrebbe scritto il divieto nel registro sbagliato lasciando vivo il nickname in quello giusto.
  • La scheda dell'ambiente si crea da sola (assicura_scheda): con una scheda per ambiente la mancanza e' il caso normale del primo giro di live, non un guasto. Nasce vuota, col dropdown gia' messo, e l'intestazione la scrive appendi_righe. Dal lato lettura applica_giudizi.py non la crea (ha solo spreadsheets.readonly) ma lo dice: «la scheda giudizio_dev non esiste: nessun nome da giudicare», invece di morire su un HTTP 400.
  • 📌 Trappola pagata subito: dentro id_scheda la variabile del ciclo si chiamava scheda e ombreggiava la funzione nuova — TypeError: 'dict' object is not callable al primo lancio. Presa dalla prova a vuoto, prima di toccare qualsiasi dato.
  • SU-511 provata su dati veri, col nickname di Ivan che lui stesso ha bocciato dal foglio: 12 ok → «gia' applicato» (stato assente vale ok, e infatti non scrive niente), Divano bocciato → nickname cancellato (13 → 12), player svuotato (nome e nome_lower a ""), bocciati/divano scritto. Rilancio: «gia' applicato», zero scritture.
  • E il divieto morde davvero, misurato dal lato client con la chiave del gioco e le regole in mezzo (il service account le salta, quindi non avrebbe provato niente): create di d1van0 — la variante leet che si normalizza in divanocon normHTTP 403 PERMISSION_DENIED; la stessa create senza normHTTP 200, creata (cancellata subito dopo). ⚠️ Il secondo caso non e' un difetto ma il buco dichiarato di SU-510 misurato su stage: le build 0.43 in mano ai tester non mandano norm, e per loro la lista dei bocciati non morde finche' non aggiornano.
  • [nuovo] SU-909, il pezzo che da qui non si prova: che il bocciato, riaprendo il gioco, si trovi senza nome pubblico e se ne veda chiedere un altro. Lo prova Ivan sul suo account, e non prima della prima build con la sezione [firestore] — le 0.43 installate parlano ancora con Apps Script e non mandano norm, quindi su di loro il divieto non morderebbe. Bloccato da SU-697, legato a SU-511, in backlog.
  • Divano rimesso com'era, coi valori del «prima» presi prima di toccarlo: nickname ricreato identico (creato 2026-09-05T15:56:15Z compreso), nome/nome_lower ripristinati sul player, bocciati/divano cancellato, e il verdetto sul foglio riportato a ok — senza quello, il giro dopo lo avrebbe ribocciato. Controllo finale: 13 nickname in stage, entrambi gli script a vuoto.

Il giro dei nomi tocca il foglio vero: SU-509 provato contro Sheets e Firestore (SU-509)2026-09-09

Ivan ha condiviso StreetU_registro_nickname col service account come Editor; il resto e' stato fatto qui, dalla CLI. ⚠️ La condivisione da sola non bastava: sul progetto axiomatic-path-505007-u7 (933602919752) erano spente sia la Sheets API sia la Drive API, e il 403 di Sheets che il lotto del 05/09 aveva letto come «foglio non condiviso» era in realta' SERVICE_DISABLED. Accese con gcloud services enable sheets.googleapis.com drive.googleapis.com (reversibile con services disable).

  • La scheda giudizio non esisteva: il foglio aveva solo le due schede stage e dev dell'Apps Script (la live non c'era mai stata). Creata qui via spreadsheets:batchUpdate, 5 colonne, vuota; l'intestazione l'ha scritta lo script al primo giro, come previsto. La scheda stage non e' stata toccata.
  • Criteri 1 e 2 provati su Firestore e sul foglio veri, con quattro finti claim in registro/dev (3 scelti dal giocatore + 1 con riserva piena): primo giro 3 righe accodate, il nome di riserva non compare; secondo giro 0 righe, timbro invariato.
  • Caduta fra append e timbro simulata davvero (timbro cancellato a righe gia' scritte): al rilancio 0 doppioni, la deduplica sul foglio regge, il timbro riavanza da solo. Poi pulizia: 3 righe finte tolte dal foglio, 4 documenti e timbro cancellati da dev, 0 residui.
  • Giro vero sull'ambiente dei tester: --env stage ha accodato 13 righe (10 Google, 2 Apple, 1 Epic), rilancio 0 righe. Da adesso i nomi si giudicano scrivendo ok / dubbio / bocciato nell'ultima colonna, dall'app Fogli come voleva il ticket.
  • Il verdetto si sceglie da un menu a tendina, non si scrive (chiesto da Ivan in chat il 09/09): validazione ONE_OF_LIST vincolante (strict) su tutta la colonna E della scheda giudizio — ok / dubbio / bocciato. ⚠️ Non e' comodita': tools/firestore/applica_giudizi.py rifiuta un verdetto sconosciuto e si ferma senza scrivere niente, quindi un solo refuso in una riga blocca il giudizio di tutte le altre. tools/firestore/nomi_da_giudicare.py ora la riapplica da solo a ogni giro che scrive (assicura_dropdown, +48 righe): provato togliendo la regola a mano e rilanciando lo script — 0 celle con la regola prima, 1014 dopo, e la riga appena accodata la eredita. La regola copre la colonna senza fine, quindi vale anche oltre le 1000 righe della griglia iniziale.
  • 📌 Il default dello script e' --env live, che e' vuoto: il giro dei tester va lanciato con --env stage, altrimenti non fa niente e sembra rotto. L'id del foglio e' 1Edww5SwFxfILGnYCTEVB9ks8cJWHSBwxLQUPz13dRTU (STREETU_REGISTRO_SHEET_ID), ma ora che la Drive API e' accesa la ricerca per titolo funziona da sola.
  • 📌 Il service account non puo' creare fogli (spreadsheets.create → 403 PERMISSION_DENIED: non ha quota Drive propria). Per questo la prova dei criteri e' girata sulla scheda vera con righe finte poi cancellate, invece che su un foglio di prova separato.
  • ⚠️ Buco di progetto trovato provando, non un difetto del codice: la scheda giudizio non ha una colonna env, e applica_giudizi.py applica *ogni* riga giudicata all'ambiente passato con --env. Finche' esiste solo stage non succede niente, ma il giorno che si accende live le righe dei due ambienti si mescolano nella stessa scheda e un bocciato di stage applicato con --env live scriverebbe bocciati/{norm} nell'ambiente sbagliato. Da decidere prima del passaggio a live.

Quattro decisioni di Ivan messe sui ticket, e una premessa sbagliata corretta (SU-899, SU-908, SU-790, SU-785, SU-891)2026-09-09

Giro senza codice di gioco: sono le decisioni date in chat, scritte sui ticket lo stesso giorno perche' altrimenti si rilavora il requisito vecchio.

  • [decisione] SU-899, strada 2: l'host non conosce il saldo, valida il gesto — l'apertura del tunnel si concede solo a chi l'host ha visto attraversare il tornello, e il valore dichiarato dal client sparisce dall'RPC.
  • [nuovo] SU-908, l'altra faccia della strada 2, sollevata da Ivan: se l'host cade, o la partita si annulla del tutto o l'host passa a qualcun altro — e allora i fatti che oggi terrebbe solo lui devono stare su tutti in anticipo, perche' chi eredita non puo' ricostruire cio' che non ha mai visto. ⚠️ Letto nel codice: _on_server_disconnected() con stato IN_GAME chiude e riporta tutti al menu, quindi «si annulla del tutto» e' gia' il comportamento in vigore — per costruzione, non per scelta scritta. La strada 2 non rischia niente finche' non esiste un erede.
  • [decisione] SU-790 fermo per ora: la firma HMAC del codice di ripristino non si decide adesso.
  • [fix] SU-785, premessa sbagliata: la EncryptionKey di EOS non sta sul portale e non e' recuperabile da li' — Epic non la gestisce ne' la salva nelle impostazioni del prodotto, la definisce lo sviluppatore. Si genera una volta con openssl rand -hex 32 e si custodisce per sempre: persa o cambiata, ogni salvataggio gia' caricato diventa illeggibile. Corretto anche il commento di files/homeless_city/eos_credentials.example.cfg, che diceva «per cifrare il traffico P2P»: serve a Player Data Storage e Title Storage.
  • [audio] SU-891: rigenerati i tre bocciati — i due clown senza le voci di fondo, e lo sputafuoco in tre prese di sola fiammata, di cui Ivan ha scelto la v2. ⚠️ La misura ha scoperto un difetto della conversione: il guadagno si calcolava sull'mp3 stereo e poi il downmix a mono abbassava il picco, cosi' il livello finale mancava il bersaglio. Rifatti in due passate lo sputafuoco e busk_magician.wav, che stava a -5,40 dBFS contro i -1,2/-1,7 degli altri quindici: in gioco il mago suonava piu' piano di tutti. Set finale: 16 file, 4,48 s ciascuno, picchi fra -1,72 e -1,19 dBFS (escursione 0,52 dB, era 4,20), 16 checksum distinti. ⚠️ Corretta una mia misura del commento precedente: la durata e' 4,48 s, non 4,00 — awk con la virgola decimale italiana aveva troncato il numero.

«La chiamata»: il tunnel si apre a tutti, e un buco dichiarato invece che aggirato (SU-880)2026-09-09

Chi apre il tunnel lo apre a tutti per 20 secondi: arcobaleno su tutte le fermate, ingresso gratis al giro di chi paga, lista compatta dei peer sottoterra, pulizia a risalita e a disconnessione. L'host ricava sempre il peer dal mittente e rifiuta sconosciuti, fantasmi, indici falsi, giri fuori da 1..64 e attori oltre 110 px dalla fermata.

  • [misura] Chiamata attiva con 19,937 s residui; arcobaleni 4/4 accesi e 4/4 spenti alla scadenza; ingresso a 19,191 s sì e a 21,406 s no. Payload var_to_bytes: 32 / 32 / 48 / 8 byte contro il limite EOS di 1170.
  • [fix] Il tunnel poteva restare occupato per sempre: l'id entrava nella lista dei sottoterra ma il percorso di *successo* non liberava mai lo slot — bastava una risalita riuscita per bloccare tutte le chiamate seguenti.
  • [fix] Un ACK tardivo lasciava un peer fantasma nella lista: il client annullava l'attesa da solo dopo 2,5 s, ma un ACK affidabile in ritardo poteva ancora registrarlo sull'host — e quel peer, non essendo mai entrato, non mandava mai la risalita. Ora l'annullamento è concordato, non unilaterale.
  • [fix] La durata dipendeva dagli orologi di sistema: si sottraeva l'orologio Unix del client da quello dell'host, e gli orologi dei telefoni non sono sincronizzati per ipotesi — un invitato con l'orologio avanti avrebbe visto la chiamata già scaduta. Ora sul filo viaggia una durata, non due istanti presi da due macchine.
  • [fix] Identificatori rinominati in inglese: nel codice i nomi vanno in inglese, in italiano stanno i commenti e i testi UI.
  • ⚠️ Il rilievo critico NON è chiuso, ed è dichiarato apposta invece che aggirato. L'host accetta ancora opens_call e round_number come li dichiara il client: un client rimaneggiato può aprire il tunnel senza aver pagato e scegliere il giro fino a 64. 📌 Il builder si è fermato con la ragione giusta, ed è il motivo per cui è diventato un ticket suo (SU-899): nei file del lotto non esiste uno stato host per-peer del deposito o del tier — players contiene solo nome, slot, sblocchi e skin —, spostare la dichiarazione in un'altra RPC sarebbe *la stessa falla con un altro nome*, e negare le aperture remote romperebbe la funzionalità. Serve decidere quale sia la fonte autoritativa dell'economia in multiplayer, che è una domanda di design, non un controllo da aggiungere. Metà del buco lo chiude già SU-881, che porta il contatore di giro sull'host.

La possessione, parte 1, e sette difetti che la review ha trovato in due giri (SU-886)2026-09-09

Da fantasma si tiene premuto su un nemico per 3 secondi e lo si guida: possessione host-authoritative, input a 20 Hz su canale non affidabile con timeout di 0,18 s (senza, un pacchetto perso lasciava l'NPC a camminare all'infinito), cooldown 10 s, uscita col gatto o volontaria, rilascio su disconnessione, fine run e despawn. Il velo rosso sta su un nodo sprite sovrapposto che copia animazione, frame e specchio — mai modulate sul corpo, come chiede la decisione di Ivan dell'08/09 — e l'etichetta col nome nel colore di slot dice chi guida, senza portare il rosso.

Sette difetti trovati dalla review incrociata in due giri, nessuno dal builder. Vale la pena elencarli perché quattro sono la stessa famiglia:

  • ⚠️ L'host si fidava del client sui 3 secondi. Validava fantasma, distanza, tipo e cooldown, ma non la durata della pressione, misurata solo dal client: un client rimaneggiato possedeva all'istante. 📌 L'aggravante era il commento: *«Nessuna di queste decisioni vive sul client»* — vero quando fu scritto, falso ora, e un commento che mente costa più di una riga mancante. Ora c'è un handshake start/ack/cancel e l'host rifiuta prima dei 3.000 ms: richiesta a 49 ms respinta.
  • ⚠️ Si poteva guidare un NPC fuori dal mondo e lasciarlo fuori dal navmesh, cioè rotto anche dopo l'uscita del fantasma. Clamp autoritativo a 8 px dal bordo: misurato (92,0) su un mondo 100×100 spingendo verso est.
  • ⚠️ Si potevano possedere bersagli sbagliati: poliziotto stordito, modulare abbattuto o in zuffa. Esclusi, misurati uno per uno.
  • ⚠️ BLOCCANTE, e il compile-check non lo vedeva: si scriveva ghost.velocity su una variabile dichiarata Node2D, che quella proprietà non ha. check_files.sh dava verde — è la trappola nota dei warning-trattati-come-errore: verde headless, rifiutato dall'editor. Nessuna sonda l'avrebbe preso; l'ha preso un secondo modello che leggeva il diff.
  • ⚠️ Il posseduto lontano andava a scatti. Fermando il fantasma la sua posizione restava quella d'ingresso, e l'area d'interesse continuava a misurare da lì: oltre 900 px l'NPC scendeva da ~10 Hz a ~1,1 Hz proprio per chi lo stava guidando. Ora l'AoI usa la posizione dell'NPC posseduto: distanza 0 px, periodo 1.
  • ⚠️ Durante la possessione l'NPC si congelava troppo: l'early-return fermava anche dialogo, allarme, panico, blocco tiri e sbornia, che restavano appesi per tutta la possessione. Ora si ferma solo il movimento AI.
  • ⚠️ Il fantasma ricompariva nel posto sbagliato se l'NPC era già stato liberato, perché Vector2.ZERO faceva due mestieri — «nessuna posizione» e «posizione (0,0)». Ora has_pos viaggia separato.
  • [misura] Il rischio dell'ordine di physics, misurato invece che curato a scatola chiusa: scarto AI intermedio del poliziotto 1,0 px, errore finale 0,0 px, nessuna trasformata intermedia arriva al rendering. Nessuna cura necessaria, e ora il numero c'è.
  • ⚠️ Non provati: la matrice a 3 peer reali e gli scatti dal peer vivo.

SU-896 secondo giro: chi viene respinto legge perché, e il margine è misurato2026-09-09

Il blocco di versione funzionava già dal primo giro — il collaudo a due peer l'aveva confermato — ma chi veniva respinto leggeva un generico «L'HOST HA CHIUSO LA LOBBY»: la chiave MP_RIFIUTO_VERSIONE_STORE esisteva in 8 lingue e nessuno la vedeva. Il difetto lo ha trovato il collaudo, non una sonda: era invisibile a chiunque guardasse il codice.

  • 📌 Due cause che si sommavano, e curarne una sola avrebbe lasciato una corsa fra pacchetti. Lato host, disconnect_peer() stava sulla riga subito dopo l'RPC che porta il motivo, e la batteva sul tempo. Lato client, _on_server_disconnected scriveva sulla stessa etichetta: anche se l'RPC fosse arrivata, la chiusura di sessione le avrebbe scritto sopra.
  • [fix] Host: margine di 0,15 s prima del distacco, e dopo l'attesa un ricontrollo di essere ancora host e che il peer esista. Il commento accanto dice perché quella riga non può tornare attaccata all'altra — è esattamente il tipo di riga che qualcuno «ripulisce» fra sei mesi.
  • [fix] Client: il rifiuto arma un segno usa-e-getta che vince sulla chiusura di sessione. 📌 E scade dopo 1 s, protetto da un seriale contro i timer vecchi: senza scadenza la prossima disconnessione *vera* avrebbe mostrato il messaggio sbagliato al contrario, cioè lo stesso difetto rovesciato.
  • [misura] Controprova a due peer, sul commit del rimedio. Versioni diverse: il testo che compare è ora *«TU HAI LA 0.0.0-mismatch, L'HOST HA LA 0.43. AGGIORNA DALLO STORE E RIPROVA.»* — la chiave giusta, con entrambe le versioni.
  • [misura] Il margine non è scelto a occhio: è misurato. Il client vede il motivo a 28 ms dalla join_game(), contro i 150 ms in cui l'host aspetta a disconnettere: ~120 ms di scarto su loopback. Detto onestamente, quei 28 ms includono anche l'handshake ENet e la _srv_register, non solo il volo dell'RPC; e su un trasporto reale via relay EOS lo scarto si restringerebbe — qui non è riproducibile.
  • [misura] Il caso normale non è rotto, che era il rischio del segno: stessa versione, host che chiude la lobby → il client mostra ancora «L'HOST HA CHIUSO LA LOBBY», a +1630 ms.
  • [feat] Sonda files/homeless_city/scripts/tools/probe_su896_deny_text.gd, headless: legge il testo dell'etichetta invece di fotografarlo. È la strada giusta su questo Mac, dove lo scatto dal secondo peer è impedito da App Nap.

Il collaudo a due peer veri: quattro criteri chiusi e un difetto che nessuna sonda vedeva2026-09-09

Tre lotti di questo giro avevano dichiarato NON PROVATO lo stesso identico criterio — *il multiplayer vero a due peer* — perché nella sandbox di Codex run_mp.sh non parte: il SU_TMP obbligatorio dentro il repo fa rifiutare la copia a force_enet, e i path con spazi spezzano i log, coi processi che ricadono in single player. Fuori dalla sandbox invece parte, e quello che si vede lì non lo vedeva nessuna sonda.

  • [misura] SU-896, il blocco di versione funziona: con versioni diverse il picco giocatori dell'host resta 1 invece di 2 e il client non entra mai in partita — l'ospite non compare proprio nella lobby. Confermato due volte. Il bypass per i collaudi funziona: con --mp-no-version-check due versioni diverse entrano regolarmente, picco 2 e 72 NPC sul client.
  • [misura] E non aggiunge ritardo, che era l'altro criterio: lobby → partita in 3,0 s su 3 run su 3, identico sul commit 4fe3a8c e sul commit b31f03c di prima del ticket. Confronto vero, non stima.
  • ⚠️ Il difetto che solo il collaudo poteva trovare: chi viene respinto non legge la spiegazione, legge «L'HOST HA CHIUSO LA LOBBY». Riprodotto 2/2 con una sonda dedicata e uno scatto. La causa è un ordine, non un testo mancante: _srv_register chiama disconnect_peer() subito dopo l'RPC che porta il motivo, e il disconnect batte sul tempo la consegna dell'RPC — arriva solo session_closed, che scrive per prima sulla stessa etichetta. Il ticket resta aperto: la chiave in 8 lingue c'è, ma nessuno la vede.
  • [misura] SU-874, il quartiere arriva: l'host forza brina e il client no — entrambi generano brina, mai centro, verificato sui dump di tutti e due. La RIVINCITA dopo il collasso di entrambi riparte in brina su host e client, seguita fino a 107 s dopo il rematch.
  • [misura] SU-885, il fantasma si vede fantasma: collasso reale da ubriaco (due bevute e poi il game over vero, lo stesso ingresso del gioco) con un peer davvero connesso in rete — il collassato risulta fantasma su 3 run consistenti. Non simulato.
  • ⚠️ Una trappola d'ambiente da ricordare, perché costerà tempo di nuovo: lo scatto dal secondo peer non si ottiene. macOS applica App Nap alla finestra senza fuoco, la rallenta pesantemente, e il World non arriva mai a caricare in tempo utile — 4 tentativi su SU-885, 3 su SU-884, tutti fermi sulla lobby o su uno schermo vuoto. caffeinate -i non basta: impedisce il sonno, non App Nap. Non è un difetto del gioco, e va dichiarato NON RIUSCITO per ragione ambientale, non NON SODDISFATTO. Le prove visive in multiplayer restano da fare su device.
  • ⚠️ SU-884 resta senza la sua prova più severa: non esiste un modo di iniettare latenza da riga di comando. I numeri che ha (0 frame di walk a spostamento nullo a 0,5, 1 e 2 s di ritardo) vengono da pacchetti simulati, non da una rete lenta vera.

Il TestBot sa scendere nel tunnel, e senza non si misurava niente (SU-897)2026-09-09

Nato chiudendo SU-879. Il quarto criterio di quel ticket — *misurato con due peer: host sotto, sul client gli NPC si muovono e l'orologio avanza* — non era misurabile per una ragione sola, e non era il tunnel né la rete: il bot non sa scendere. Mancava l'innesco.

  • 📌 Non era il problema di un ticket solo, ed è per questo che è diventato un ticket suo invece di un NON PROVATO in fondo a un commento: gli stessi due peer servono a SU-880 (la chiamata), SU-881 (il tabellino CAPOLINEA), SU-882 e SU-883 (la scoreggia sotto terra). Senza il flag si sarebbero chiusi tutti e quattro con lo stesso buco, e il primo KO sarebbe arrivato su device.
  • [feat] --bot-bonus-after N in TestBot.gd: arma una fermata via API pubblica, posa il barbone davanti al trigger e preme davvero interact. Nessun teletrasporto dentro il tunnel, e non è pignoleria: il percorso che la catena deve misurare comincia alla porta — è lì che vivono lo scudo di SU-727, il tornello e la chiamata di SU-880.
  • 📌 I log sono agganciati ai segnali veri bonus_level_started/ended, non a un timer: se il bot non entra davvero, la riga non compare. Un log che si stampa a tempo avrebbe raccontato una discesa mai avvenuta.
  • [misura] Caso host: giù a 30,3 s, su a 48,0 s. Caso ospite: 30,3 e 48,1. Zero SCRIPT ERROR nei quattro log. Senza il flag: 0 righe [BOT] bonus sui due processi di controllo, cioè il bot si comporta esattamente come prima.
  • ⚠️ Quelle due misure sono la discesa, non la rete. Nella sandbox il SU_TMP obbligatorio dentro il repo fa rifiutare la copia a force_enet, e run_mp.sh spezza i path con spazi: i due processi ricadono in single player. È lo stesso muro che avevano dichiarato anche i lotti di SU-874/896 e SU-884/885 — fuori dalla sandbox run_mp.sh invece gira, e infatti SU-879 ci ha misurato 2 giocatori e 72 NPC.

Fine partita online: due voci per tutti, e la terza volta che lo stesso difetto cambia vestito (SU-876)2026-09-09

Menu di fine partita uguale per tutti in multiplayer — TORNA IN LOBBY e MENU PRINCIPALE — con la rivincita tolta dal percorso MP, e una transizione host-authoritative IN_GAME → IN_LOBBY individuale, nella stessa sessione. Le due RPC nuove sono reliable e senza argomenti, e il registro di lobby continua a viaggiare a blocchi, quindi il pacchetto EOS non cresce (è la garanzia misurata in SU-852, che sta nel blocco e non nella piccolezza del registro).

  • ⚠️ Il primo giro sembrava finito e non lo era. Il segnale lobby_returned veniva emesso e nessuno lo ascoltava: in MainMenu.gd non esisteva alcun listener, e chi premeva TORNA IN LOBBY finiva nel menu principale. Il cuore del ticket non funzionava, e il compile-check era verde.
  • 📌 E aggiungere un listener non sarebbe bastato, che è la parte interessante: il segnale parte prima del cambio scena, quindi quando MainMenu nasce è già passato — una connessione nel _ready() non lo sentirebbe mai. Rimedio: un handoff persistente one-shot su NetworkManager, che sopravvive al cambio scena, viene consumato da MainMenu e si azzera anche a nuova partita o a sessione chiusa.
  • 📌 È la terza volta in questo giro che lo stesso difetto cambia vestito: gli effetti audio congelati sul bus perché _update_heat_music_warp smette di girare (SU-890), l'audio della città che torna udibile perché World riscrive il volume al frame dopo (SU-879), e ora un segnale emesso mentre chi doveva sentirlo non esisteva ancora. Tre facce di *qualcosa che vale solo finché un nodo è vivo*.
  • [misura] Sonda nuova sul percorso vero (return_to_lobby → segnale → cambio scena → lettura della schermata): schermata = mp_lobby, sessione attiva, stato IN_LOBBY, host true, quartiere mercato prima e dopo, ospite non pronto. Nella lobby riaperta AVVIA è visibile e RITARDA abilitato. Il criterio è misurato, non dedotto dal codice.
  • [fix] Se l'host sparisce durante la dissolvenza di TORNA IN LOBBY, _scene_change_pending viene disarmato e si inoltra a MENU PRINCIPALE: l'unica uscita superstite non resta bloccata.
  • 📌 Le chiavi RIVINCITA restano nel CSV ma senza consumatori in .gd/.tscn: sono definizioni, non testo a schermo, e il protocollo rematch interno può restare.
  • ⚠️ Non provati: il giro completo a due processi (fine → lobby → quartiere diverso → nuova partita) e la caduta host reale durante la dissolvenza.

Il tunnel entra in multiplayer, e tre difetti fermati dalla review (SU-879)2026-09-09

L'8/9 due giocatori hanno depositato, premuto l'arcobaleno e non hanno visto niente: BonusLevel.puo_entrare() rifiutava se NetworkManager.active, perché il congelamento della superficie spegneva il _process del World — sull'host avrebbe fermato la città a tutti. Ora in multiplayer il congelamento è solo video: si nascondono i layer, si tace l'audio, l'input va al tunnel, e la città continua a girare. In single il congelamento vero resta identico.

  • 📌 _superficie["mp"] fotografa quale ramo è stato preso, così lo scongelamento disfa quello che è stato fatto e non quello che farebbe adesso: se la rete cadesse mentre si è sotto, ridomandarlo lascerebbe la città spenta per sempre.
  • [feat] Una sola riga di rete nuova, 2 pacchetti reliable per giro: il puppet di chi è sotto esce dal gruppo mp_puppet, che è la lista dei bersagli della polizia dell'host. È quella riga a rendere «intoccabile» chi sta sotto — la lacuna che il commento in testa a PoliceOfficer.gd dichiarava scoperta, e che serviva un evento e non uno stato per colmare.
  • [fix] Interactable.gd: lo scudo di SU-727 legge anche bonus_level_active, e _metro_shield_target viene catturato anche sulla porta del bonus — prima lo prendeva solo la pressione lunga, quindi scendendo con un tocco secco restava null. In single non si vedeva perché la polizia era congelata.

I tre difetti che ha trovato la review incrociata, non i builder — e tutti e tre riprodotti prima di essere curati:

  • ⚠️ L'RPC nuova era sfruttabile. Era any_peer con peer_id come argomento e nessun controllo sul mittente: un client poteva togliere dal gruppo mp_puppet il puppet di chiunque — rendendosi intoccabile dalla polizia senza mai scendere nel tunnel. Ora accetta solo chi dichiara sé stesso. 📌 E il comportamento del relay non è stato assunto ma misurato: sonda ENet nuova a tre processi, zero righe di gioco, che dimostra che il terzo peer — quello che la riceve *rilanciata dal server* — legge l'id originale del mittente e non 1. Nel progetto non c'era un precedente da cui dedurlo: tutte le altre RPC che leggono il mittente sono _srv_* con mittente diretto.
  • ⚠️ L'audio della città tornava udibile dentro il tunnel, e la misura del primo giro non poteva accorgersene. World riscrive volume_db a ogni frame per le calamità e per la sirena, e in multiplayer World continua a girare: il «250 lettori su 250 zittiti» era preso nello stesso fotogramma del muto. Riprodotto mettendoci 15 frame in mezzo: SfxWaveRain a −15,4 dB e SfxPoliceChase a −3,0 dB, di nuovo udibili. Rimedio: un punto di strozzatura unico _set_volume_citta() che salta la scrittura mentre si è sotto. 📌 Cercate tutte le scritture, non le due segnalate: 7 in World, 5 fuori — e queste ultime scrivono tutte *prima* di add_child(), quindi le prende l'aggancio a node_added. Niente bus: quelli del layout sono Master/Music/SFX e il tunnel usa gli stessi due, mentre un bus creato a runtime è muto sul web.
  • ⚠️ Un CanvasLayer nato mentre si è sotto restava acceso sopra il tunnel, e il vettore non era teorico: SpeechCanvas sta dentro ogni scena di NPC, e in multiplayer gli NPC nascono anche mentre si è sotto. 📌 Lo strato si spegne in differita, e il perché è la parte che conta: node_added scatta durante l'ENTER TREE, *prima* del _ready — e il ventaglio delle carte entra nel suo gruppo proprio nel _ready. Spegnerlo subito avrebbe riaperto il difetto chiuso da SU-541, la cerimonia che si apre invisibile. L'audio invece resta immediato: un frame di ritardo lì si sentirebbe come uno schiocco.
  • [misura] Il numero che serviva a SU-880/883: con l'host in pausa i suoi NPC percorrono 0,0 px in 30 frame contro 168,5 px liberi, e sui client sono puppet che seguono l'host, quindi si fermano anche lì. Il menu di pausa fa ancora get_tree().paused = true anche in MP — è preesistente (MapOverlay.open() fa già lo stesso) e non è stato corretto qui, ma ora la decisione si prende su un numero invece che a intuito.
  • 📌 Un alleggerimento arrivato gratis, da confermare come voluto: con la camera nel tunnel, Cactus, Pigeon e DistrictFoe escono d'inquadratura e sospendono il _process da soli (SU-226), ricalcolando la posizione dal tempo al rientro. Sull'host quelle tre categorie si fermano per tutti mentre lui è sotto. È la «simulazione nascosta alleggerita» che il ticket proponeva, ma nessuno l'ha scritta.
  • [misura] Regressioni: SU-830 sirena verde, giro banca→fermata→bonus→risalita 42/42, probe_su467 75 OK / 10 falliti — gli stessi 10 misurati sulla baseline a HEAD, quindi preesistenti e non del lotto. run_mp.sh con 2 giocatori e 72 NPC: zero SCRIPT ERROR.
  • ⚠️ Una misura di ieri ritirata dal builder stesso: «SU-727, 0 arresti su 10 hold» veniva da UNA esecuzione. Rimisurata su tre alberi, la conta HOLD è non deterministica — 0/10, 2/10, 0/10 sul lotto; 1/10 e 5/10 sull'albero a HEAD senza le modifiche. Il TOCCO invece è 0/16 sempre. Aperto SU-898: un numero che dà 0, poi 5, poi 1 sullo stesso codice non può né bocciare né promuovere niente.
  • ⚠️ Non provati: le misure a due peer del quarto criterio — il bot non sa scendere, ed è per questo che è nato SU-897 — e il p95 del frame time su device del quinto.

La lobby: il quartiere scelto arriva davvero, e chi ha un'altra versione non entra (SU-874, SU-896)2026-09-09

  • [fix] SU-874 — la RIVINCITA partiva senza quartiere. NetworkManager.gd:1115: la rivincita ora manda Quartieri.selected_id insieme al seme, esattamente come fa l'avvio iniziale; prima mandava solo il seme.
  • [fix] E all'ingresso di un ospite la scelta tornava a centro. MainMenu.gd:11179: un peer che non ha ancora la chiave unlocked vale «non ancora dichiarato» e non invalida più la scelta dell'host — prima veniva letto come «non ce l'ha» e faceva ripiegare la partita al centro. MainMenu.gd:11224: Quartieri.save() scatta solo dalla selezione esplicita dell'host, non da init, registro o ripiego. MainMenu.gd:11255: a un diniego vero si ripiega a centro senza salvare, e l'avviso col nome resta visibile.
  • [feat] SU-896 — in lobby si gioca solo fra chi ha la stessa versione. Nasce dalla decisione di Ivan del 09/09 che chiude il punto 2 di SU-895: invece di inseguire la compatibilità del protocollo — impossibile, visto che in Godot 4 gli id delle RPC seguono l'ordine alfabetico dei nomi — si mette un blocco all'ingresso. La versione si legge da application/config/version (NetworkManager.gd:254), mai da una costante ricopiata a mano, viaggia dentro il saluto che già esiste (:1434) e l'host confronta per uguaglianza esatta prima di inserire il peer (:1470).
  • [misura] Il saluto cresce di 44 byte, misurati con var_to_bytes su un nome da 16 caratteri più la versione: margine 1126 byte sul limite EOS di 1170. Non è una stima.
  • [feat] Chiave nuova MP_RIFIUTO_VERSIONE_STORE in tutte e 8 le lingue: il rifiuto dice la versione del client, quella dell'host e l'invito ad aggiornare dallo store — non un errore generico.
  • [feat] Scorciatoia «Ignora versioni MP» nel DebugPanel (NetworkManager.gd:285), più --mp-version X e --mp-no-version-check per le harness: senza, i nostri stessi collaudi a più peer diventerebbero impossibili. Non persiste e non ha effetto in release.
  • 📌 Chi è già dentro non viene espulso: il controllo vive solo in _srv_register, cioè all'ingresso.
  • ⚠️ Non provato nella sandbox: rifiuto e ingresso reali fra due versioni, e la latenza d'ingresso a versioni uguali. run_mp.sh non parte lì dentro perché il SU_TMP obbligatorio cade in un path con spazi e force_enet rifiuta la copia (ERR_CANT_CREATE=20). Rifatto fuori dalla sandbox — vedi la voce del collaudo.

Le repliche in multiplayer: il fantasma si vede fantasma, e gli NPC non camminano sul posto (SU-885, SU-884)2026-09-09

  • [fix] SU-885 — il collassato si vedeva col suo sprite normale. _apply_ghost_visuals() metteva i frame del fantasma ma non azzerava _net_puppet_cat, _net_puppet_drunk e _net_puppet_busking: i flag a false che il proprietario mandava subito dopo, su un canale diverso da quello che aveva portato la ghostificazione, rimettevano i frame normali e cancellavano la tinta. Per questo era intermittente: scattava solo se al collasso c'erano gatto in braccio, sbornia o busking. Ora la ghostificazione ripulisce i tre flag prima dei frame (Player.gd:1330), e cat/drunk/skin non possono più riscrivere un fantasma (:1601, :1624, :4430apply_skin() è no-op sul ghost).
  • [fix] SU-884 — gli NPC replicati camminavano sul posto. NPC._net_puppet_process() sceglieva walk o idle da _net_t_vel.length() > 5, e quella velocità non decadeva mai: finita l'estrapolazione al cap, il puppet restava fermo con la camminata accesa fino al pacchetto dopo (i lontani viaggiano a ~1,1 Hz). Ora la replica azzera _net_t_vel allo scadere del cap e decide l'animazione dallo spostamento reale su finestra (NPC.gd:2069), incluso il caso opposto — velocità nulla con posizione lontana, che prima faceva scivolare da fermo.
  • [misura] Con pacchetti simulati in ritardo di 0,5, 1 e 2 secondi: 0 frame di walk a spostamento nullo e 0 frame di idle sopra soglia, in tutti e tre i casi.
  • [misura] Il rischio secondario del ticket era reale, ed è stato misurato prima di curarlo: i puppet dei giocatori ricevono l'animazione come stringa su unreliable_ordered, e il ritardo del recupero walk→idle è 0,550 s, sopra la soglia di 0,5 s che il ticket poneva come condizione. Quindi la guardia a spostamento è stata aggiunta anche lì (Player.gd:1399) — se fosse stato sotto, non si sarebbe toccato niente.
  • ⚠️ Non provati: il multiplayer vero a due peer e lo scatto dal secondo peer col fantasma a schermo. La sonda è headless e verifica la logica, non la resa.

Telemetria: il file di una partita dice la verità e pesa la metà (SU-867, SU-869, SU-868)2026-09-09

  • [fix] SU-867 — il run_end di chi esce di scena raccontava una partita che non era successa. Tutte e 10 le uscite di scena della 0.43 stage riportavano livello 1, vite 9, 0 $, earned 0 e super vuoto: erano letti dopo il reset di GameState. SU-773 aveva salvato solo il livello. Ora RunRecorder.gd:332 tiene uno snapshot di vite, denaro, earned e super durante la run, e :782 lo usa per la sola causa uscito_dalla_scena — le altre continuano a leggere gli autoload vivi.
  • [misura] Uscita simulata dopo il reset: il run_end riporta 5 / 7 / 120 con earned 430, cioè i valori veri. Le altre quattro cause (vite, collasso, retata, app_chiusa) invariate: 4 su 4.
  • 📌 Una correzione al ticket, non un'omissione. SU-867 chiedeva di salvare anche i counts «se vengono da GameState». Non ci vengono: arrivano da ScoreSystem, che il reset non tocca. Nessuno snapshot, e il motivo è scritto.
  • 📌 Il frame di transizione era una trappola (:827): lo snapshot si aggiorna solo se il path corrente coincide ancora con la scena della run, altrimenti l'ultimo aggiornamento sarebbe stato quello vuoto del cambio scena — cioè lo stesso bug, spostato di un frame.
  • [fix] SU-869 — i tocchi da 10 $ della banca erano il 42% del file. Nella partita da 2.072 s dell'S23, 1.934 eventi interact bank pesavano 221.770 byte su 524.653. Ora _flush_interacts (:1054) fonde i bank consecutivi con lo stesso segno entro 2 s in un evento solo con dmoney totale e n tocchi.
  • [misura] 40 depositi da −10 in raffica: 1 evento, dmoney −400, n 40. Un deposito isolato resta 1 evento, −10, n 1. porte.py continua a trovare 7 tier su 7 (100, 200, 400, 800, 1600, 3200, 6400) e a leggere la 0.43 come 32 porte / 20 vinte / 11 perse / 1 ignota.
  • [feat] run_start porta ora ts in UTC (:663): finora sessioni, pause fra partite e adozione dell'aggiornamento si ricostruivano dall'mtime dei file su Drive.
  • [docs] SU-868 — la patch dell'Apps Script è pronta ma non installata, e per decisione di Ivan (in chat, 09/09) il client non è stato toccato: TMP/sprint_0909/apps_script_dedup.md cerca run_<run_id>.jsonl.gz nel mese e risponde ok senza riscrivere, dentro un lock atomico. Il punto 2 del ticket — marcare .sent dopo il secondo timeout — è scartato di proposito, non dimenticato: perdere un file costa più che contarlo due volte, finché il lato server basta.

Il log smette di riempirsi, e il SESTO SENSO si apre alla prima partita online (SU-866, SU-875)2026-09-09

  • [fix] SU-866 — un rigo che riempiva i log. erase_section_key("meta", "benches") girava a ogni save() anche quando la chiave non c'era: 847 righe di ERROR in 12 code di log su 22 della 0.43, e zero nella 0.42. La coda da 16 KB spedita dopo un crash ne era piena — un crash vero non ci sarebbe stato dentro. Ora si cancella solo se esiste (MetaProgress.gd:1230).
  • [misura] 20 save() di fila con un file che non ha la chiave: 0 righe ERROR. Con un file che la porta ancora: cancellata al primo save(), assente in quello dopo.
  • [fix] SU-875 — SESTO SENSO si sblocca alla prima partita online, non alla terza (MetaProgress.gd:94).
  • 📌 E vale anche per chi ha già giocato, che è la parte che si dimentica: una soglia abbassata non serve a niente se chi aveva già due partite deve rifarne una terza. Al caricamento le regole vengono rivalutate dai contatori salvati e le carte maturate persistono, senza timbro di migrazione (:1136). Misurato: contatori salvati a 1 e a 2 → carta sbloccata al primo caricamento.
  • ⚠️ La riconciliazione agisce su tutte le carte, non solo su questa — è la stessa regola di check_card_unlocks(), applicata al caricamento invece che durante la run, quindi non inventa sblocchi: apre solo carte i cui contatori erano già oltre la soglia. Senza notifica, perché all'avvio l'HUD non esiste ancora.
  • [misura] Prima partita online simulata a motore acceso: contatore 1, sbloccata, pescabile. Single player, 200 ventagli: 0 volte il SESTO SENSO, come dev'essere. probe_su362_sesto_senso.gd rilanciato fuori dalla sandbox, sul Mac, con HOME isolata: verde, e legge la soglia nuova (partite_online ≥ 1).
  • 📌 L'indizio della carta dice ancora «chi ha giocato abbastanza in compagnia»: con la soglia a 1 la parola è larga. Non toccato — è testo di sapore, e cambiarlo è una scelta di Ivan.

La musica non resta deformata fuori dalla partita, e al collasso non ne suonano due (SU-890, SU-873)2026-09-09

Due bug d'audio che si vedevano solo giocando online, con due cause diverse ma la stessa forma: qualcosa che si spegne solo mentre un _process gira, e che nessuno spegne quando quel _process smette di girare.

  • [fix] SU-890 — gli effetti sul bus «Music» tornano neutri all'uscita. Il deforma-musica non sta sul nodo $Music del World: sono un AudioEffectPitchShift e un AudioEffectLowPassFilter innestati sul bus globale, che sopravvive al cambio di scena. A riportarli neutri era _update_heat_music_warp() dentro World._process: uscendo dalla partita quella funzione smette di girare e l'ultimo valore scritto resta congelato sul bus, che il menu principale poi eredita. Ora World._exit_tree() li riazzera di scatto (World.gd:9321), senza rimuovere gli effetti dal bus — toglierli invaliderebbe gli indici _music_warp_pitch_idx/_music_warp_lpf_idx.
  • [misura] 9 misure su 9. Ondata di caldo: pitch_scale 0,9715 → 1,0000 e cutoff_hz 1200 → 20000. Kazoo del dispetto MP (SU-756): 1,2599 → 1,0000 e 2600 → 20000. Una run nuova nasce già pulita (quantità a 0,000), quindi sparisce anche la scia dei due secondi di rientro all'inizio della partita successiva.
  • 📌 La domanda aperta del ticket è chiusa, e la risposta è «no». SU-890 avvertiva che la sbornia *pura* passa da music.pitch_scale sul nodo $Music e chiedeva di provare se restasse appesa anche lei — nel qual caso ci sarebbe stata una seconda strada da trovare. Non si riproduce: un World o un MainMenu nati dopo hanno un $Music nuovo, col pitch_scale di default a 1,0. Misurato, non dedotto: non c'è nessuna seconda strada.
  • [fix] SU-873 — al collasso in multiplayer non suonano più due musiche. Il ramo spettatore di World._on_game_over() esce apposta *prima* della pausa dell'albero, così il nodo Music continua a suonare mentre l'HUD fa partire il giornale: due brani sovrapposti. E alla chiusura del giornale non lo fermava nessuno — _stop_music_game_over() non aveva chiamanti — quindi da fantasma restava acceso per i suoi 2'31". Ora _net_enter_spectator_mode() mette la musica del quartiere in stream_paused (World.gd:10124) e HUD._on_run_report_finished() la risveglia con la nuova _resume_world_music() (HUD.gd:6755) quando il giornale si chiude e la run condivisa non è ancora finita.
  • 📌 stream_paused, non _musica_del_quartiere(): la seconda riparte da capo, e il ticket chiedeva esplicitamente che il brano riprendesse da dove era.
  • [misura] 9 misure su 9 contando i player in playing in ogni fase: durante il giornale quartiere false / giornale true; alla chiusura del giornale con la run ancora viva, quartiere true / giornale false dopo una dissolvenza reale di 803-814 ms (attesi 800); alla fine della run condivisa solo il giornale, nessun accavallamento; identico per l'host collassato per primo (clock_only). probe_musica_gameover.gd resta verde su PROVA 1/2/3 più controprova: il single player non è cambiato.
  • 📌 Un rilievo del cancello verificato e scartato. La review automatica ha segnalato che _fade_out_music_game_over() è una coroutine chiamata senza await (HUD.gd:6072). È voluto: la dissolvenza del giornale deve correre *insieme* alla ripresa del quartiere, non prima — con l'await la musica del quartiere rientrerebbe 800 ms dopo, cioè il difetto al contrario. La dissolvenza arriva comunque in fondo, ed è la misura dei 803-814 ms a dirlo.
  • ⚠️ Non provato: le tre uscite azionate dai bottoni veri (ESCI AL MENU dalla pausa, ESCI dal game over, RICOMINCIA) — le sonde chiamano i punti d'ingresso, non navigano l'HUD a joystick. Verificato però per lettura che tutte e tre convergono sullo stesso meccanismo misurato (change_scene_to_file() → il World lascia l'albero → _exit_tree()): le prime due passano entrambe da _on_exit_to_menu_pressed(), RICOMINCIA da LoadingVeil.go_to_scene(), che chiama change_scene_to_file() sia col velo sia in headless. Non provato nemmeno il multiplayer vero a due peer: la sonda di SU-873 chiama direttamente _net_enter_spectator_mode() e _on_run_report_finished().
  • [feat] Sonde nuove riusabili: files/homeless_city/scripts/tools/probe_su890_musica_menu.gd e files/homeless_city/scripts/tools/probe_su873_doppia_musica.gd.

SU-852 chiuso su misure, non su affermazioni: il registro di lobby regge perché va a blocchi2026-09-09

Il codice di SU-852 — la skin che viaggia in multiplayer e la striscia del busking per personaggio — era già scritto e già in partita da ieri; il ticket era rimasto aperto in attesa delle ultime strisce, che nel frattempo sono arrivate tutte e diciassette. Restavano da prendere due misure che il ticket chiedeva e che nessuno aveva preso. Nessuna riga di gioco toccata: solo una sonda nuova.

  • [misura] Il payload del registro di lobby, e la cosa che lo salva non è quella che sembra. Con 4 peer, nomi da 16 caratteri, otto quartieri sbloccati e gli id di skin più lunghi (firebreather, pickpocket), il registro intero serializzato con var_to_bytes pesa 1160 byte contro il limite EOS di 1170: dieci byte di margine. A 8 peer sarebbe 2312 byte, cioè il doppio del limite. Ma _cl_lobby_state non manda mai il registro intero: lo spedisce a blocchi da 3 voci, e il blocco più grosso misura 872 byte — a qualunque numero di peer da 1 a 8, col margine peggiore di 298 byte.
  • ⚠️ Quindi la garanzia sta nel blocco, non nella piccolezza del registro. Chi un giorno toccasse la dimensione del blocco, o togliesse la spedizione a pezzi credendola un'ottimizzazione inutile, farebbe sforare il limite a quattro giocatori — cioè in una partita normale, non in un caso estremo. È il genere di cosa che si scopre come «la lobby a volte non si popola», non come un errore.
  • [misura] Le diciassette strisce del busking, una per una: per ogni id, Player.apply_skin(id) e poi lettura di _music_sprite.hframes e del path della texture, confrontati con quanto dichiara Skins.busk_strip(id). 17 su 17 corrette — 4 frame per barbone, barbona e mimo, 6 per sputafuoco, borseggiatore, punk e rapper, 8 per breakdancer, clown, giocolieri, mago e uomo/donna orchestra. Il frame mostrato sta sempre dentro 0..N-1.
  • [feat] Sonda riusabile in files/homeless_city/scripts/tools/probe_su852_misure.gd.
  • 📌 La verifica statica non aveva trovato difetti (RPC, ripiego su m1 per una skin sconosciuta via Skins.resolve_id, tutte e quattro le varianti del puppet, ramo beg per una skin senza striscia): il ticket è stato chiuso su quello più queste due misure.
  • ⚠️ Non provati: il multiplayer vero a due peer ENet e gli scatti a video del busking. Il criterio di accettazione che chiedeva «la RPC nuova è l'ultima della lista» è stato ritirato, non spuntato: misurava il posto sbagliato, e il perché sta in SU-895 e nella voce 107.

Una verifica che guardava il posto sbagliato: gli id RPC di Godot 4 sono alfabetici (SU-895)2026-09-09

Verificando SU-852 è venuta fuori una cosa che vale più del ticket: la regola «metti la RPC nuova in coda alla lista» non protegge niente. Era scritta come criterio di accettazione di SU-852 — «la RPC nuova è l'ultima della lista in NetworkManager.gd (il diff lo mostra)» — e la spunta passava.

  • 📌 Come funziona davvero. In Godot 4 l'id di una RPC è l'indice del suo NOME nell'elenco ordinato ALFABETICAMENTE delle RPC di quello script: modules/multiplayer/scene_rpc_interface.cpp, _parse_rpc_config() prende config.keys(), le ordina con StringName::AlphCompare e assegna id = i. La posizione nel file è irrilevante. In Godot 3 il problema non c'era: il nome viaggiava in chiaro nel pacchetto.
  • [misura] Calcolato sui nomi veri: _srv_declare_skin è l'ultima del file e prende l'id 5 su 10, facendo slittare _srv_declare_unlocks, _srv_heartbeat, _srv_pong, _srv_register e _srv_set_ready. Le due RPC nuove di SU-894 in World.gd prendono id 27 e 83 su 91 e ne fanno slittare 63.
  • ⚠️ Il difetto non si presenta come un errore. Il checksum dell'elenco RPC che Godot scambia (scene_cache_interface.cpp) segnala la differenza a log ma non ferma la partita: i pacchetti proseguono e possono finire sul metodo sbagliato.
  • [docs] Corretti i due posti dove la regola sbagliata era affermata: il commento sopra _srv_declare_skin in NetworkManager.gd e questa regola in ORCHESTRAZIONE.md. Ticket SU-895 per la decisione che resta aperta: se la compatibilità fra versioni ci serva davvero (oggi i tester aggiornano tutti insieme), perché se sì l'unica leva è una convenzione di nomi, non di posizione.
  • 📌 La lezione non è di rete. Un criterio di accettazione può essere verificabile, verificato e inutile insieme: «il diff lo mostra» descriveva un fatto vero sul file e una proprietà falsa sul programma. La domanda che lo smaschera è sempre la stessa — *se questa verifica passasse anche in un mondo dove il difetto c'è, cosa ho dimostrato?*

La carta dei cani fa qualcosa dal primo livello, e i cani mordono davvero (SU-894)2026-09-09

Il punto di partenza era peggio di come sembrava: fischietto_cani ai livelli 1-8 non faceva niente. Chi pescava la carta spendeva una scelta e per otto livelli non vedeva nulla — i tre cani esistevano solo dentro la super «IL BRANCO», al livello 9. Ora ogni pesca ne fa comparire tre, che orbitano e mordono.

  • [feat] PowerUpSystem.gd:879: ogni pesca evoca o rinnova il branco, già dal livello 1. Ripescare rinnova i tre cani (timer da capo), non ne aggiunge altri tre.
  • [feat] SuperPower.gd:740,827,891,905: branco della carta indipendente da quello della super, 3 cani per 10 s, movimento condiviso che esce dall'orbita ellittica esistente e ci rientra, bersaglio più vicino entro 180 px.
  • 📌 Nessun sistema di danno inventato. Nel gioco non esistono vita né take_damage() condivisi: il cane riusa i tre verbi che già ci sono — hit_by_cat() per gli NPC nemici, stun_by_cat() per la polizia, burn() per i nemici di quartiere. Un nemico che non ha nessuno dei tre viene ignorato, non gli si costruisce un quarto canale.
  • [feat] World.gd:4857,4868: in multiplayer il client comunica solo la comparsa; centro, bersagli, durata e morsi li simula l'host, con lo stesso schema di _srv_cat_hit_npc. Un client non può neutralizzare da solo.
  • ⚠️ La retata resta intoccabile — deciso in chat con Ivan il 09/09, e supera la descrizione del ticket, che elencava raid_officers fra i bersagli. Il codice lo diceva già in quattro punti (SuperPower.gd:661 «Esclude SEMPRE la Squadra Sgombero: la retata è intoccabile», più 633, 668, World.gd:4903, Interactable.gd:1757): tre cani che stordiscono la squadra a ogni pesca smonterebbero la minaccia principale, e la carta è più disponibile di qualsiasi super. Trappola per chi ci tornerà: un raid_officer sta ANCHE nel gruppo police, quindi toglierlo dai gruppi cercati non basta — serve il salto esplicito, che ora sta in _dog_can_target().
  • [misura] Sonda headless, 19 controlli su 19 verdi dopo l'esclusione della retata: al livello 1 tre cani a t=0 e zero a t=10 s; ripescando a 5 s restano 3, non 6, col timer ripartito; nemico vicino sconfitto in 1 morso, giullare che assorbe in 3; civili colpiti = 0; carta+super = 3+3 e a fine super 3+0; guardia client senza rete: server=false, morsi=0, nemico intatto; fenicottero, pinguino, yeti e pupazzo tutti bersagliabili→neutralizzati.
  • [fix] Il testo della carta diceva ancora «al livello 9 arriva IL BRANCO» in tutte e 8 le lingue: ora dice che i cani mordono e che al 9 arriva la super (ui_meta.csv:87).
  • [fix] La sonda scriveva il referto in OS.get_environment("SU_TMP"), che esiste solo nella sandbox di Codex: fuori di lì stampava tutti gli OK e poi moriva con uno SCRIPT ERROR sul null. Ora ripiega sulla TMP/ alla radice del repo.
  • 📌 Un rilievo della review incrociata è stato verificato e scartato: «l'host accetta la richiesta di branco senza controllare che il mittente possieda la carta». È vero, ma _srv_cat_hit_npc — la RPC che il ticket indica come modello — non valida il mittente nemmeno lei. Non è una regressione di questo ticket, è il modello di fiducia già in uso.
  • [misura] L'effetto ad area della super, misurato dopo il collaudo di fine onda. Il collaudatore ha fatto notare che nessuno l'aveva verificato — la sonda dei cani copre morso e richiamo, non l'area — e che in sei partite reali la carta non è mai stata pescata. Su un file che ha appena preso 258 righe nuove è il difetto peggiore possibile: la super non farebbe niente, in silenzio. Sonda nuova probe_su894_area.gd, che conta le chiamate vere invece di leggere il codice: civile nel raggio 3 super_flee, polizia nel raggio 3 super_confuse, chi sta a 360 px col raggio a 180 zero, e la Squadra Sgombero 0 e 0 — la retata è intoccabile per misura, non per affermazione.
  • ⚠️ Non provati: gli fps sull'Honor 10 con tre cani e ~45 NPC (serve il telefono) e il multiplayer vero a due peer. La carta non è mai stata pescata in una partita vera: il percorso carta→cani→morso resta verificato dalle sonde, non dal gioco.

I quindici ritratti che mancavano alla selezione personaggio (SU-865, da approvare)2026-09-09

Dei diciassette personaggi giocabili, solo il barbone e la barbona avevano un ritratto ad alta risoluzione: gli altri quindici la vetrinetta li mostrava ingrandendo lo sprite di gioco, morbido, con un avviso [SU-750] skin senza ritratto nel log. Adesso ci sono tutti. Nessuna riga di codice: la vetrinetta carica già skin_portrait_<id>.png quando il file esiste.

  • [feat] 15 PNG in files/homeless_city/assets/ui/, generati da Astra (gpt-6-astra) a figura intera su fondo magenta e passati in tools/su599_ritratto_skin.py --figura-intera --altezza 591 — maschera del fondo, UnsharpMask e contorno di 2 px, lo stesso trattamento di m1 e f1.
  • [feat] tools/sprite/su865_ritratti.sh, versionato come gli altri script d'arte: per ogni skin passa ad Astra il foglio raw di sprites_raw/PEOPLE/ e il PNG di gioco come riferimenti.
  • [misura] 17 ritratti su 17 a 591 px di altezza (larghezze da 228 a 336). Provino di tutti e diciassette in TMP/su865_provino_ritratti.png, con m1 e f1 in testa come metro di paragone.
  • 📌 Il ciuffo del punk maschio ha rimesso in discussione la pipeline, ed è la cosa più utile uscita da questo ticket. Il suo ciuffo in gioco è un fucsia acceso; Astra, disegnandolo grande, l'ha reso quasi identico al fondo #FF00FF e la maschera se l'è mangiato insieme allo sfondo. Il primo rimedio è stato chiedere ad Astra un ciuffo bordeaux — funzionava, ma tradiva il criterio 3 del ticket: il ritratto non mostrava più il personaggio che si vede in partita.
  • [feat] La strada scelta da Ivan (09/09) è l'altra, e vale per sempre: il fondo di generazione non deve MAI essere un colore che il personaggio porta addosso. tools/su599_ritratto_skin.py accetta ora --fondo {magenta,verde}, con magenta di default — gli altri quattordici ritratti non cambiano di un pixel, verificato rigenerando breaker e confrontando l'md5, identico. Il nuovo tools/sprite/su865_ritratto_fondo_verde.sh genera su verde #00FF00 e chiede esplicitamente di tenere il rosa vero del personaggio, invece di scansarlo.
  • [misura] Ciuffo del ritratto rifatto: RGB medio (211, 32, 109) contro il (160, 30, 94) della cella di gioco — stessa tinta fucsia, più luminosa come è normale in un ritratto ad alta risoluzione. Il confronto affiancato sta in TMP/su865_punk_m_confronto.png.
  • ⚠️ Il verde non è universale: vale per chi indossa il magenta e non il verde. Il giocoliere ha il gilet a rombi verdi, quindi per lui resta il magenta. È la stessa regola vista dall'altro lato.
  • 📌 Gli altri quattro confronti tornano: la punk femmina tiene il suo ciuffo azzurro, i due uomini orchestra la giubba rossa, il breakdancer il cartone sotto il braccio.
  • ⚠️ Non provato: che l'avviso [SU-750] non scatti più a runtime. È verificato staticamente — i quindici nomi rispettano il pattern che MainMenu.gd:8836 cerca — ma non aprendo la vetrinetta. Fatto lo mette Ivan dopo aver guardato.

La panchina «garantita» non nasce più apposta fuori dai parchi (SU-893)2026-09-09

Non era un caso raro, era una regola scritta: con park_min > 0 il minimo garantito contava solo le panchine fuori dai parchi (WorldGenerator.gd:4193), così il ripiego ne piantava sempre una fuori, escludendo apposta i park_rects e finendo sugli angoli di marciapiede dei palazzi. Ora che ogni quartiere ha almeno due parchi, quella regola produceva solo panchine fuori posto.

  • [fix] WorldGenerator.gd:4193: il minimo BENCH guarda il totale, quindi le panchine dentro i parchi lo soddisfano. Con park_min = 0 il percorso storico resta identico — è lì perché il giocatore non resti senza un posto dove dormire.
  • [fix] WorldGenerator.gd:4200,4240: il vecchio ripiego sopravvive solo se il candidato cade dentro una piazza; gli angoli di marciapiede sono scartati. Le panchine delle piazze restano, come deciso in chat l'08/09: una piazza arredata è un posto sensato per una panchina.
  • [feat] su323_censimento.gd ora conta in quattro colonne — totali, dentro un parco, dentro una piazza, altrove — per quartiere e per seme, e fallisce da solo se un seme ha zero panchine o altrove > 0. Prima la piazza non la distingueva: la misura era metà del ticket.
  • [misura] 30 semi × 8 quartieri, prima e dopo: altrove 95 → 0; panchine di piazza identiche su 240 righe su 240 (502 in totale); dentro i parchi identiche (2.882); nessun seme a zero panchine; totale 3.479 → 3.384. Tabelle in TMP/su_L2/su893_prima.tsv e TMP/su_L2/su893_dopo.tsv.
  • 📌 La misura vedeva il difetto PRIMA del fix, ed era la condizione posta nel brief: un censimento che desse altrove = 0 già prima non avrebbe dimostrato niente — avrebbe solo guardato nel posto sbagliato.
  • [misura] Determinismo preservato: nessun dado nuovo e nessun Dictionary iterato per decidere la città, che in multiplayer è ciò che tiene uguali le città dei due peer.

Da ubriaco panchina, fontana e bidone tornano usabili (SU-889)2026-09-09

Decisione di Ivan: il barcollamento e la vista che ondeggia bastano come penalità dell'ubriachezza. Toglierti anche il diritto di dormire, lavarti e mangiare puniva troppo — e frugare nel bidone è l'unico modo gratuito di mangiare.

  • [fix] Interactable.gd:663: cade per intero il gate if GameState.is_drunk: che intercettava BIN, BIN_CLOSED, FOUNTAIN e BENCH respingendoli con una notifica e un cooldown di rimbalzo da 1 s. Da ubriaco i quattro tipi si comportano ora esattamente come da sobrio, col loro use_cooldown. Nessuna penalità sostitutiva: niente tiro di dado, niente effetto dimezzato, niente ritardo — è proprio la penalizzazione che il ticket toglie.
  • [fix] ui_mondo.csv: via le tre chiavi MONDO_UBRIACO_* in tutte e 8 le lingue. grep -rn "MONDO_UBRIACO" files/homeless_city/ torna zero righe, che era un criterio letterale del ticket: una chiave orfana nel CSV lo lasciava aperto.
  • [misura] Effetti pieni, misurati su GameState prima/dopo, sobrio e ubriaco: panchina energia 10→40, fontana igiene 10→35, bidone fame 10→35 e igiene 50→45, forziere denaro 0→36. Cooldown 37,0 s = use_cooldown dell'oggetto in tutti i casi, cioè il rimbalzo da 1 s è sparito col gate.
  • 📌 Quello che NON è caduto, ed era l'errore facile di questo ticket: i blocchi dello spazzino su panchina, fontana e bidone (Interactable.gd:642,650,658) stanno poche righe più su, sono un'altra cosa e continuano a valere anche da ubriaco. Resta vietato anche il borseggio da ubriaco (ModularNPC.gd:943): lì la mano ferma è il senso del gesto.
  • ⚠️ Non provato: la partita MP con la panchina già occupata da un altro giocatore. La garanzia sta in Interactable.gd:717-724, che include BENCH nel broadcast del cooldown condiviso e la rimozione del gate avviene prima, senza scavalcarlo — ma non è stato riprodotto con due peer. E l'ubriachezza nella sonda è stata forzata, non raggiunta bevendo due drink.

Sedici numeri di strada: un suono di busking per ogni skin (SU-891, in TMP e da approvare)2026-09-09

Fino a ieri tutte e diciassette le skin suonavano busking.wav, la trombetta del barbone: il mago, lo sputafuoco e la rapper facevano il loro numero con lo strumento di un altro. Adesso ognuno ha il suo. Nessuna riga di codice toccata e nessun file importato nel gioco: il ticket genera e basta, il montaggio è SU-892 e parte solo dopo che Ivan ha ascoltato.

  • [feat] 16 file in TMP/su_busk_audio/, busk_<id>.wav con l'id copiato da Skins.gd. Sono 16 e non 17 perché m1 tiene la trombetta di oggi. Generati con ElevenLabs (text_to_sound_effects, loop=true), convertiti con ffmpeg in mono 44,1 kHz.
  • 📌 Il cancello della licenza è caduto prima di generare, come chiedeva il ticket: piano starter, a pagamento (status: active), non il gratuito che in INVENTARIO_PROVENIENZA_ASSET.md è un debito già aperto. Le licenze di questi sedici file sono pulite — e valgono dalla generazione, non dal download.
  • [misura] Tutti 4,48 s (chiesti 3-6), mono 44,1 kHz, picco fra −1,19 e −5,40 dBFS (chiesto ≤ −1). Verificati due volte: dall'agente con ffmpeg e dall'orchestratore con una lettura indipendente dei campioni.
  • 📌 Il «loop pulito» è diventato un numero invece di un'opinione. Confrontare il primo e l'ultimo campione non basta: un salto grande non è un click se dentro il file i salti sono altrettanto grandi. La misura che risponde davvero è il rapporto fra il salto al punto di ricucitura e i salti ordinari fra campioni consecutivi: sta fra 0,00× e 0,64× su tutti e sedici, cioè la giuntura è più dolce delle transizioni normali del suono. Nessun click, e la prova non passa dall'orecchio di nessuno.
  • [misura] 16 checksum distinti: le cinque coppie maschio/femmina (clown, giocolieri, uomo/donna orchestra, punk, rapper) sono prese diverse, non lo stesso file copiato due volte — che era il modo più facile di far finta.
  • ⚠️ Quello che resta non provato è l'unica cosa che conta davvero: se ogni suono corrisponda al *suo* numero. Nessuna misura lo dice — busk_mime.wav può essere tecnicamente perfetto e contenere una trombetta. Lo decide Ivan ascoltando, ed è per questo che su questo ticket «Fatto» è la sua approvazione.
  • ⚠️ I file stanno in TMP/, che è ignorata da git: finché SU-892 non li importa in files/homeless_city/assets/sounds/busk/, esistono solo su questo Mac. Un git clean li porta via.

Le finestre di Claude e Codex non si confrontano allo stesso modo2026-09-08

Regola di Ivan: con l'account Max di Codex la finestra è settimanale, non da 5 ore — quindi «Codex 11% contro Claude 10%» non è un confronto, e va guardata anche la settimanale di Claude, che quella sera era al 70%. Nessuna riga di gioco toccata.

  • 📌 Il caso che ha fatto nascere la regola. CODEX 11% e CLAUDE 5h 10% sembravano due piani pari: i lotti sarebbero rimasti a Claude. La settimanale di Claude era al 70% — 14 punti di scorta alla soglia contro 79 di Codex, quattro volte meno. Aggiunta di Ivan: anche quando nella 5h di Claude il lotto ci starebbe, se la settimana di Claude è avanti e Codex è scarico il lotto fungibile si sposta su Codex — quel che si spende su Claude non torna fino al reset settimanale, la 5h si riazzera stasera comunque.
  • ⚠️ Le settimanali di Claude sono più di una, e la seconda non si vedeva. L'API di /usageseven_day (la globale) ma anche una lista limits con le finestre *scoped* per modello: l'8/09 la globale era al 71% e quella di Fable al 76%, severity warning — il vincolo vero della serata, e tools/token_finestra.py non la leggeva. Una scoped limita l'orchestrazione con quel modello, non i builder: un dev-sonnet non tocca il budget di Fable, ma pesa sulla globale. Perciò i builder si contano sulla globale, e il fattore token/punto di una scoped si misura filtrando i transcript per modello (consumo_da(..., modello=)).
  • [feat] tools/token_finestra.py: legge tutte le settimanali, stampa una seconda riga SETTIMANALE N% · al 90% mancano X punti ≈ N builder (più l'avviso della scoped se è la più stretta), e in --entrambi chiude con una riga CONFRONTO settimanale che dice a chi vanno i lotti fungibili — con lo scarto sotto i 10 punti dichiara le scorte pari e rimanda al tipo di lotto. La soglia di congelamento ora scatta anche sulla settimanale, non solo sulla 5h; --json porta il blocco settimanale_stato.
  • [fix] Il costo di un builder era il doppio del vero. La costante era ferma a 29M grezzi (i 37 subagenti del 21-22/08); la misura del 29/08 su 91 lotti dice 14,5M, ed è quella scritta in CLAUDE.md da giorni. Con la vecchia, «al 90% mancano 77 punti» dava 4 builder invece di 7.
  • [docs] ORCHESTRAZIONE.md § «Codex — il secondo piano» punto 2 (la vecchia riga diceva l'opposto: fungibili «al piano con più margine sulle 5 ore») e regola 9 della Dieta token; CLAUDE.md, riga dei due piani. Allineati anche WORKFLOW/README.md, WORKFLOW/02_SPRINT.md (esempio della riga e § «Congelamento e ripresa») e .claude/commands/sprint.md, punto 7.