← Novità

v0.42

2026-08-28 → 2026-09-02 · 96 voce/i di changelog

L'ondata Apple è partita da sola: 23 su 23, senza svegliare nessuno2026-09-02

Ivan la sera prima: *«avvisa tester fallo te in automatico quando le versioni ios e android saranno attive, distinguendo quando una e attiva o l'altra è attiva»*, e poi *«ma fallo ogni mezz'ora»*.

  • Apple approvata e 23 destinatari avvisati alle 08:09: 22 WhatsApp (tutti success: true) e 1 Telegram (Marco Bianco, scritto a mano e riletto nel campo prima dell'invio). Nessuna conferma richiesta a Ivan, perché il testo l'aveva già approvato in anteprima il giorno prima — ed è esattamente ciò che aveva chiesto.
  • 📌 Il cron ha fatto quello per cui esisteva. Al controllo delle 08:07 stato_canali.py ha visto betaReviewState APPROVED (alle 12:23, 19:07 e a ogni giro prima era WAITING_FOR_REVIEW) e ha mandato. La distinzione fra i due canali ha retto: gli Android erano stati avvisati il 1/09 col link del Play Store in coda, gli Apple ora senza quella coda, che su TestFlight non ha equivalente.
  • Il marcatore serve, e si è visto oggi: il cron era rimasto accodato più volte, e senza BETATESTING/avvisati_apple_v042.marker ogni firing avrebbe rimandato l'annuncio alle stesse 23 persone. Scritto a invio finito, come per la 0.41.
  • 📌 Cron spento a giro concluso invece di lasciarlo girare fino alla scadenza dei 7 giorni: un controllo che non ha più niente da controllare è solo rumore nella chat di Ivan.
  • ⚠️ E resta vero il limite dichiarato quando è stato acceso: cron e job di sessione vivono solo finché vive la sessione. Stavolta ha retto — la sessione era ancora aperta quando Apple ha approvato — ma non è una garanzia, ed è giusto che sia scritto qui e non scoperto la prossima volta.

Consuelo Maione arruolata: due elenchi Android, e l'annuncio che non le serve2026-09-01

Ivan porta nome, numero, piattaforma e mail insieme: è la scorciatoia prevista da /arruola-tester, quindi niente messaggio di reclutamento e si salta dritto alla registrazione.

  • Riga 81, ID 80, +39 370 326 4948, consuelomaione@gmail.com, Android = Sì, Avvisa tester = Sì. Nessun doppione: controllato sul Nome prima di scrivere.
  • Aggiunta nei DUE elenchi, ed entrambi verificati dopo il salvataggio e non prima: mailing list «Test Interno Street University Android» del canale Alpha, da 41 a 42 utenti; utenti di prova OAuth del progetto axiomatic-path-505007-u7, da 53 a 54 (restano 46 posti su 100).
  • 📌 Il secondo elenco è quello che nessuno collega al sintomo: la sua mail è una @gmail.com, e chi sta solo fra i tester di Play scarica il gioco e poi viene respinto al login Google. Sembra il gioco rotto, ed è un elenco incompleto.
  • ⚠️ Due trappole dell'interfaccia, tutte e due già viste oggi. La griglia della mailing list rende in ritardo: subito dopo il salvataggio la riga risultava vuota, e a leggerla lì si sarebbe concluso che il salvataggio non era passato — la conferma è arrivata ricaricando la pagina (42 utenti). E in Cloud Console il primo clic su «Salva» non salva: trasforma il testo digitato in chip, e serve un secondo clic. Chi si ferma al primo se ne va convinto di aver aggiunto la persona.
  • 📌 Non riceve l'annuncio della 0.42, partito agli altri 39 poco prima: con la conferma entra e scarica direttamente l'ultima, quindi non le manca niente. Segnalato a Ivan invece di deciderlo da solo.
  • 📌 Data invito resta vuota, e non è una dimenticanza: --invitato non la scrive, e nel foglio è compilata a macchia di leopardo (41 «Invitato» senza data contro 10 con). La skill dice di non toccare altre colonne, quindi si lascia com'è.

`stato_canali.py`: chi ha davvero la versione, senza chiederlo a tre API a mano2026-09-01

Ivan vuole che l'avviso ai tester parta da solo quando i canali si accendono, distinguendo Android da Apple. Il §0 di /avvisa-tester dice già «guarda lo stato vero, non il tag git», ma quel controllo era tre giri di API diversi fatti a mano ogni volta.

  • Strumento nuovo tools/release/stato_canali.py: legge Play (edit → traccia alphastatus), TestFlight (betaReviewState della build della versione in uscita) e desktop (data dei file nei cloud, che API non ne ha), e stampa una riga per canale. Con --attendi android|apple diventa la sentinella dell'automazione: esce 0 quando quel canale è vivo.
  • 📌 La versione la legge da files/homeless_city/project.godot, non gliela si passa: così non può controllare lo stato della build sbagliata, che è il modo silenzioso di avvisare i tester di una versione che non c'è.
  • ⚠️ Un errore di rete non uccide l'attesa. Il ciclo cattura le eccezioni e stampa «errore ignorato»: se uscisse lì, il job risulterebbe *completato* e nessuno avviserebbe i tester — la trappola già scritta in memoria per le sentinelle che escono 0.
  • 📌 Primo esito, e dice esattamente perché serviva: Android vivo (alpha, versionCode 42, completed), Apple ancora WAITING_FOR_REVIEW, desktop fresco di oggi. Tre stati diversi nello stesso minuto: avvisare tutti insieme avrebbe mandato 23 persone su una build che non possono scaricare.
  • ⚠️ Due sviste mie corrette misurando, non rileggendo: RADICE risaliva un livello di meno (il file sta in tools/release/, non in tools/) e il primo conteggio dei destinatari usava grep -c su una riga per record — ma il record di un tester Android occupa più righe, perché il messaggio contiene a capo. Contato bene: 39 Android, 23 Apple.

I simboli nativi: la riga della skill era ferma alla 4.6.22026-09-01

Domanda di Ivan: *«ma per android devo caricare i simboli anche per questa versione?»*.

  • Sì, e stavolta lo zip va anche rifatto da capo. La regola scritta in .claude/skills/pubblica-store/SKILL.md è «a ogni release, non solo alla prima» e «da capo a ogni cambio di versione di Godot»: con SU-700 il motore è passato alla 4.7.2 (verificato col comando: 4.7.2.stable.official.ed1daf0bf), quindi lo zip della 4.6.2 usato finora non vale più — sarebbero i simboli di un motore diverso da quello che ha costruito la build.
  • ⚠️ E la skill inchiodava «oggi 4.6.2» con l'URL a fianco: chi l'avesse seguita alla lettera avrebbe caricato i simboli sbagliati credendo di aver fatto la cosa giusta, che è il modo peggiore di sbagliare — nessun errore, e i crash in Android vitals restano illeggibili lo stesso. Aggiornata alla 4.7.2, e con scritto che quel numero si legge dal comando e non dalla riga, perché scade a ogni bump.
  • URL della 4.7.2 verificato prima di scriverlo (HTTP 200), invece di dedurlo dallo schema del nome.
  • 📌 Aperto SU-709 per il buco che resta: il ponte EOS è compilato senza simboli. Domanda di Ivan — *«ma per eos già con il vecchio godot era così?»* — e la risposta, misurata: sì, ed è la stessa identica .so. file su tools/android/eosg_16kb/libeosg.android.template_release.arm64.so dice stripped; in git quel file ha un commit solo (86efdb5, SU-280) e non è mai stata ricompilata, nemmeno col bump alla 4.7.2; la riga di build nella ricetta non passa debug_symbols. Non è una regressione del motore nuovo: è la nostra ricetta, identica da agosto.
    • ⚠️ Una misura sbagliata corretta in corsa: avevo concluso «zero simboli» da un llvm-readelf che su questo Mac non esiste via xcrun — quello zero era il comando che falliva, non un dato. La risposta vera l'ha data file, in una riga.
    • 📌 Il nodo che rende SU-709 meno banale di «aggiungi due flag»: su Play il file dei simboli è uno solo per bundle, quindi caricare quelli di EOS *sostituirebbe* quelli del motore invece di aggiungersi. Serve uno zip combinato, ed è la parte che può far fallire il ticket — scritta lì dentro perché non venga scoperta a caricamento fatto.
  • 🔴 E il passo non torna a vivere in una skill: adesso lo stampa lo strumento. Ivan: *«mi raccomando questa cosa in futuro controllala sempre, non te la devo ricordare io»*. Aveva ragione due volte — non l'avevo controllato, e il passo *esisteva già* scritto in /pubblica-store. Ma quel giorno Play era stato fatto lanciando tools/release/play_upload.sh direttamente, e chi non apre la skill non incontra mai il passo. Quindi ora è play_upload.sh a dirlo in coda all'invio, con l'URL calcolato sulla versione vera del motore letta da Godot --version, non copiata da una riga che scade. 📌 La regola generale, che vale oltre questo caso: un passo che si può saltare senza vedere un errore va detto dove si salta, non in un documento che si apre solo se ci si ricorda di aprirlo.
  • 📌 Non è automatizzabile con quello che abbiamo: tools/release/play_upload.sh non sa caricare i simboli — nessuna occorrenza di deobfuscation/symbol nello script — e lo zip scompattato pesa ~700 MB, quindi nemmeno il tool del browser può farlo (accetta file sotto i 10 MB). Oggi è un passo a mano di Ivan in Play Console. L'API di Play ha un endpoint per i file di deoffuscamento: se un domani questo passo diventa noioso, è un ticket, non una cosa da improvvisare qui.

La v0.42 in revisione su TestFlight, e una GET che manda a caccia2026-09-01

  • Build 0.42 su TestFlight, revisione partita: processingState VALID, note *What to Test* scritte (1.324 caratteri, italiano nello slot en-US che è il ripiego universale), collegamento al gruppo esterno «Divano» HTTP 204, betaAppReviewSubmission HTTP 201, e la prova che conta — externalBuildState passato da READY_FOR_BETA_SUBMISSION a WAITING_FOR_BETA_REVIEW.
  • ⚠️ Corretta una nota di .claude/commands/testflight.md che mi ha mandato a caccia di un guasto inesistente. Diceva che la GET del gruppo «si aggiorna con ~10 s di ritardo»: in realtà GET /v1/builds/<id>/betaGroups e GET /v1/betaGroups/<id>/builds tornano vuote e basta, sempre. 📌 A dirlo è la controprova, non l'attesa: la 0.41, che i tester hanno installato ed è IN_BETA_TESTING, risulta anche lei con gruppi=[]. Tre giri di polling buttati a cercare conferma dove non ce n'è mai stata. L'unica conferma del collegamento è il 204 della POST; l'unica prova che la revisione è partita è externalBuildState.

SU-708: le statiche di Firebase si linkano, non si incorporano2026-09-01

Seguito diretto della voce (74): trovata la causa, corretta e verificata. Ivan: *«apri il ticket e risolvilo subito»*.

  • Dodici .xcframework spostati da embedded=[…] a linked=[…] nei due file files/homeless_city/iOS/plugins/firebase_core/firebase_core.gdip e files/homeless_city/iOS/plugins/firebase_messaging/firebase_messaging.gdip. Sono tutte e dodici librerie statiche — censite una per una con file, che risponde current ar archive: la lettura precedente, che dava GoogleAdsOnDeviceConversion come dinamica, era un artefatto di un uniq sporco ed è stata rifatta pulita.
  • Il post-volo passa e Apple conferma: MinimumOSVersion 15.0, ogni framework coerente col proprio binario, firmato «Apple Distribution», profilo con beta-reports-active, e la validazione di App Store Connect risponde VERIFY SUCCEEDED with no errors. Dove prima c'erano dodici ITMS-90208 certo.
  • In Payload/…app/Frameworks/ non c'è più nessun Firebase: restano EOSSDK, libeosg e libsu_native_auth, cioè esattamente i tre che sono davvero dinamici. Il criterio del ticket chiedeva questo.
  • 📌 E la prova che non sia semplicemente sparito, che è la domanda vera quando si smette di incorporare qualcosa: otool -oV sull'eseguibile trova 114 classi Objective-C FIRApp/FIRMessaging e 188 stringhe firebase, e il binario dell'app passa a 63 MB. Firebase è *dentro*. ⚠️ nm invece ne trova zero, e non vuol dire niente: in release i simboli sono strippati — cercare lì avrebbe dato un «non c'è» falso.
  • 🔴 E la correzione NON poteva restare sul disco: ios/plugins/ è gitignorato (files/homeless_city/.gitignore riga 46, i moduli li porta lo zip di GodotX). Un git add dei due .gdip viene rifiutato, e una correzione fatta a mano sarebbe sparita alla prima reinstallazione del plugin — esattamente come è già successo con la libeosg a 16 KB. Quindi lo spostamento vive ora dentro tools/installa_firebase.sh (passo 4a-bis), che lo riapplica a ogni installazione.
  • Il passo è idempotente e provato sul difetto vero, non solo sul verde: sui .gdip già corretti stampa «era già a posto» e non tocca niente; su un .gdip sporco ricostruito apposta sposta le voci e le fa comparire in linked. E se un domani lo zip arrivasse con qualcosa già dentro linked, si ferma invece di fondere le due liste alla cieca, che è il modo di rompere il modulo in silenzio.
  • ⚠️ Resta da verificare sul telefono che le push arrivino ancora, ed è il criterio 3 del ticket: -ObjC era già nei linker_flags (serve alle statiche di Firebase per registrare le categorie), ma «compila e valida» non è «il token nasce e la notifica arriva». Non si chiude sul solo verde del post-volo.
  • 📌 L'IPA è pronto e non è stato caricato: testflight.sh senza --invia si ferma prima, ed è il cancello di Ivan.

Il pre-volo iOS moriva senza dire niente, e sotto c'era un secondo guaio2026-09-01

Trovati portando la v0.42 su TestFlight, e sono due difetti distinti che si nascondevano a vicenda.

  • ⚠️ tools/release/testflight.sh --controlla usciva 1 dopo il secondo controllo, senza stampare una riga. Non un fallimento: una MORTE. _minos() è una pipeline (xcrun vtool | awk) e con set -o pipefail (riga 55) un vtool che esce non-zero fa fallire l'assegnazione m="$(_minos …)", e set -e ammazza lo script. A farlo uscire non-zero sono gli .xcframework di Firebase arrivati con SU-701/707, che dentro portano anche fette macos, tvos, watchos e maccatalyst: 41 cartelle su cui non esiste nessun minos iOS. Il find le raccoglieva tutte perché escludeva solo *simulator*.
  • Corretto in due punti, perché uno solo lasciava il difetto latente: _minos ora chiude con || true e non può più uccidere lo script, e il find guarda le sole fette iOS. 📌 Il secondo non è ridondante: senza il filtro, passata la morte, il pre-volo avrebbe confrontato il minos di tvOS col deployment target dell'app — due numeri che non parlano della stessa piattaforma, e un ❌ inventato. Dopo la correzione il pre-volo passa tutti e quattro i controlli: certificato, profilo (scade 2027-08-04), deployment target iOS 15.0.
  • ⚠️ E sotto c'era il guaio vero, che il pre-volo morendo copriva: il post-volo boccia dodici framework Firebase con «ITMS-90208 certo» e non carica niente. Nell'IPA dichiarano minos 100.0 — nel Mach-O e nell'Info.plist — e 100.0 non è una versione di iOS.
  • 📌 La prima diagnosi era sbagliata, e la misura l'ha smentita prima che diventasse una toppa. Avevo concluso «Firebase distribuisce framework dinamici con un minimo-sentinella, si riscrive il minos» e avevo già scritto il passo che lo faceva. Poi ho guardato il sorgente invece della copia incorporata: xcrun vtool sul binario dentro ios-arm64/FirebaseCore.framework non trova nessun minos, e file dice current ar archive. Sono librerie STATICHE, e lo sono quasi tutte — misurate una per una: FBLPromises, FirebaseCore, FirebaseCoreInternal, FirebaseInstallations, FirebaseAnalytics, FirebaseMessaging, GoogleUtilities, GoogleDataTransport, nanopb, GoogleAppMeasurement*. L'unica dinamica è GoogleAdsOnDeviceConversion.
  • 🔴 La causa vera: i due .gdip del plugin dichiarano linked=[] e mettono TUTTO in embedded=[…]. Una libreria statica non si incorpora, si linka: incorporandola finisce in Payload/…app/Frameworks/ e il suo MinimumOSVersion 100.0 — che dentro una statica non ha alcun significato e non sarebbe mai uscito — diventa una dichiarazione vera del bundle. Il 100.0 è il sintomo; il difetto è l'embedded. Va spostato in linked in files/homeless_city/iOS/plugins/firebase_core/firebase_core.gdip e .../firebase_messaging/firebase_messaging.gdip, e riprovato.
  • La toppa scritta sulla diagnosi sbagliata è stata TOLTA, non lasciata «tanto non fa danno»: riscriveva il minos di dodici binari di terzi con vtool per curare un sintomo, e sarebbe rimasta in pipeline a nascondere il difetto vero a chiunque fosse passato di lì dopo. In testflight.sh resta la sola correzione giusta, quella del pre-volo che moriva.
  • ⚠️ La build iOS della v0.42 resta ferma sul disco. macOS, Windows e Play non sono toccati: il post-volo ha fatto esattamente il suo mestiere, fermandosi prima di bruciare un numero di build. 📌 E altool non avrebbe aiutato: il commento in testa a testflight.sh racconta che il 2026-08-05 la sua validazione passò «with no errors» e Apple bocciò comunque in elaborazione.

Le note della v0.42 in otto lingue, da 94 voci di CHANGELOG2026-09-01

Ivan: *«sì, gli 8 In revisione li rilasciamo così, parti»*.

  • 26 voci scritte per il giocatore, tirate fuori dalle 94 voci di CHANGELOG e dai 133 commit accumulati dal tag v0.41-performance-improvements-telemetry-fix-2sec-cooldown-hit. Le novità grosse: i topi che aspettano al muro e la banchina che dal giro 3 diventa un tiro a segno di zombie, SOLLEONE col sole che ti cerca e i cactus, IL BRANCO, i piccioni e quello d'oro, il punteggio che si muove mentre lo guadagni, l'ENDLESS che si accende un quartiere alla volta, le push su Android.
  • 📌 I nomi propri sono stati presi dai CSV di traduzione, non inventati: SOLLEONE è DOG DAYS/CANICULE/ПЕКЛО/三伏天, IL BRANCO è THE PACK/LA MEUTE/СТАЯ/狗群, LA BARACCA è THE SHACK/LA CHABOLA/ХАЛУПА/小窝. Tradurre a orecchio avrebbe prodotto note che nominano cose che a schermo si chiamano in un altro modo — il difetto che il giocatore nota subito e che nessuna sonda vede.
  • Versione a 0.42 in files/homeless_city/project.godot, e version/code 41 → 42 su entrambi i preset Android: il pre-volo di export_all.sh confronta i due numeri e si sarebbe fermato con «41 non allineato a 0.42». Nessuno script fa questo secondo bump, è scritto solo in /pubblica-store.
  • ⚠️ Sette voci riscritte e due aggiunte su correzione di Ivan alla rilettura, e le due più grosse erano errori di sostanza. (1) I topi non stanno appiattiti al muro: corrono nel tunnel. Avevo scritto l'agguato al muro pescandolo dalla voce (24) del 29/08 — ma quel disegno è caduto col KO di Ivan del 31/08 («devono arrivare da sinistra verso destra e devono essere tanti»), ed è annotato in files/homeless_city/scripts/world/BonusLevel.gd:840 proprio perché nessuno lo rimetta. Riscritta sul codice di oggi: nascono 300 px dietro le spalle, corrono a 170 px/s contro i 90 del barbone, derivano di 26 px/s verso di te (intralcio, non inseguimento) e squittiscono alla nascita, cioè si sentono prima di vedersi. 📌 Una nota di CHANGELOG vecchia di due giorni può descrivere un comportamento già smontato: la fonte giusta per le note di rilascio è il codice, non la cronologia. (2) Le push non sono solo di Android: iOS è entrato in funzione con SU-701/707, e il preset iOS imbarca GodotxFirebaseMessaging e il GoogleService-Info.plist.
  • Le altre cinque, e le due voci nuove: i nemici della metropolitana hanno un livello d'ingresso e va detto (topi dal livello 2, zombie dal livello 3); la dimensione di joystick e tasti si regola separatamente dalle opzioni; scoreggiare vuole la pressione tenuta e ora si può anche col gatto in braccio, che è la novità vera; la cartina è più grande senza sottintendere che prima fosse illeggibile. Aggiunte su richiesta di Ivan una voce sulla prima taratura della difficoltà letta sulla telemetria 0.40/0.41 — la gattara toglieva una vita nel primo minuto in 18 partite su 18, e sue erano 17 delle 22 morti per vite finite di tutta la beta — e una sul gatto che non attraversa più i muri, tolta dall'elenco generico dei difetti perché è il caso che si riconosce.
  • Controllato: guardia 1b senza avvisi (nessuna delle otto lingue è rimasta indietro), zero parentesi quadre in tutti e otto i file — il parser di files/homeless_city/scripts/ui/MainMenu.gd inietta BBCode senza escape — 13 sezioni per file con le vecchie tutte al loro posto, e compile-check exit 0 senza errori di parse.

Gli asset raw seguono i commit: un tag senza sorgenti non è un ripristino2026-09-01

Ivan: *«allora committa LFS, e da ora in poi quando committiamo e tagghiamo anche LFS deve seguire i commit, perchè altrimenti ripristinando una vecchia versione non troviamo più i raw»*.

  • ⚠️ Fuori da git c'erano 29 MB di arte, e nessun passo del ciclo se ne accorgeva. Controllando i cancelli prima della release: 23 file raw mai aggiunti — i cani del branco, i tre piccioni, il ratto della metro, i cuori della vita, map_bank/map_church, la tazzina di PICKUPS/, i nove wav fra tunnel e bonus stage — più 10 con modifiche vere sopra la versione committata. Non «falsi modified»: sprites_raw/WORLD/subway_train.png era passato da 171 KB a 1,4 MB, le cinque arch_delivery_* erano state ridisegnate. Verificato confrontando l'oid del puntatore LFS con lo sha del file su disco, non fidandosi dello stato.
  • Committati e spinti (81d0b50): 33 file, tutti e 33 controllati uno per uno come puntatori LFS — 89 righe in git, 137 MB di oggetti caricati sul remoto. Se uno fosse entrato come blob normale sarebbe finito nella storia per sempre.
  • 📌 Perché il giro di release non li vedeva, ed è la parte che valeva la pena capire. rg_raccogli_modifiche li escludeva apposta (RAW_EXCLUDE) come falsi-modified da sandbox Cowork: la bozza delle note non li nominava, il piano git nemmeno. Una regola nata giusta per un ambiente era diventata una cecità nell'altro. Ora i raw non sono più scartati ma tenuti a parte per essere mostrati: RAW_DIRTY finisce in una sezione dedicata della bozza, e rg_guardia_raw_lfs stampa l'avviso col comando da lanciare prima del piano git. Vale per /release e per /tag-intermedio, che condividono la lib.
  • ⚠️ E il piano stampato conteneva il comando che avrebbe distrutto quell'arte: git lfs checkout, con la nota «ripulisce i finti modified». Sul Mac quei modified sono veri — lanciarlo avrebbe riportato sprites_raw/WORLD/subway_train.png a 171 KB in silenzio, senza un errore. Tolto dal piano e sostituito con l'avviso opposto; resta vero solo in sandbox Cowork, e nei documenti adesso è scritto da che parte vale.
  • La guardia è stata provata sul difetto vero, non solo a raw puliti: con un file sporco in sprites_raw/ scatta, lo nomina e finisce nella bozza; a raw allineati tace. bash -n verde su tutti e tre gli script.
  • 📌 Regola scritta in quattro punti, perché era in quattro punti che diceva il contrario: CLAUDE.md, WORKFLOW/04_RELEASE.md (passo 5 e nota sandbox), WORKFLOW/README.md, e i commenti degli script.

La folla di zombie adesso mugugna anche dopo essere scesa2026-09-01

Ivan: *«gli zombie e la metro alzali ancora di volume, mentre gli zombie riproduci il suono più spesso, più che altro quando sono usciti tutti dalla metro non viene più riprodotto, consiglierei di riprodurre il suono ogni tot fuori dalla metro, magari random così non sembra scandito, come se fosse appunto una folla di zombie che si muove e qualcuno mugugna ogni tanto»*.

  • ⚠️ La parte centrale non è un volume, è un difetto di progetto — mio. La voce era attaccata solo all'uscita dalla porta: scesi i quaranta, la banchina ammutoliva. E non per un istante: la discesa dura ~6 s su BANCHINA_SEC = 30, quindi restavano ~23 secondi di zombie che ti inseguono in silenzio. Nessuna sonda poteva vederlo, perché tutte misuravano la discesa — l'ha sentito Ivan giocando.
  • Adesso la folla mugugna di suo, con l'intervallo pescato ogni volta fra 0,5 e 1,6 s: mai fisso, che è letteralmente il «magari random così non sembra scandito» della richiesta, ed è la stessa cura già presa per gli squittii dei topi.
  • 📌 E il ritmo rallenta man mano che la folla si assottiglia (ZOMBIE_MUGUGNO_FATTORE_MAX): con l'intervallo fisso, tre zombie superstiti mugugnerebbero come quaranta — si sentirebbe una folla che non c'è più, il genere di cosa che non si sa dire perché suona falsa. Col tetto a 4× gli ultimi due non restano zitti mezzo minuto, che è l'errore opposto.
  • ⚠️ Trovato scrivendolo: senza una guardia la folla avrebbe mugugnato anche NELL'OUTRO. passo_uscita() chiama anche lui _muovi_folla() — quindi a round chiuso, mentre il barbone sale in carrozza e la gente sgombera, i versi sarebbero continuati. Il conto si fa su chi è ancora in giro (STATO_USCITA escluso), che sistema insieme il difetto e il ritmo.
  • Voci più fitte anche alla discesa: probabilità da 0,30 a 0,45.
  • Volumi: voce degli zombie −5 → −2 dB, morso −1 → +1, stridio delle ruote 0 → +2. ⚠️ E il rombo del treno 0 → −2, che è la chiave di tutto il resto: era già scritto nella voce precedente che «se servisse ancora più stridio la strada non è alzare le ruote ma abbassare il rombo, che di headroom ne ha». Così lo stridio guadagna 4 dB di rapporto (quello che si sente) mentre il picco sommato scende a ~+0,3 dBFS invece di salire — con +2 sulle sole ruote sarebbe arrivato a ~+1,4, e nella catena non c'è nessun limiter. Il rombo è anche ciò che copriva il mugugno (144 Hz contro 523, stessa banda): toglierne 2 fa spazio a entrambi.
  • Misurato (tools/autotest/probe_suoni_metro.sh, 26 criteri 0 KO): a discesa finita 11 mugugni in 10 s simulati dove prima erano 0, 27 voci nel tratto della discesa, e il criterio dello stridio guarda ora il rapporto ruote/rombo e non il valore assoluto — un criterio sul solo +2 avrebbe lasciato passare un domani in cui il rombo torna a 0 e le ruote spariscono di nuovo. probe_su664 25 criteri 0 KO, compile-check exit 0.

Tre trappole della sonda, che sono costate più del lavoro e sono tutte annotate sul posto:

  • ⚠️ passeggeri_scesi() non dice quanti sono scesi: ritorna _passeggeri.size(), cioè quanti sono in lista — e tutti e quaranta ci finiscono in un colpo appena la folla nasce, in attesa dietro le porte. Usata come «sono usciti tutti?», faceva uscire il ciclo al primo passo e il criterio contava zero voci su un gancio perfettamente funzionante. Ora si guarda lo stato vero.
  • ⚠️ Contare i lettori audio per indice è sbagliato, e lo nasconde bene: si liberano da soli quando finiscono, quindi se durante la misura ne muore uno la lista si accorcia e un conteggio «dall'indice N in poi» torna zero anche con venti suoni partiti. Sostituito con un registro per instance_id campionato a ogni passo.
  • ⚠️ Il barbone fermo in banchina viene morso in 4,9 secondi, e chiudeva il round prima di ogni misura. Non è un difetto: è la caccia di SU-665 che funziona, e il dato è interessante di suo. Azzerare _zombie_ha_preso non basta — la bandiera la alza BonusRewards.passo() e la legge BonusLevel nello stesso fotogramma — quindi la sonda ora lo tiene lontano. 📌 Il prezzo è che la misura avviene con la fase tornata a «corridoio», ed è stampato nel log invece che nascosto: il mugugno non dipende dalla fase, ma chi legge il numero deve sapere in che condizioni è stato preso.
  • ⚠️ Non verificato all'ascolto in gioco: ritmo e volumi sono ragionati sulle misure. Se il mugugno risultasse troppo fitto la leva è ZOMBIE_MUGUGNO_MIN/MAX_SEC, senza toccare altro.

I volumi della metropolitana, e perché il topo si sentiva e il gemito no2026-09-01

Ivan in gioco: *«il suono della metro e quello degli zombie in gioco è troppo basso, mentre quello del topo si sente bene»*.

  • Alzati tre volumi: ruote sui binari −6 → 0 dB, voce degli zombie −12 → −5 dB, morso −3 → −1 dB (tirato su con la voce, per tenere lo scarto che lo fa leggere come «il morso» e non come l'ennesimo zombie che passa). Il tonfo del topo resta a −9: Ivan dice che si sente bene, e infatti è il riferimento di tutta questa voce.
  • 📌 «Alzali come il topo» sarebbe stato l'errore, e la misura spiega perché: i tre suoni non partivano dalla stessa scala. Il tonfo del topo è un AudioStreamPlayer semplice, che suona a volume pieno; le ruote sono un AudioStreamPlayer2D e pagano anche l'attenuazione della distanza (SFX_TRENO_ATTENUAZIONE 1,4). Confrontare −9 con −6 era confrontare due numeri che non vogliono dire la stessa cosa.
  • ⚠️ E il gemito aveva un secondo handicap, invisibile nel volume: sta nella banda del rombo. Misurato con tools/analizza_sfx.py: la voce degli zombie ha centroide 523 Hz con il 28% dell'energia sotto i 250, mentre il rombo della metro sta a 144 Hz con l'82% sotto i 250. In banchina quel rombo non smette mai, quindi il gemito non era «basso»: era coperto. Il tonfo del topo invece ha centroide 3870 Hz e lo 0% sotto i 250 — non compete con niente, ed è la ragione per cui si sente benissimo con 3 dB in meno. Ecco perché la voce è stata alzata di 7 dB e non di 3.
  • Le ruote invece NON erano coperte, ed è il motivo per cui bastava alzarle: centroide 2343 Hz, l'1% sotto i 250. Stanno in una banda diversa dal rombo — si sommano, non si mangiano.
  • ⚠️ Le ruote si fermano a 0 dB, e il tetto è calcolato, non prudenza generica. Nella catena non c'è nessun limiter — il bus «SFX» lo crea Settings._ensure_sfx_bus() e inoltra dritto a Master — e sfx_volume vale 1.0 di default, quindi il bus non toglie niente. Ruote (picco −3,2 dBFS) e rombo (−2,1) sommati quando i picchi coincidono danno ~+0,4 dBFS a 0 dB, ma ~+1,4 dBFS se le ruote stessero a +2, che era il valore messo al primo tentativo e poi tolto. 📌 Se servisse ancora più stridio, la strada non è alzare questo numero: è abbassare il rombo, che di headroom ne ha.
  • Il criterio della sonda ha due lati, perché il rischio è simmetrico: sotto 0 le ruote tornano a non sentirsi (il KO di partenza), sopra 0 si rischia di distorcere. tools/autotest/probe_suoni_metro.sh24 criteri, 0 KO; compile-check exit 0.
  • ⚠️ Se il gemito ancora non basta, NON va alzato ancora: due suoni nella stessa banda, oltre un certo punto, non si scoprono — si impastano, e il gemito diventerebbe parte del rombo invece che una voce. La strada in quel caso è rigenerarlo più acuto. Scritto accanto alla costante, così chi ci torna non ricomincia da capo.
  • ⚠️ Non verificato all'ascolto in gioco: i tre valori nuovi sono ragionati sulle misure, ma chi decide se adesso si sentono è Ivan alla prossima partita.

Lo zombie col telefono camminava all'indietro, e la metro adesso striscia sui binari2026-09-01

Due cose viste da Ivan giocando: *«lo zombie con la maglietta viola (credo che guardi il telefonino) cammina al contrario!»* e *«aggiungerei solo un suono per quando passa la metro, del suo passaggio sui binari, visto che abbiamo fatto anche le scintille»*.

  • Lo zombie viola è subway_commuter_zombie_headphones, e camminava davvero all'indietro: nella riga walk_e guardava a ovest mentre le altre quattro varianti guardano a est.
  • 📌 La causa non è dove sembrava, ed è la parte che vale. Non è un difetto di codice né del .tres — i cinque .tres sono identici a meno del nome, verificato con diff. È l'arte: risalendo ai raw di Codex in TMP/su668_zombie/, due varianti su cinque erano uscite girate a ovest, headphones e tailleur. Al montaggio degli sheet tailleur è stata specchiata e headphones dimenticata. Un errore di una variante su cinque, in mezzo a quaranta zombie che si muovono: è rimasto lì un giorno intero senza che nessuno lo vedesse.
  • Corretto specchiando lo sheet CELLA PER CELLA, non l'immagine intera: specchiare i 160×160 in un colpo avrebbe invertito anche l'ordine delle colonne, cioè i fotogrammi della camminata (1..5 → 5..1). Il difetto sarebbe passato da «cammina all'indietro» a «cammina all'indietro e muove le gambe al contrario», che è peggio e si nota meno.
  • ⚠️ La misura che ha deciso non è stata la prima che ho provato. Il baricentro della sagoma non distingue niente: le sagome dei pendolari sono quasi simmetriche e davano −0,08 per la variante girata contro +0,31 per una sana, cioè rumore. Quella che funziona è il baricentro dei pixel di pelle, perché la faccia sporge sempre dalla parte in cui si guarda: lì la girata dava −0,88 contro +1,0…+3,3 delle sane. Dopo la correzione: +1,06, in fila con le altre.
  • E adesso c'è un criterio che lo difende, perché il modo in cui il difetto è nato può ripetersi identico al prossimo rimontaggio dell'arte: la sonda misura tutte e cinque le varianti e boccia quelle girate. ⚠️ Verificato che sappia riprodurre il difetto — rimesso lo sheet vecchio, la sonda dà KO e nomina headphones (-1.06); con quello corretto, OK. Un criterio che non fosse stato provato sul difetto vero sarebbe stato solo una riga che dice sempre sì.
  • Lo stridio delle ruote sui binari è un subway_train_rails.wav di 4 s in files/homeless_city/assets/sounds/, su un secondo AudioStreamPlayer2D («Ruote») appeso al treno accanto al «Rombo», al centro del convoglio e non sul muso — se no il suono passerebbe un secondo prima del treno.
  • ⚠️ NON è agganciato alla singola scintilla, e non era un'opzione. Le scintille scoccano ogni SCINTILLA_OGNI, cioè 28 volte al secondo: un suono per scintilla non sarebbe stato uno stridio ma un ronzio. Parte insieme al rombo — e le due soglie coincidono per costruzione, SFX_TRENO_DISTANZA e SCINTILLA_DISTANZA valgono entrambe 900 — quindi nei fatti si accende col primo getto di scintille, che è quello che la richiesta chiedeva.
  • Il tono cambia a ogni treno (0,94–1,06): in un giro ne passano parecchi e col tono fisso il quinto si sente come una registrazione riattaccata. 📌 Il caso è quello globale: sopra _scocca_scintille c'è già scritto per esteso cosa era costato pescare da _rng per un effetto — il corridoio finiva allo 0%.
  • Misurato (tools/autotest/probe_suoni_metro.sh, ora 23 criteri 0 KO): ogni treno nasce con le sue ruote, due treni hanno tono diverso, alla nascita sono zitte (se partissero subito si sentirebbe stridere un treno ancora fuori campo) e si accendono insieme al rombo quando il convoglio arriva — 2 ruote e 2 rombi. Nessuna regressione: probe_su664 25 criteri 0 KO.
  • ⚠️ Non provato all'ascolto in gioco: lo stridio è stato scelto sui numeri, come gli altri quattro. Tre varianti in TMP/su_suoni_metro/wav/ (ruote_A.wav, ruote_B.wav, ruote_C.wav), si sentono con bash TMP/su_suoni_metro/ascolta.sh ruote.

I quattro suoni che mancavano alla metropolitana2026-09-01

Richiesta di Ivan in chat, dopo aver chiesto se lo zombie avesse un verso: *«fai un suono zombie alla comparsa voce dello zombie quando scende dalla metro, e poi per quando mi morde un secondo suono, fai anche un suono per quando la metropolitana mi colpisce e uno per quando i topi mi colpiscono e proviamoli»*. Non ce n'era nessuno dei quattro: il tunnel aveva un suono per ogni cosa che si raccoglie e nessuno per le cose che ti fanno male — treno, topo e morso finivano in silenzio, con la sola scritta a schermo.

  • Quattro file nuovi, generati con ElevenLabs (abbonamento Starter attivo, quindi con licenza commerciale — è il buco segnato in memoria il 2026-08-30, quando gli effetti erano stati generati in piano free): in files/homeless_city/assets/sounds/: subway_zombie_moan.wav (1,20 s), subway_zombie_bite.wav (1,48 s), subway_train_hit.wav (1,48 s), subway_rat_hit.wav (1,00 s) — e le sorgenti in Sounds_raw/.
  • ⚠️ Erano stati normalizzati a picco −1 dB, ed era sbagliato: si rifà a −20 dB RMS. Lo dice il commento già scritto sopra SFX_TRENO (files/homeless_city/scripts/world/BonusLevel.gd:139) — «senza allineamento un effetto nuovo entra sempre più forte degli altri e sembra un errore di missaggio» — ed è verificato sui tre del tunnel già in gioco, tutti a −20,0 esatti. Un allineamento a picco avrebbe fatto entrare i quattro nuovi sopra tutti gli altri: la regola del progetto non era intuibile dai file, stava in un commento.
  • I tre volumi in gioco NON sono uguali, ed è la scelta che tiene in piedi il senso di ognuno. Morso e treno a −3 dB: chiudono il round e si sentono una volta sola. Tonfo del topo a −9 dB: capita fino a tre volte per giro e sopra ci sta sempre il rombo del convoglio — alla voce del morso avrebbe trasformato un intralcio in una catastrofe, il contrario di quello che il topo è («è un intralcio, non una minaccia», sta scritto in _nasce_topo). Voce degli zombie a −12 dB, la più bassa: è ambiente, e se il gemito di uno qualunque avesse la voce del morso, il morso non si distinguerebbe da un altro zombie che passa.
  • ⚠️ Non gemono tutti e quaranta, ed è il cuore della taratura. La folla esce a scaglioni di FOLLA_INTERVALLO (0,16 s): quaranta versi uno ogni 160 ms sono 6,4 secondi di mitragliata: lo stesso muro di rumore che per i topi aveva già costretto a mettere SQUITTIO_GAP_MSEC. Con probabilità 0,30 e freno di 300 ms ne escono una dozzina sparsi sulla discesa — una folla che geme, non un coro. E il tono sgarra sempre (0,85–1,15): senza, dodici gemiti dallo stesso file non sono dodici zombie, sono uno zombie con l'eco.
  • ⚠️ Il dado si tira col generatore GLOBALE, non con _rng, e non è pigrizia. _rng è il seme che rende riproducibili le corse delle sonde: pescarci dentro per un suono avrebbe spostato tutti i tiri successivi — quale sheet tocca a chi, quanti topi, dove nascono. probe_su665_zombie avrebbe cominciato a dare numeri diversi a parità di seme, e il suono sarebbe stato l'ultimo posto in cui cercare la causa. Annotato in tutti e due i punti.
  • Un helper solo invece della quarta copia: _suona_colpo() in files/homeless_city/scripts/world/BonusLevel.gd prende il flusso da una cache a chiave, così i suoni nuovi non aggiungono una funzione a testa. ⚠️ I due _suona_gatto* non sono stati rifatti passandoci: sono codice collaudato e unificarli non era il lavoro chiesto.
  • Misurato (tools/autotest/probe_suoni_metro.sh, 17 criteri 0 KO): i quattro file caricano davvero come AudioStream con durata > 0 (non basta ResourceLoader.exists(): un .import che punta a una cache mai generata esiste e poi carica null); tre inciampi = tre tonfi, e oltre INCIAMPI_MAX nessuno — il tonfo sta dopo il return, quindi non suona per gli inciampi che il tetto ha annullato; i tre toni sono diversi fra loro e dentro la fascia; il dado misurato 0,322 su 1000 tiri contro 0,30 dichiarato; il freno lascia passare 1 chiamata su 200 consecutive; al giro 1 zero gemiti su 40 scesi, al giro degli zombie almeno uno; morso e treno partono col loro volume.
  • ⚠️ Il dado e il freno si misurano SEPARATI, e una sonda che li misurasse insieme darebbe un KO falso. Il freno conta millisecondi veri, la sonda fa scorrere il livello a passi simulati: le quaranta discese che in partita durano 6,4 s lì passano in pochi millisecondi, quindi il freno le taglia quasi tutte e un conteggio «in gioco» direbbe 1-2 gemiti su un codice sano. Perciò il dado si misura con il freno neutralizzato (mille tiri) e il freno da solo (duecento chiamate di fila).
  • ⚠️ La sonda ha dato un KO a se stessa, ed è il genere di errore che si prende per un difetto del codice. La prima stesura chiamava _diventa_zombie() sullo stesso livello appena usato per contare i gemiti, dopo dodici secondi simulati — e in quei dodici secondi il tempo della banchina era scaduto, quindi la funzione usciva subito dalla sua guardia if _finito. Diceva «il morso non suona» con il morso perfettamente funzionante. Ora il morso si prova su un livello fresco, e c'è una controprova esplicita che il livello sia vivo: se un domani il KO torna, si legge subito di chi è la colpa.
  • ⚠️ probe_su665_zombie è ROTTA, e lo era già prima di questo lavoro: 6 KO su 13 controlli (32 zombie scesi su 40 attesi, 0 abbattuti, bottino 0). Verificato con controprova: messe da parte le modifiche di oggi con git stash, la sonda dà gli stessi 6 KO sul codice pulito — anzi 7. Non è una regressione di questa voce, ma va guardata: è probabile che la misura sia rimasta indietro rispetto a SU-699 (l'arte dedicata degli zombie montata il 31/08), cioè lo stesso genere di trappola già registrata — «una sonda che boccia il codice giusto perché difende un requisito superato». probe_su664 invece resta 25 criteri 0 KO.
  • ⚠️ Niente --import sul progetto vero: l'editor di Ivan era aperto in live. Stessa situazione e stessa tecnica di SU-699 — i quattro file di import sono nati in una sandbox isolata con HOME dedicata e sono stati copiati, con i path già definitivi. ✅ Verificato che i quattro uid non collidono con nessuno esistente.
  • ⚠️ NON È STATO GIUDICATO L'ASCOLTO, ed è la parte che manca. Le varianti montate sono state scelte su criteri misurabili (durata, energia media, attacco, assenza di silenzio in testa), non sentendole. Ce ne sono tre per suono in TMP/su_suoni_metro/wav/ e si ascoltano con bash TMP/su_suoni_metro/ascolta.sh: cambiare variante è sovrascrivere il file montato, il codice non si tocca. 📌 Attenzione se si sceglie morso_C o topo_A: hanno il picco a 0,0 dB e vanno attenuati prima di montarli.

SU-704: la cartina a telefono era piccola per aritmetica, non per disegno2026-09-01

Scatto di Ivan, telefono in orizzontale: *«ingrandire la mappa anche per capire un po' meglio, sia il background cartaceo che la mappa stessa»*. Pergamena su poco più di metà schermo, mappa grande come un francobollo, icone impastate.

  • La causa non era la pergamena, era che ogni misura della cartina era scritta in PIXEL FISSI. Con DIMENSIONE TESTO su AUTO il fattore 1.3 di SU-317 accorcia il canvas logico a ~1318×591: margine 70 + rientro 36 + legenda 210 pesano lì il 30% in più che su desktop, e il conto lo pagava tutto la mappa — 275 px di mappa su 521 px di carta, contro i 398 su desktop. A telefono la mappa non era più limitata dalla carta ma dalla legenda.
  • Margine, rientro e icone diventano frazioni con un pavimento di leggibilità, e la fascia della legenda smette di essere un numero: si misura il pannello vero e le si riserva quello (_legend_prepare() prima di ritagliare la mappa, _legend_place_in() dopo). Niente circolarità — la scala delle righe dipende solo dall'ALTEZZA della carta, che a quel punto è già decisa.
  • Misurato col provino, telefono ~1318×591: la mappa passa da 274,6 a 366,9 px logici (+34%), e la pergamena da 676×451 a 815×544 (dal 51% al 62% della larghezza dello schermo). Su desktop 1024×768 non rimpicciolisce: da 397,8 a 433,2 — cresce anche lì, che è quello che Ivan ha chiesto ma vale per una piattaforma che non era nella segnalazione: se la cartina desktop più grande non piace, si alza MARGIN_FRACTION e torna com'era.
  • ⚠️ Il sintomo delle icone sovrapposte NON si riproduceva, e per poco non ci cascavo. Col seme degli scatti le 16 icone, per fortuna sua, non si intersecavano nemmeno col codice rotto: un provino su quel solo seme avrebbe dato «zero coppie sovrapposte» prima e dopo, spacciando per fix una coincidenza. Allargato a 8 città: zero coppie in tutte e otto, anche prima. Il numero che invece racconta il difetto è l'aria fra due icone — 5,2 px su 26 di icona — e l'ingombro dell'icona in px di MONDO: 155. Dopo: aria 17,5 px e ingombro 108, cioè le icone coprono un terzo in meno di città.
    • 📌 Il ticket diceva 197 px di mondo perché contava su un mondo di 2080; il mondo vero è ~1637 (lo dice anche il commento dell'arcobaleno di SU-485: «1632 px in ~440 di carta»). La direzione era giusta, la cifra no.
  • La regola anti-sovrapposizione c'è lo stesso, ed è scritta una volta sola in MapOverlay — vale per la città e per il tutorial, quindi SU-594 non deve riscriverla (e il suo criterio «in città la mappa non cambia» decade, come previsto dal ticket). Non rimpicciolisce le icone per far sparire il problema (sarebbe tornare al difetto di partenza) e non toglie POI: sposta del minimo indispensabile, sull'asse dove la sovrapposizione è minore, e pretende un filo d'aria del 10% del lato. Niente random: quando due POI sono esattamente sovrapposti la direzione viene dagli indici, così la stessa città riaperta dà la stessa cartina.
  • La legenda non tocca più il bordo della carta: il pannello sbordava di 2,7 px sotto (è il difetto dell'ultima voce «Chiesa» nello scatto), ora 0,0. Il clamp su _paper_rect in _legend_place_in() resta come rete anche per le voci future.
  • 📌 Il tetto della fascia legenda è 0,36 e non è un numero a caso: è il valore sotto il quale la mappa resta limitata dall'altezza della carta — cioè grande quanto può — a entrambe le forme, anche se la legenda arrivasse al tetto. Il gioco è in otto lingue e «Fermata bus (riparo)» in tedesco è molto più lungo: senza tetto sarebbe quella riga a decidere quanto è grande la cartina, che è esattamente il difetto di questo ticket. Le altre sette lingue non sono state misurate: il tetto è una garanzia per costruzione, non una prova.
  • ⚠️ La legenda a due colonne non serve, ed è il ramo che il ticket lasciava aperto «da proporre a Ivan»: a telefono la mappa ha 409 px di larghezza disponibile contro 367 di altezza, quindi è tornata a essere limitata dalla carta e non dalla legenda. Non c'è niente da proporre.
  • Provino nuovo riusabile: files/homeless_city/scripts/tools/shot_su704_mappa.gd + tools/autotest/probe_su704.sh. Scatti A/B in TMP/su704_prima/ e TMP/su704_dopo/.

SU-707: una strada sola per le push, e restano trenta righe di Java2026-08-31

Osservazione di Ivan, la sera stessa in cui iOS è entrato in funzione: *«se sono due strade diverse, avendo ora una strada ufficiale per iOS la userei anche su Android evitando poi di dover mantenere due strade diverse»*. Aveva ragione, e la prima risposta che gli avevo dato era peggiore della sua proposta.

  • Escono 323 righe di Java nostro (SUPushPlugin + SUPushService, SU-687) ed entra lo stesso GodotX Firebase che usa iOS. ✅ PushNotifiche.gd si SEMPLIFICA invece di complicarsi: il plugin di GodotX gestisce da sé anche POST_NOTIFICATIONS, quindi sparisce tutto il ramo Android del permesso — niente più OS.request_permission, niente attesa a polling dell'esito, niente _gia_concesso().
  • 📌 Su Android si installa il SOLO firebase_messaging, senza firebase_core, e non è una svista: nel loro Kotlin initialize() su Android si limita a controllare che una FirebaseApp esista già (FirebaseApp.getApps(ctx)), e quella la crea FirebaseInitProvider leggendo le stringhe di resValue che generiamo da google-services.json. Su iOS invece Core va inizializzato davvero. Ricaduta gradita: firebase_core si trascina dentro FirebaseAnalytics e GoogleAppMeasurement — su iOS li abbiamo dovuti spegnere dall'Info.plist, su Android il problema non si pone proprio.
  • ⚠️ Resta una scheggia di Java nostra, SUPushAutoInit, ~30 righe, e il motivo non è tecnico ma legale. GodotX non espone setAutoInitEnabled — verificato nel loro sorgente, dove i metodi pubblici sono initialize/request_permission/get_token/subscribe_to_topic/unsubscribe_from_topic e nient'altro. Senza quella riga il token FCM nasce al primo avvio, e la raccolta andrebbe dichiarata a Play come «Richiesta» invece che facoltativa, con una base giuridica diversa dal consenso nell'informativa (SU-689). È un ponte, non una casa, e c'è un modo di sapere quando buttarlo giù: la stessa funzione (set_auto_init_enabled e is_auto_init_enabled, Android e iOS, più il README) è stata proposta a monte — [godot-x/firebase#45](https://github.com/godot-x/firebase/pull/45). Se la accettano, il file si cancella e non resta più una riga di Java nostra sulle push. La PR dichiara apertamente ciò che non è stato provato — il loro AAR e l'xcframework non sono stati compilati — e ciò che invece lo è: l'API sotto, verificata sull'Honor.
  • Provato sull'Honor 10 (Android 10) col motore 4.7.2, catena intera nel logcat: FirebaseInitProvider initialization successfulGodotPluginRegistry: GodotxFirebaseMessaging e SUPushAutoInitSUPushAutoInit: auto-init accesoFirebaseMessagingPlugin: Subscribed to topic: su_beta. Poi, col gioco chiuso, una push mandata da manda_push.py è arrivata sulla tendina: scatto in TMP/su702_push/notifica_gioco_chiuso.png.
    • ⚠️ Il gioco è stato chiuso con am kill dopo averlo mandato in background con HOME: am kill non tocca un'app in primo piano — il primo tentativo aveva lasciato il pid vivo, e una prova fatta così sarebbe stata un falso positivo. E resta valido che force-stop non si usa: mette il pacchetto in stato «stopped», dove Android non consegna più niente.
    • 📌 È anche la prima prova della 4.7 su un dispositivo vero: nello stesso giro EOS ha ritrovato l'identità del dispositivo e il modulo di login nativo si è agganciato. Il criterio 3 di SU-700 è chiuso per Android.
  • ⚠️ Un errore mio, che vale più della funzione: spostando l'installer da tools/ios/ a tools/ gli ho lasciato il ../.. di quando stava un livello più in basso. ROOT finiva sopra il repo, e siccome più sotto c'è un mkdir -p, lo script non ha protestato: ha creato una finta files/homeless_city accanto al repo, ci ha copiato dentro 123 MB e ha detto «fatto». Un path sbagliato che porta a una cartella inesistente è innocuo solo finché nessuno la crea. Rimedio: la guardia che pretende di vedere project.godot prima di scrivere qualsiasi cosa.
  • 📌 Il ticket si chiama SU-707 e non SU-702: i commenti nel codice erano già stati scritti citando il numero che mi aspettavo, e Jira ne ha assegnato un altro. Corretti tutti e quattro i file prima del commit — un riferimento sbagliato in un commento manda chi legge sul ticket di qualcun altro.

SU-700: il motore passa alla 4.7.2, e SU-701 apre la strada iOS alle push2026-08-31

Il motore serviva per le push su iOS: GodotX Firebase (asset 4475, MIT) — l'unico plugin che porta FCM su iPhone senza linkare a mano l'SDK dentro il progetto Xcode che Godot rigenera a ogni export — dichiara Godot 4.7 o superiore. I due ticket sono quindi uno solo diviso in due.

SU-700 — l'aggiornamento del motore

  • Il rischio numero uno del ticket è caduto al primo colpo: l'estensione EOS carica sotto 4.7 e libeosg NON va ricompilata. Contava perché quella libreria è ricompilata a mano per i 16 KB (SU-280) e rifarla sarebbe stato il pezzo peggiore del giro. La conferma è la riga [UE4] Shutdown handler: initialize in testa al primo import: è l'SDK Epic che si accende. eosg.gdextension dichiara compatibility_minimum = 4.1 e la promessa è stata mantenuta.
  • La 4.7.2 è installata ACCANTO alla 4.6.2, non al posto: /Applications/Godot47.app contro /Applications/Godot.app, e i due set di export template convivono in ~/Library/Application Support/Godot/export_templates/. Tutti gli script di progetto accettano già GODOT= / GODOT_BIN= / GODOT_APP=, quindi la prova non ha richiesto di toccarne nemmeno uno.
  • 📌 Il progetto è stato aperto in una COPIA, mai nel repo di lavoro: cp -Rc (clone APFS) ha fatto 11 GB in 5,8 secondi. Stessa ragione della build di prova di SU-688: un import fatto da una versione nuova riscrive .godot/, e su un repo condiviso è roba che finisce nel commit di qualcun altro.
  • Cosa si è rotto nel gioco: niente. Import pulito (zero righe di errore), 656 file fra .gd e .tscn verdi al compile-check, 8 quartieri su 8 PASS col bot, export macOS e Android riusciti, progetto Xcode generato. La 4.7 non ha riscritto una sola riga di project.godot: git diff vuoto dopo il primo import.
  • ⚠️ L'unico avviso sospetto è un falso allarme, e si è verificato invece di crederci: ogni run stampa «8 ObjectDB instances were leaked at exit». La stessa run sotto 4.6.2 stampa la stessa riga (senza il numero): non è una regressione della 4.7, è rumore di chiusura che c'era già.
  • ⚠️ Le patch al template Android si sono fermate DUE volte, ed è esattamente ciò per cui erano state scritte cosìinstalla_template_android.sh si interrompe da solo dicendo quale ancora non trova, invece di applicare una patch a metà. Le due rotture e il rimedio:
    • buildTools '35.0.1' non c'era più: la 4.7.2 spedisce 36.1.0, cioè un valore migliore del 36.0.0 che noi forzavamo, e la patch si è fermata solo perché non era la stringa attesa. Rimedio: almeno() confronta numeri, non stringhe — la soglia è un minimo, se il template è già pari o avanti non si tocca niente e non si arretra mai. Le tre patch di compileSdk/targetSdk/buildTools ora dicono tutte «già a posto», perché la 4.7.2 spedisce di suo l'API 36 che Play pretende dal 31 agosto.
    • SplashScreen.installSplashScreen(this); è diventato SplashScreen splashScreen = SplashScreen.installSplashScreen(this);: una riga che non ci riguarda, e bastava a bloccare l'innesto di EOSSDK.init(). Rimedio: l'ancora è ora la sola firma di onCreate, cioè «la prima cosa dentro il metodo» — che cambia molto più di rado del suo corpo.
  • L'APK del 4.7 è completo, verificato dentro il pacchetto e non solo dall'exit code: lib/arm64-v8a/libEOSSDK.so (24 MB), le classi SUPushPlugin/SUPushService in classes3.dex, 675 riferimenti a com/google/firebase nei dex, e nel manifest fuso ci sono POST_NOTIFICATIONS, il meta-data del plugin SUPush e firebase_messaging_auto_init_enabled ancora spento (la protezione di SU-687 sul token che nascerebbe prima del consenso).
  • su_native_auth si ricompila contro la 4.7 senza toccare una riga di codice: build_su_native_auth.sh rigenera gdextension_interface.h dalla versione di Godot che gira, e le tre fette (iOS device, simulatore, macOS) escono col simbolo d'ingresso al suo posto.
  • ⚠️ Cosa NON è stato provato, e va detto: il gioco non è stato installato su un telefono vero né su iPad sotto 4.7, e il login EOS non è stato esercitato — è stato verificato che l'estensione *carica*, non che l'accesso Epic funzioni. Il gancio multiplayer del collaudo (MP_CHECK=1) si ferma prima della rete, su «join_game» rifiutato: no_account: è il gate account di SU-447 che rifiuta un bot senza nome registrato. Verificato per controprova: la stessa run sotto 4.6.2 si ferma sulla stessa riga, quindi il gancio MP era già inservibile e non è un difetto del motore nuovo.
  • 🔁 Poi Ivan ha chiesto di togliere la doppia installazione, e /Applications/Godot.app È diventata la 4.7.2 (la 4.6.2 spostata nel cestino, recuperabile). Con lo scambio è cambiato anche il default di installa_template_android.sh, che puntava ancora a 4.6.2.stable: era l'unico posto dove la vecchia versione sarebbe rientrata dalla finestra, reinstallando un template Android della 4.6 sotto un progetto 4.7. Riprovato senza nessun override: il template si rigenera come 4.7.2.stable e il compile-check dei 656 file gira sulla 4.7.2 con FALLITI 0.

SU-701 — le push su iOS

  • Si installano DUE moduli su quattro, e non è per risparmiare spazio: firebase_core (obbligatorio) e firebase_messaging. Analytics e Crashlytics restano fuori perché raccolgono dati, e ogni cosa che esce dal dispositivo è una riga nei moduli store e una base giuridica nell'informativa — per una funzione che nessuno ha chiesto.
  • ⚠️ La trappola del giro, trovata leggendo il .gdip invece che il README: firebase_core si porta dentro FirebaseAnalytics e GoogleAppMeasurement lo stesso. Sono elencati fra i suoi embedded, non è una nostra scelta, e su iOS Firebase li accende da solo al primo avvio, prima che giri una riga di GDScript. Senza rimedio la build manderebbe a Google eventi che l'informativa non dichiara. Si spegne dall'Info.plist con FIREBASE_ANALYTICS_COLLECTION_DEACTIVATED, che disattiva Analytics per l'intera app in modo definitivo e non riaccendibile a runtime — che qui è un pregio, non un limite.
  • 📌 Non è stato messo FirebaseMessagingAutoInitEnabled=false, che su Android è la protezione chiave, e la ragione è che su iOS servirebbe a niente e farebbe danno. Su iOS il token FCM non può nascere prima della registrazione ad APNs, che avviene solo dentro request_permission(): il consenso viene prima per costruzione. Metterlo avrebbe invece rischiato di impedire per sempre la nascita del token, perché GodotX non espone nessun modo per riaccendere l'auto-init a runtime.
  • tools/ios/installa_firebase_ios.sh, sul modello di quello Android: scarica la release 2.5.0 in cache, installa i due moduli, e i 123 MB di xcframework non entrano in git — stessa scelta già presa per l'addon EOS (489 MB) e il template Android (200 MB), che sono dipendenze terze rigenerabili in un comando.
  • ⚠️ Due valori sbagliati che sembravano giusti, entrambi scoperti guardando il progetto Xcode prodotto invece che l'exit code dell'export:
    • entitlements/push_notifications="Enabled" non è un valore valido: Godot ammette Disabled,Production,Development, e con «Enabled» scriveva aps-environment = enabled, che Apple avrebbe rifiutato a valle. Ora è Production, che è l'ambiente APNs di TestFlight, dove stanno i 28 tester iOS. Conseguenza da sapere: una build installata a mano sull'iPad in debug vorrebbe Development, e con questo preset non riceverebbe push.
    • Senza le chiavi plugins/GodotxFirebaseCore=true e plugins/GodotxFirebaseMessaging=true nel preset, i moduli venivano RILEVATI ma non innestati: l'export usciva EXIT=0, con l'entitlement giusto e nessun framework Firebase dentro. È il modo perfetto di sbagliare in silenzio.
  • Verificato nel progetto Xcode prodotto: gli xcframework stanno in StreetUniversity/dylibs/ios/plugins/, il project.pbxproj elenca GodotxFirebaseCore, GodotxFirebaseMessaging, FirebaseMessaging, FirebaseCore accanto a EOSSDK, col flag -ObjC; l'entitlement dice aps-environment = production e l'Info.plist ha UIBackgroundModes = remote-notification. Il .pck resta a 55 MB: i 123 MB di framework non ci sono finiti dentro.
  • PushNotifiche.gd ha imparato iOS mantenendo tutto ciò che conta: gli stessi due topic (su_beta/su_pubblico decisi da Env), lo stesso momento in cui si chiede il permesso, lo stesso pannello in tono col gioco (i cui testi erano già neutri: «IL BARBONE TIENE D'OCCHIO IL CASSONETTO» vale identico su iPhone), e lo stesso fail-open. ⚠️ Quello che cambia è il tempo: su Android si decide tutto dentro _ready(), su iOS niente è sincrono — Core e Messaging si accendono a segnali, e il permesso non si può nemmeno interrogare, perché Apple non ha nulla come OS.get_granted_permissions(). L'unico modo di sapere è richiedere: GodotX non riapre il dialogo se la domanda è già stata posta, quindi all'avvio si chiede solo se _gia_chiesto(), mai a freddo.
  • Fatti in serata, con lo script: l'app iOS è registrata sul progetto Firebase (1:933602919752:ios:742cab05a6966df9dbdf7d), GoogleService-Info.plist è nel repo — versionato, come google-services.json di Android: è configurazione che viaggia dentro ogni copia dell'app, non un segreto — e la capability *Push Notifications* è attiva sull'App ID. Riprovato l'export: [Firebase] Adding iOS config: res://GoogleService-Info.plist, e il file finisce dentro il progetto Xcode.
  • ⚠️ Aggiungere la capability ha reso INVALID tutti e due i profili di provisioning — «Street University Distribution» e «Street University AdHoc». Era annunciato e ora è misurato: vanno rigenerati con lo stesso nome prima del prossimo giro TestFlight, altrimenti testflight.sh non li trova.
  • La chiave APNs c'è, ed è caricata su Firebase per entrambi gli ambienti (KLFK9K6H9H, team RD6E4K56FB). 📌 Dentro la creazione c'era una scelta irreversibile: nella schermata *Configure Key* l'Environment nasce su Sandbox, che da solo NON copre TestFlight (che usa APNs *production*, come dice il nostro entitlement) — e Apple avverte che dopo il salvataggio non si cambia più. Messo su Sandbox & Production, Key Restriction su *Team Scoped (All Topics)*.
    • ⚠️ E una svista che sarebbe costata l'intera feature: nella riga «produzione» della console Firebase l'ID chiave era stato scritto KLFK9K6H9, nove caratteri invece di dieci. Gli ID Apple sono sempre dieci: Firebase accetta l'upload senza fiatare, e le push ai 28 tester iOS non sarebbero mai arrivate, senza un errore da nessuna parte. Vista rileggendo lo scatto della pagina, non da un messaggio d'errore.
  • I due profili rigenerati con --profili e installati dove testflight.sh li cerca davvero: Street University Distribution e Street University AdHoc, entrambi di nuovo ACTIVE e con aps-environment = production dentro.
    • ⚠️ Il trabocchetto che vanificava tutto il resto: rigenerare un profilo NON sostituisce il file vecchio, ne aggiunge uno accanto con lo stesso Name e un UUID diverso. E sia testflight.sh sia Xcode risolvono per nome: con due omonimi potevano pescare quello vecchio, che non ha aps-environment perché è anteriore alla capability — cioè una build firmata senza il diritto alle push, e nessun errore a dirlo. In cartella c'erano sei file e due nomi doppi. Ora lo script sposta i superati in scaduti/ (spostati, non cancellati: un profilo di firma buttato via non si recupera).
  • ⚠️ Cosa resta: la prova vera — export, TestFlight, manda_push.py --topic su_beta, e una notifica sull'iPad col gioco chiuso — e poi i moduli store (App Privacy e §3.4 dell'informativa), che si toccano dopo che iOS raccoglie davvero il token, mai prima.
  • tools/ios/prepara_push_ios.sh fa da solo i punti 1 e 3 di quella lista — registra l'app iOS su Firebase, scarica il plist, attiva la capability — e dei due che restano stampa i passi nell'ordine giusto. Ha --prova, come manda_push.py, e in quella modalità è già stato lanciato contro le API vere: legge il progetto Firebase e le capability dell'App ID senza scrivere niente. ⚠️ Non tocca i profili di provisioning di proposito: aggiungere la capability li invalida e vanno rigenerati con lo stesso nome, ma cancellare un profilo di distribuzione è l'unica azione del giro che, se va storta, blocca una release — quella resta un gesto deliberato di Ivan.
  • 📌 Un avviso cosmetico lasciato in piedi di proposito: su disco la cartella si chiama files/homeless_city/iOS/ (maiuscola, ci vivono icona e splash) mentre Godot cerca res://ios/plugins/, e su macOS il filesystem insensibile alle maiuscole li fa combaciare stampando un avviso di «Case mismatch» a ogni export. Non ha effetto qui — l'export iOS gira solo su Mac — ma su un sistema case-sensitive il plugin non verrebbe trovato. Rinominare la cartella tocca file tracciati e i percorsi dell'icona: è un ticket suo, non un pezzo di questo.

SU-688: il pannello del permesso tornava a ogni partita, e l'ha detto solo giocare2026-08-31

  • ⚠️ Difetto vero, trovato e corretto: la guardia «si chiede una volta sola» era codice morto. In Settings push_permesso_chiesto è una variabile, e PushNotifiche la interrogava con has_method(), che su una variabile torna sempre false. Risultato: la condizione non è mai stata vera e il pannello ricompariva a ogni fine partita — esattamente ciò che il ticket vieta. I *setter* invece sono funzioni vere, quindi la scrittura funzionava: dall'esterno sembrava tutto a posto. Lo stesso errore stava in altri due punti — il topic precedente non veniva mai riletto (al cambio d'ambiente si sarebbe restati iscritti a due topic, ricevendo ogni annuncio in doppio) e stato_permesso() avrebbe risposto «mai» per sempre, cioè la telemetria appena aggiunta sarebbe stata muta e ce ne saremmo accorti fra un mese guardando dati tutti uguali. Rimedio: le proprietà si leggono con "nome" in nodo + get().
  • 📌 Nessuno degli strumenti poteva vederlo, ed è la parte che vale la pena ricordare. Il compile-check no: has_method() si risolve a runtime e la riga è sintatticamente perfetta. Un provino che si costruisce da sé il pannello nemmeno, perché salta proprio la guardia. L'ha visto solo giocare due partite di fila su un Android 13+ — cioè la prova che stava per essere delegata a un tester perché «in casa non si può fare».
  • E in casa si poteva fare. L'emulatore non era rotto: era Godot che gli parlava in Vulkan e lo swapchain dell'emulatore rifiutava ogni frame (QueuePresentKHR failed with error: 5, schermo nero allo scatto, invariato con due modalità GPU). Con una build usa-e-getta col renderer di compatibilità (renderer/rendering_method.mobile="gl_compatibility") il gioco si vede e si gioca. ⚠️ La chiave che conta è .mobile: cambiare solo rendering_method non sposta niente su Android, dove Godot legge l'override di piattaforma — il primo tentativo è uscito ancora in Vulkan.
  • 📌 La build di prova è nata da una COPIA del progetto, non dal repo: cp -Rc (clone APFS) fa 2,3 GB in 2,8 secondi e non occupa spazio finché non si scrive. Non è pignoleria: sullo stesso repo stava lavorando un'altra sessione, e lasciare il renderer cambiato anche per pochi minuti significava rischiare che finisse in un commit altrui. ⚠️ Serve il HOME vero: common.sh lo dirotta su una cartella temporanea, e da lì Godot non vede i percorsi di JDK e SDK Android configurati nell'editor — l'export muore con «Un percorso valido per lo Java SDK è richiesto».
  • La telemetria adesso risponde alla domanda da sola (SU-688): RunRecorder._device_payload() porta push_perm"n/a" | "mai" | "chiesto" | "ok". Se un dispositivo alla run 1 dice «mai» e alla run 2 dice «chiesto», il pannello è comparso fra le due: il criterio, misurato su tutti gli Android 13+ della beta invece che sulla memoria di uno. ⚠️ Sta nel blocco del dispositivo e non fra gli eventi perché il pannello compare sulla schermata di fine partita, cioè dopo che _end_run() ha messo _run_open = false: un _write() lì viene scartato in silenzio, senza un errore che lo dica.
  • Verificato per contrasto, non solo dopo il rimedio: la stessa build prima del fix mostra due righe [Push] permesso rifiutato dal pannello in due partite consecutive (19:47:15 e 19:50:42); dopo il fix, con i dati dell'app azzerati, la seconda partita non ne produce nessuna.
  • Ricaduta gradita: dopo aver risposto al pannello, la schermata di fine partita resta usabile — RESTART e QUIT rispondono al tocco. È la trappola del KO di SU-418 (i menu che restavano sordi dopo un pannello), e il congeda() copiato da lì la evita.

Epic SU-686: le notifiche push, dal progetto Firebase alla notifica sull'Honor2026-08-31

Lavorata l'epic N1 — Notifiche push (FCM): SU-687 (prova su Android), SU-688 (il permesso), SU-689 (moduli store e privacy), SU-690 (manda_push.py e l'innesto in /release).

  • Il criterio di SU-687 è soddisfatto e si vede: una notifica mandata dal Mac è arrivata sull'Honor 10 col gioco chiuso — «Il barbone ha trovato roba nuova nel cassonetto». Scatti in TMP/su687_push/notifica_gioco_chiuso.png (tendina) e notifica_espansa.png (con anche la riga inglese). ⚠️ Il gioco è stato chiuso con am kill, non con am force-stop, e la differenza è tutta: force-stop mette il pacchetto nello stato «stopped», dove Android per progetto non gli consegna più niente finché non lo riapri a mano — una prova fatta così sarebbe stata un falso negativo perfetto, con la push che non arriva per colpa del modo in cui l'hai chiusa.
  • Il progetto Firebase è agganciato al progetto Google Cloud esistente, street-university-pubblisher / axiomatic-path-505007-u7, numero 933602919752. ⚠️ Il nome ovvio era la trappola: nell'elenco c'è anche un progetto chiamato «Street University» (gen-lang-client-0874937409), nato da AI Studio e vuoto, senza un solo client OAuth. Verificato aprendo /auth/clients su entrambi PRIMA di scegliere — e aggiungere Firebase a un progetto non si annulla, quindi lo sbaglio sarebbe stato definitivo.
  • ⚠️ Il piano non è Spark ma Blaze, e l'epic diceva Spark. Siccome quel progetto GCP ha già la fatturazione attiva, Firebase ci entra in pagamento a consumo. Per FCM non cambia niente — è gratis su entrambi i piani — ma la premessa scritta nel design era inesatta, e va saputa prima di accendere altri servizi Firebase, che gratis non sono. Google Analytics è stato spento in creazione: era acceso di default, non serve alle push, e avrebbe aggiunto una raccolta dati nuova da dichiarare agli store — l'opposto di quello che fa questa epic.
  • 📌 La scoperta che ha cambiato il codice: di serie il token FCM nasce al primo avvio, da solo, prima che il gioco abbia chiesto niente a nessuno. Non si vede provando l'app — è saltata fuori compilando il modulo «Sicurezza dei dati» e chiedendosi cosa si stesse dichiarando. Con quel comportamento la risposta onesta sarebbe stata «È richiesta la raccolta», e l'informativa privacy non avrebbe potuto dire che la base giuridica è il consenso. Rimedio: firebase_messaging_auto_init_enabled=false nel manifest e setAutoInitEnabled(true) solo dentro push_subscribe, che parte solo a notifiche permesse. Verificato sull'emulatore Android 16: all'avvio, senza permesso, il log dice [Push] notifiche non permesse: nessuna iscrizione, nessun token — mentre sull'Honor (Android 10, dove il permesso non esiste) si iscrive e il token nasce. ⚠️ Se qualcuno rimette l'auto-init a true, la casella del modulo Play va cambiata in «Richiesta» nello stesso commit.
  • Il topic lo decide Env, non un interruttore nuovo: dev/stagesu_beta, livesu_pubblico. Così il giorno del lancio pubblico non si tocca il codice del client. Scartato il beta-gate come criterio: BetaGate gira solo nelle build desktop, cioè proprio dove le push non arrivano — sarebbe stato un criterio che sul telefono non risponde mai.
  • 📌 Innesto Gradle senza il plugin google-services, che è la strada ufficiale. Quel plugin fa una cosa sola: legge google-services.json e ne genera sei stringhe di risorsa. Averlo costava due ancore in più nel template (una in settings.gradle, dentro pluginManagement) e una versione compatibile con l'AGP — due punti che si rompono al prossimo aggiornamento di Godot, per generare sei stringhe. Le generiamo con resValue, che è lo stesso gesto già usato per eos_login_protocol_scheme. Il log del telefono dice FirebaseInitProvider: FirebaseApp initialization successful: funziona.
  • ⚠️ Tutto l'innesto FCM è condizionato a google-services.json: se il file non c'è, installa_template_android.sh lo salta e la build esce identica a prima, senza push e senza errori. Provato in tutti e due i versi prima di scaricare il file.
  • tools/release/manda_push.py (SU-690): JWT firmato con openssl (non con la wheel cryptography di play_upload.sh: su questo Mac le wheel hanno già fatto danni quando un timeout di Homebrew ha trascinato python3 sotto Rosetta), scambio OAuth2 e POST a FCM v1, tutte le chiamate con curl perché python3 qui non ha i certificati CA. La chiave privata passa da un file descriptor effimero, mai da un file su disco. Provata la firma anche contro la chiave pubblica: Verified OK.
    • Il topic è obbligatorio e non ha default: lanciato senza, si ferma e non manda niente (provato). Un topic inventato viene rifiutato (provato). Un testo che dice «aggiorna» viene rifiutato con la spiegazione — su Android Play ha già aggiornato da sé, e il messaggio giusto dice cosa c'è di nuovo (provato, e si zittisce con --anche-se-dice-aggiorna).
    • --prova stampa il messaggio senza mandarlo e funziona anche senza la chiave: chiedere una credenziale per NON mandare niente renderebbe scomodo proprio il giro che deve essere facile fare, cioè far rileggere il testo prima di spedirlo.
  • SU-689, la parte store e privacy. Riga nuova nel modulo Play: ID dispositivo o altri ID, raccolto, condiviso con Google, Facoltativo, scopo «Comunicazioni dello sviluppatore». CSV pronto da importare in STORE_ASSETS/google_play/data_safety_2026-08-31.csv (Google ha import/export CSV: non si ricompila a mano). La casella era vuota fino a oggi e la nota che diceva «resta VUOTA» è stata corretta invece che lasciata a contraddire la tabella.
    • 🔎 Il «Condiviso: Sì» è la risposta prudente, non l'unica difendibile: Google tratta il token da *service provider*, e con quella lettura varrebbe la stessa logica già applicata al PUID di SU-444 (raccolto sì, condiviso no). Il ticket chiedeva «condivisione con Google» e fra le due si è scelta quella che dichiara di più — su Play si viene puniti per aver dichiarato meno del vero.
    • ⚠️ Su Apple NON si dichiara niente, ed è la risposta giusta oggi. Il ticket chiedeva «la stessa dichiarazione anche in App Privacy», ma la versione iOS non genera nessun token: le push iOS sono il ticket 5 dell'epic ed è esplicitamente rinviato. Dichiarare un identificativo che iOS non raccoglie sarebbe una dichiarazione falsa in eccesso. Stessa ragione per la capability *Push Notifications* sul profilo: si aggiunge quando si scrive il codice APNs, non prima — e rigenerare adesso il profilo di firma per una funzione che non esiste significherebbe toccare la catena iOS per niente.
  • ⚠️ L'informativa privacy esisteva in DUE copie identiche byte per byte, e i documenti di progetto ne nominavano una sola. Quella che gli store dichiarano è l'altra: street-university-site/privacy/index.html, servita su streetuniversitygame.com/privacy/. Aggiornarne una sola avrebbe lasciato l'URL depositato nel modulo Play a puntare a un'informativa senza le push. Aggiornate e pubblicate entrambe (versione 1.3, §3.4 in italiano e inglese) e verificate sulle pagine online, non solo in locale.
  • SU-688, il permesso: si chiede da un solo puntoHUD._setup_gameover_menu(), l'unico in cui il giocatore ha appena visto il proprio punteggio ed è fermo — con una riga sola nell'HUD e tutte le guardie dall'altra parte (prima run, una volta sola, non sotto Android 13, non col bot, non in demo, non in headless). Prima del dialogo di sistema c'è un pannello nostro: quello di Android non si può riscrivere, dice «Consentire a Street University di inviare notifiche?» in tono da sistema operativo, e soprattutto si può mostrare una volta sola per sempre — bruciarlo su chi non ha capito cosa gli si chiede è definitivo.
    • 📌 La domanda si segna come «posta» PRIMA di mostrarla, non dopo la risposta. Se il gioco viene chiuso o ucciso mentre il pannello è a schermo, la domanda risulta comunque fatta: altrimenti basterebbe chiudere l'app per rivedersela a ogni partita, che è proprio il comportamento che il ticket vieta.
    • Non si aspetta l'esito per decidere cosa fa il gioco (fail-open: fa la stessa identica cosa nei due casi), ma lo si guarda per sapere se iscriversi — perché è lì che nasce il token. L'attesa è a interrogazione e non a segnale: Godot consegna l'esito del permesso dal MainLoop con un nome cambiato fra le versioni, e legarcisi vorrebbe dire un file che smette di compilare al prossimo aggiornamento del motore.
    • Testo e impaginazione da approvare: 7 scatti in TMP/su688_permesso/ (it, en, de, ru, zh a 1024×768 più de/it a 1200×540). Il tedesco — la lingua più lunga — sta dentro il cartone anche alla misura stretta, e il cinese rende senza caratteri mancanti.
    • ⚠️ Quello che NON è provato, ed è la metà che conta: che il pannello compaia dopo la prima run su un telefono vero. L'Honor 10 è Android 10 (API 29), sotto la soglia dei 13: lì il permesso non esiste e il pannello — correttamente — non compare mai. Sull'emulatore Android 16 il gioco parte e i log sono giusti, ma QueuePresentKHR fallisce a ogni frame (swapchain Vulkan dell'emulatore, non del gioco): schermo nero allo scatto, provato con due modalità GPU diverse. Quindi è verificato che il pannello non compare prima (35 s di avvio, splash e menu senza una riga di dialogo) e che il permesso non nasce da sé; resta da provare che compaia dopo, e serve un Android 13+ vero. Il provino shot_su688_permesso.gd non copre quel pezzo, e lo dice nella propria testata: si costruisce da sé il pannello, quindi esclude per definizione il ramo in cui il pannello non arriva.

SU-699: i cinque pendolari zombie sono in gioco, e la tinta verde si spegne2026-08-31

  • Ivan aveva scelto tutte e cinque le varianti («li scelgo tutti, vanno messi tutti per variare»). Montate: valigetta, tailleur, cuffie, caffè, giornale, in assets/sprites/npc/bodies/ coi loro .tres.
  • 📌 Il codice era già predisposto, ma per UNA risorsa sola. _zombie_dedicato() faceva ResourceLoader.exists() su un singolo path e il commento prometteva che «la tinta va via da sola quando arriva l'arte, chi consegna lo sprite nuovo non deve ricordarsi di togliere niente». Con cinque varianti quel meccanismo andava esteso, non usato: ora c'è l'archetipo bonus_pendolare_zombie nel manifest, _corpi_zombie() pesca dai cinque, e il ripiego tinto resta come rete di sicurezza se l'arte manca.
  • ⚠️ Il nuovo archetipo è stato ESCLUSO dal sacchetto dei passeggeri normali, ed è la riga che salva il ticket. _elenco_corpi() appiattisce *tutto* il manifest: senza l'esclusione, al giro 1 dalla metro sarebbero scesi degli zombie — cioè il criterio «ai giri 1 e 2 nessuno zombie» rotto nel modo più vistoso, e per costruzione, non per caso.
  • arch_office_zombie rientra fra i passeggeri normali. Era escluso solo perché faceva da ripiego zombie; con l'arte dedicata torna a essere quello che è sempre stato — l'«Impiegato esaurito» di NPCDatabase, camicia azzurra e pelle normale. ⚠️ È la trappola che era già costata un giro intero: il nome inganna, e per settimane è stato scambiato per uno zombie.
  • 📌 Un'esclusione in più, non chiesta dal ticket ma necessaria: NPCDatabase._build_vendor_look_pool() appiattisce anch'esso tutte le varianti scoperte. Senza escluderlo, uno zombie sarebbe potuto comparire dietro il banco di un venditore in città. Trovata dal builder ragionando sulle conseguenze dell'archetipo nuovo, non dal testo del ticket.
  • ⚠️ Nei .tres sono definite anche le animazioni che l'arte non haidle_e/se/ne/n, più idle e walk senza suffisso — aliasate ai fotogrammi di idle_s/walk_s. Costano zero e tolgono per intero una classe di bug: in Godot un'animazione che manca non è un errore rumoroso, il nodo semplicemente non la trova, e il difetto salta fuori in gioco molto dopo, in un caso raro.
  • ⚠️⚠️ DISINNESCATA UNA BOMBA A OROLOGERIA, ed è la cosa più importante di questa voce. npc_variants_manifest.gd è auto-generato da files/homeless_city/tools/import_people_sprites.py, che riscrive il file da zero scandendo bodies/ — e riconosce solo nomi arch_*. I nostri subway_commuter_zombie_*_frames.tres finivano nel ramo WARN: nome non riconosciuto, quindi al primo rilancio dello script la voce degli zombie sarebbe sparita in silenzio e la banchina sarebbe tornata ai pendolari tinti di verde, senza un errore da nessuna parte. Lo script ora li riconosce e li rigenera nell'archetipo giusto — verificato che la voce rigenerata è equivalente a quella scritta a mano (stessi cinque path, stessa corsia), quindi un rilancio è un no-op e non un cambio di comportamento nascosto.
  • ⚠️ L'import non è stato eseguito sul repo vero, ed è una scelta: l'editor di Ivan era aperto in live sul progetto (un --import headless concorrente rischiava il dialog di reload e la cache condivisa). I cinque .png.import sono stati generati in una sandbox isolata e copiati — stessa tecnica già in uso per i .uid, che sono deterministici dal path. ✅ Verificato: parametri identici al fratello arch_office_zombie_male_v01_sheet.png.import, cinque uid tutti diversi e nessuno in collisione con quelli esistenti. La cache compilata si rigenera al primo avvio.
  • Provato guardando: TMP/su699/su699_banchina.png — sulla banchina del giro 3 si contano quattro o più aspetti distinti nello stesso giro (felpa viola, camicia azzurra, gilet e cravatta, abito blu con valigetta), e i vestiti hanno i loro colori, che è la prova che la tinta è spenta: con il ripiego sarebbe stata verde anche la stoffa. probe_su664.sh: 25 criteri, 0 KO, nessuna regressione. Incasso invariato: 40 × 5 = 200.

I topi prendono la luce della lanterna, invece di stare sopra al buio2026-08-31

  • Ivan, provando il giro 3: «i topi vanno illuminati come il terreno dalla luce del barbone, ora passano sopra e staccano troppo».
  • ⚠️ La causa non era il disegno del topo: era che il topo non era tinto affatto. Il buio di questo livello è fatto senza luci e senza shader — è una scelta scritta nell'etichetta «device» in fondo a BonusLevel.gd, perché le luci 2D sono la voce che costa di più sul telefono e l'Honor 10 è già al limite di fill rate. Quindi: le texture del tunnel sono scurite con modulate e l'alone del barbone è uno sprite additivo che le rischiara. Il pavimento su cui corre il topo è TEX_TRACK tinto TINTA_BINARI, cioè sta al 46-54% — e il topo veniva disegnato a piena luminosità. Su un fondo scurito di metà, un topo pieno è un adesivo, ed è esattamente la parola che ha usato Ivan.
  • Il rimedio applica al topo le stesse due cose che subisce il terreno: la tinta di buio del binario (TOPO_TINTA_BUIO := TINTA_BINARI, così un topo lontano vale esattamente quanto il binario su cui cammina) più il contributo additivo dell'alone, calcolato con la sua stessa curva _alone_alfa() e il suo stesso raggio. Al centro della pozza il topo torna pieno, fuori sparisce nel buio come tutto il resto. Nessuna luce 2D, nessuno shader: costo zero, coerente con la regola del file.
  • 📌 Il colore dell'alone è diventato una costante (ALONE_COLORE) invece di un letterale ripetuto: ora lo leggono sia l'alone sia l'illuminazione dei topi, e due copie sarebbero divergute al primo ritocco lasciando i topi illuminati da una luce che non esiste più.
  • ⚠️ Il centro della luce non sono i piedi del barbone: _aggiorna_luce() mette l'alone a _runner.position + (0, -8). Usare la posizione del barbone avrebbe acceso il topo 8 px fuori dalla pozza vera — poco, ma abbastanza da vedersi su uno sprite alto 14.
  • ⚠️ L'alfa non si tocca: _spazza_topi() la usa per far svanire il topo preso dal treno, e riscriverla avrebbe tolto quella dissolvenza senza lasciare traccia di dove fosse finita. Si scrivono solo r, g, b.
  • Verificato guardando, e non solo eseguendo: il primo confronto sospettava che la modifica non fosse arrivata nello scatto, perché i PNG pesavano quasi uguale. La differenza pixel per pixel dice 2.467 pixel cambiati, tutti nella fascia x 105-437 · y 453-497, cioè esattamente dove stanno i topi: il peso quasi identico era solo perché i topi occupano poco schermo. Confronto sopra/sotto in TMP/su664_luce/confronto_luce_topi.png.
  • 📌 Nota per il seguito: Ivan ha detto che valuterà se rifare gli sprite dei topi. Se succede, questa illuminazione non va rifatta — lavora sul modulate, quindi vale per qualunque texture ci sia sotto.

SU-668: i cinque pendolari zombie camminano in cinque direzioni2026-08-31

  • Ivan ha scelto: «li scelgo tutti! vanno messi tutti per variare!». Quindi tutte e cinque le varianti — valigetta, tailleur, cuffie, caffè, giornale — e non una sola.
  • ⚠️ Ma le pose approvate erano una sola per variante, e sulla banchina gli zombie camminano. Prima di generare, la misura: BonusRewards._anima_passeggero() chiede walk_e/se/s/ne/n (le direzioni ovest le specchia con flip_h, non si disegnano) e — scoperta utile — i due soli punti che la chiamano in stato fermo passano sempre Vector2.ZERO, quindi idle_e/se/ne/n sono irraggiungibili dal codice attuale: serve solo idle_s. Totale 21 fotogrammi per variante, non i 75 che il foglio di riferimento lascerebbe temere.
  • 📌 Fatto prima un pilota su una sola variante, non tutte e cinque insieme: un problema di stile sul ciclo di cammino, moltiplicato per cinque, sarebbe costato cinque volte tanto. Il pilota ha retto e solo allora sono partite le altre quattro.
  • ⚠️ La trappola delle diagonali è stata evitata con un riferimento, non con le parole: Codex fa uscire le pose SW/NE frontali se non le descrivi, ed è documentato da SU-201. La soluzione che ha funzionato è stata ritagliare le righe di movimento dal foglio già in gioco (arch_office_zombie_male_v01_sheet.png) e allegarle come terzo riferimento, oltre a identità e proporzioni. Le diagonali sono uscite girate al primo colpo.
  • ⚠️ Due incidenti trovati e risolti dal builder, che vale la pena registrare. Primo: un ritaglio quadrato preso dalla striscia sorgente sconfinava nei personaggi affiancati, producendo fotogrammi con doppioni fantasma — risolto costruendo un canvas quadrato sintetico per personaggio, senza mai leggere i pixel dei vicini. Secondo: il primo lancio dei 20 giri Codex è morto perché usava un array associativo bash (declare -A), che la bash 3.2 di macOS non supporta — nessun output, individuato dal silenzio, processi uccisi e rilanciato a blocchi separati.
  • Verificato il dubbio della valigetta invece di ipotizzarlo: nel ciclo di walk_s sembrava passare di mano. Un overlay a linea fissa sui quattro fotogrammi dice che resta sempre sulla stessa mano — è l'oscillazione del braccio. Per le altre quattro il vincolo «il prop resta dalla stessa parte» è finito esplicitamente nel prompt.
  • Costo misurato: 5 giri Codex per il pilota, 20 per le altre quattro, 876K token in ~7,5 minuti di orologio in parallelo.
  • Da guardare, ed è il file che decide: TMP/su668_zombie/sheets/su668_zombie_contatto_5varianti_4x.png — cinque righe per cinque direzioni, a 4× NEAREST.
  • ⚠️ Niente è montato in gioco, ed è voluto: .tres, manifest e spegnimento della tinta verde sono lavoro di codice e nascono in un ticket a parte. Finché non è fatto, gli zombie restano il pendolare con la tinta addosso.
  • ⚠️ Nota onesta sulla variante «giornale»: il rotolo color crema si confonde con la manica bianca a 32 px. Non è un difetto introdotto qui — è già così nella posa che Ivan ha approvato, verificato prima di segnalarlo.

SU-664 terzo giro: topi più piccoli, più numerosi, e il giro 3 non è più vuoto2026-08-31

  • Ivan, dopo averlo provato col dito: «giro 2 ok, ma metterei topi più piccoli di dimensione e di più, giro 3 mancano i topi».
  • Più piccoli: TOPO_SCALA = 0.75 a disegno — l'arte del topo è quella approvata il 30/08 e non è stata rigenerata. ⚠️ Col topo è stato rimpicciolito anche il raggio di contatto (TOPO_RAGGIO da 26,0 a 19,5, cioè scalato con lui): lasciarlo largo avrebbe fatto inciampare su un topo che non si era toccato, ed è il difetto peggiore che questo cambio poteva introdurre.
  • Più numerosi e il giro 3 acceso: TOPI_PER_GIRO da [0, 12, 0, 20, 28, 32] a [0, 18, 20, 26, 34, 40]. Il giro 3 passa da 0 a 20, in mezzo fra il giro 2 e il giro 4.
  • ⚠️ Cade una regola che avevamo deciso in chat il 29/08 («al giro 3 i topi sono in pausa, così si legge lo zombie da solo», *proviamo come dici tu*). Provata col dito, non regge: al giro 3 il corridoio sembrava vuoto. Il giro 1 invece resta a zero, e quello non si tocca: è il giro che insegna il livello.
  • Il tetto è misurato, non scelto: con [0, 18, 20, 26, 34, 40] il margine di schivata resta 7,2 s sul pavimento di 6,0. Spingendo a [0, 24, 26, 34, 42, 48] il margine crolla a 5,4 s, cioè sotto il pavimento. 📌 Quindi «di più» ha un limite duro: da qui in avanti la leva non è il numero dei topi, ma quanto costa ciascuno (durata dell'inciampo, o il tetto di 3).
  • ⚠️ Trovato un commento FALSO nel codice e corretto: diceva «alzare i topi non mangia il margine, per costruzione». Misurando, lo mangia eccome. In questo file un commento falso ha già fatto sbagliare un giro intero di design, ed è la seconda volta che ne salta fuori uno.
  • probe_su664.sh: 25 criteri, 0 KO — col criterio «giro 3 a zero» riscritto in «giro 3 fra il giro 2 e il giro 4», perché quello vecchio ora chiedeva la cosa sbagliata.
  • Scatti guardati in TMP/su664_giro3/: su664_topi.png (corridoio del giro 6) e su664_giro3.png (il giro 3, non più vuoto).

La TMP torna a essere una sola, e l'ombra del piccione si vede2026-08-31

  • Richiamo di Ivan: «stai ricominciando a scrivere nella cartella TMP dentro files, mentre avevamo detto di scrivere solo nella cartella TMP radice, sposta tutto lì». Aveva ragione, e non era una preferenza nuova: la regola è di SU-458 ed era ancora scritta in probe_su313_restart.gd — «TMP/ vive alla radice del repo, non dentro files/homeless_city/ — res://../../TMP e non res://TMP, altrimenti nasce una seconda TMP/». /TMP/ è pure in .gitignore con quel commento.
  • ⚠️ La deriva non era solo di questa sessione, ed è il pezzo che vale: cinque script del repo puntavano ancora dentro il progetto Godot — shot_su685.sh (con un commento che affermava il contrario della regola: «la TMP DENTRO il progetto Godot… non quella di radice»), shot_su681_lontano.sh, probe_su418_opzioni.sh, shot_su685_mia.gd (res://TMP/su685) e shot_endless_baracca.gd. Corretti tutti: quando una regola sopravvive solo nei commenti di un file, si sfalda.
  • files/homeless_city/TMP/ non esiste più. Le 15 cartelle di scatti e provini sono state spostate nella TMP/ di radice, nomi invariati.
  • 📌 Non era solo ordine, ed è la ragione tecnica per preferirla: un PNG scritto dentro res:// si porta dietro un file .import generato da Godot. Ripulendo ne sono stati cancellati 617. Fuori da res:// non nascono, e infatti la TMP/ di radice è gitignored mentre l'altra no — per questo quelle cartelle comparivano come «untracked» a ogni git status.
  • ⚠️ L'unica cosa che non può stare in TMP/ è uno script .gd: Godot carica gli script solo da res://. Le quattro sonde che vivevano nella TMP del progetto (probe_su666_giro3, probe_su669_branco, probe_su674_anim, probe_su683_684) sono andate nella loro casa, scripts/tools/, coi due runner che le richiamavano aggiornati e gli .uid rigenerati al percorso nuovo.
  • La regola è stata scritta dove serve: in CLAUDE.md (che diceva ancora «nel repo ci sono due cartelle TMP») e soprattutto in WORKFLOW/CAPSULA.md, cioè nel blocco che finisce in testa al brief di ogni agente — è l'unico posto da cui può arrivargli, visto che i doc di progetto non li può leggere.
  • SU-682, KO di Ivan: «sui piccioni normali l'ombra si vede poco, e staccano male dal terreno». L'ombra passa da 7×3 con alfa 0,30 a 11×5 con alfa 0,45, e la sfumatura verso il bordo si smorza meno (0,35 invece di 0,55), che è ciò che le dà un nucleo invece di farla svanire prima di finire.
  • ⚠️ Perché il giro precedente non se n'era accorto, ed è la lezione: il criterio era «l'ombra si vede nei tre stati e non ondeggia col corpo», ed era stato verificato misurando che non ondeggia (ombra ferma a y 199,000 mentre il corpo fa la parabolina di 3 px). La metà che contava — che si veda — non era mai stata provata. Una misura risponde alla domanda che le fai tu, non a quella del ticket.
  • 📌 Anche il ragionamento sulla taglia era sbagliato: 7×3 usciva dallo scalare i 22×8 del cactus in proporzione al corpo. Ma sotto un piccione di 9×7 sono ~15 px scuriti di un terzo, cioè niente. Il rapporto vero del cactus è ombra larga quanto il corpo, ed è così che si legge come luce mancante sotto una cosa appoggiata.
  • Il provino ora scatta su TRE fondi — erba, marciapiede chiaro, asfalto scuro — invece del solo verde: il KO diceva «staccano male dal terreno», e il terreno vero non è il verde della panchina di prova. ⚠️ Al primo giro i tre scatti uscivano tutti col fondo del giro prima: due frame d'attesa non bastano appena aperta la finestra, ora sono cinque. Uno scatto col fondo sbagliato sembra uno scatto giusto.

SU-668: cinque pendolari zombie disegnati, e perché al primo giro erano fango2026-08-31

  • Richiesta di Ivan: «prova a generare l'arte dedicata, niente variante drunk per gli zombie, generane 4 o 5 di varianti, poi li mettiamo a confronto con la versione solo tinta». Il ratto e il suo squittio erano già chiusi il 30/08: mancava solo lo zombie.
  • Cinque pendolari, tutti con la pelle verde e un mestiere addosso: valigetta, tailleur, cuffie, tazza da asporto, giornale. Più la ricostruzione fedele del ripiego attuale — il pendolare normale con la tinta verde — per il confronto che Ivan ha chiesto.
  • ⚠️ Il primo giro è stato buttato, e il motivo vale più delle immagini: le cinque varianti erano *peggiori del ripiego che dovevano sostituire*. Marroni e grigi desaturati che si confondevano, nessun contorno, facce illeggibili a 32 px: sembravano fotografie rimpicciolite. Il ripiego a tinta verde — palette corta, ombre piatte, contorno continuo — le batteva tutte.
  • 📌 La causa non era la riduzione, era il sorgente. Il riferimento era stato usato per le *proporzioni* ma non per lo *stile*, e Codex senza vincolo di stile produce un rendering morbido. La regola di casa dice che la pixel art grossa la fa la riduzione — ma vale solo se il disegno grande è già a palette corta e tinte piatte: una riduzione non può inventare un contorno che nel sorgente non c'è. Il prompt del secondo giro chiede esplicitamente palette di 10-12 colori, ombre piatte a 2-3 toni, contorno scuro continuo, alto contrasto e prop grandi a tinta piatta.
  • 📌 Il metro con cui si è giudicato, e conviene riusarlo: *se una variante non batte il ripiego, non serve a niente*. Il ripiego funziona già; l'arte dedicata esiste solo se è meglio.
  • ⚠️ Due difetti trovati sui pixel, non ragionando. Primo: quantizzare la palette *dopo* la riduzione faceva inghiottire un colore minoritario da un cluster sbagliato — la variante col felpone arancio è uscita monocolore, con la pelle verde mangiata. Ora la palette si quantizza sull'immagine grande, dove ogni area piatta ha ancora migliaia di pixel, e il 32×32 ci si aggancia. Secondo: i vestiti «caldi» (mostarda, arancio) andavano in conflitto di tonalità con la pelle verde; rigenerate con bordeaux e viola, il problema è sparito.
  • Foglio di confronto in due file, perché a 32 px veri non si giudica niente a occhio nudo: files/homeless_city/TMP/su668_zombie/su668_zombie_comparison_sheet_true32.png (dimensione vera) e su668_zombie_comparison_sheet_4x.png (lo stesso a 4× NEAREST).
  • ⚠️ Niente è stato montato in gioco, ed è voluto: il ticket chiede l'approvazione di Ivan sulle varianti *prima* del montaggio. Finché non sceglie, gli zombie restano il pendolare con la tinta verde, e la tinta si spegnerà da sola il giorno che esiste subway_commuter_zombie_frames.tres. ⚠️ Escluso di proposito il drunk, come chiesto.

SU-664 e SU-665: i topi corrono in longitudinale, e sulla banchina si può diventare zombie2026-08-31

  • Due KO dello stesso giorno, sugli stessi due file. SU-664: «i topi devono arrivare da sinistra verso destra e devono essere tanti che squittiscono… lo squittio va messo quando il topo compare o random, non quando colpisce il barbone». SU-665: «metti un gatto d'oro che dorme a terra prima della banchina… gli zombie devono molto lentamente provare a toccarti, se ti toccano diventi anche te zombie e finisce il round con la metro che va via da sola».
  • I topi cambiano asse. Buttato l'agguato al muro (TOPO_SCATTO_PX, TOPO_ATTESA_TRENO_SEC, i due posti d'attesa): ora il topo nasce 300 px dietro il barbone e fuori campo, corre verso destra a 170 px/s — 80 di chiusura — lo raggiunge e lo sorpassa, con una deriva verticale lenta verso di lui. Tabella per giro da [0, 3, 0, 5, 7, 8] a [0, 12, 0, 20, 28, 32]: quattro volte tanti, coi giri 1 e 3 che restano a zero come prima.
  • ⚠️ La trappola che la corsa longitudinale si porta dietro, trovata misurando: correndo per lungo il topo sta sui binari per tutta la vita, e le due sagome dei treni coprono l'intera fascia in cui cammina il barbone — senza contromisura i treni avrebbero spazzato via quasi ogni topo. È la stessa trappola già pagata da SU-624, con l'asse girato di 90°. Rimedio: il topo si tuffa sull'altra corsia quando sente il treno arrivare sulla propria. È anche il disegno migliore, perché si tuffa proprio dove stai scappando tu. 📌 Scartata l'alternativa «topi immuni ai treni»: avrebbe ucciso SU-667 (il treno che spazza il topo e fa cadere una lattina).
  • Lo squittio si è spostato dal contatto alla comparsa, più ricorrenze casuali finché il topo è vivo, con un SQUITTIO_GAP_MSEC a 330 che fa da tetto: con 32 topi al giro 6, senza quel tetto sarebbe un muro di rumore.
  • Il gatto d'oro dorme a terra nel corridoio, prima della banchina (misurato: x 3950 contro il traguardo a 4210), si raccoglie passandoci accanto e dà l'avviso «gatti infiniti» — chiave nuova BONUS_GATTI_INFINITI in tutte e 8 le lingue. Da lì i gatti lanciati diventano dorati. ⚠️ Rete di sicurezza voluta: chi entra in banchina senza averlo raccolto riceve comunque l'avviso, con i gatti normali. Senza quella, chi manca il gatto non saprebbe di poter lanciare, e il giro 3 sarebbe ingiocabile.
  • ⚠️ La prima tinta oro era sbagliata, ed è una lezione di palette: catsleep.png è già un gatto rosso-arancio, e una tinta oro «più rossa» lo saturava in una macchia di fiamma identica al gatto normale della città. L'oro su un soggetto già caldo si ottiene alzando il verde, non il rosso: Color(1.20, 1.50, 0.55).
  • Gli zombie ora danno la caccia, a 18 px/s contro i 90 del barbone: una minaccia solo se ti fermi. Al morso il barbone diventa zombie, la metro riparte e se ne va senza di lui, e il round finisce non vinto — passando da _termina(false), senza inventare un terzo esito. ✅ Il raccolto fino a quel momento resta suo: togliere anche quello sarebbe stata una doppia punizione che nessuno ha chiesto.
  • Sonda su albero pulito: 25 criteri, 0 KO. Margine di schivata del corridoio 7,3 s contro il pavimento di 6,0 (arrivo peggiore 52,7 s sul limite di 60), tabella dei topi rispettata sui sei giri, tetto di 3 inciampi mai sforato, morso a 7,5 s stando fermi, nessun topo vivo quando comincia la banchina.
  • 📌 Due correzioni nate dagli scatti, non dal codice: tutti i topi convergevano sulla riga esatta del barbone — un nastro trasportatore, non un branco — e ci è voluto uno scarto di mira di ±14 px; e la tinta oro di cui sopra. Nessuna delle due si vedeva leggendo il diff.
  • ⚠️ NON misurato: l'incasso lordo massimo della banchina al giro 3. È invariato per costruzionePASSEGGERI 40 × MONETE_PER_PASSEGGERO 5 = MONETE_FOLLA 200 non sono state toccate, e gli zombie pagano dalla stessa funzione dei passeggeri — ma non è stato verificato su una run intera. ⚠️ E c'è una conseguenza da provare col dito: con gli zombie che ora inseguono, l'incasso *raggiungibile* può essere sceso anche se il massimo teorico non cambia.
  • ⚠️ _corsia_runner() è rimasta senza chiamanti (la usava solo l'agguato al muro): innocua, ma chi cercherà «da che corsia nasce il topo» non troverà più niente. ⚠️ I .translation sono gitignored e sono stati rigenerati sul disco: chi esporta senza riaprire l'editor deve passare da un --import.

SU-673: le monete cadono una alla volta, e il piccione era sotto la sua stessa scia2026-08-31

  • KO di Ivan: «se segui il piccione devono cadere 20 monete una ad una, l'arcobaleno è troppo grosso lato piccione, deve essere piccolo come il piccione e al più ingrandirsi, non si vede bene il piccione per ora». Tre difetti distinti, tre cause distinte.
  • Le monete ora sgocciolano lungo l'inseguimento: una ogni 0,45 s finché il giocatore è dentro FUGA_RADIUS, cioè finché lo sta davvero inseguendo. 9 s di vita diviso 0,45 fa esattamente le 20 del ticket. Alla cattura o alla fuga il residuo cade in blocco: ✅ il totale sganciato è sempre 20, verificato sui due rami (preso: 12 residue dopo 4 s di caccia, poi zero; scappato: 20 in blocco, perché senza inseguimento non ne era caduta nessuna).
  • 📌 «Monete a terra» si è rivelata una misura inattendibile, ed è la scoperta utile del giro: il pickup di CoinDrop è un'Area2D da 17 px, e il barbone che insegue da vicino raccoglie le monete mentre corre — che è il comportamento giusto, ma falsa il conteggio (dava +3 su 20 attese). La verifica è passata al contatore interno _monete del piccione, l'unico punto da cui si decrementa. ⚠️ Trovato per strada un bug nella sonda stessa: contava i frame assumendo 1/60 s fissi, e in headless il motore gira più veloce del reale — 4 s richiesti diventavano una frazione.
  • La scia è invertita: SCIA_SPESSORE_TESTA da 14 a 5, SCIA_SPESSORE_CODA da 2 a 13. Prima era grossa proprio dove attacca al piccione. Il piccione misura 12-15 px di larghezza reale (bbox alfa sui fotogrammi di volo): la testa della scia sta sotto quella misura di proposito, e cresce andando indietro.
  • ⚠️ Perché il piccione non si vedeva, ed era un bug vero, non una questione di gusto: la scia ha SCIA_Z_INDEX = 3800 fisso (per stare sopra i tetti), mentre il piccione seguiva il normale y-sort, int(global_position.y), che sulla mappa non supera 2080. La scia gli disegnava sopra sempre. Aggiunto PICCIONE_Z_MIN = SCIA_Z_INDEX + 1.
  • Doppio contorno di casa attorno al piccione (nero a ridosso, panna appena fuori: la coppia che regge sia sul fondo chiaro sia su quello scuro). ⚠️ Al primo giro non funzionava, e il modo in cui falliva vale più della correzione: le copie di contorno erano colorate con modulate, che moltiplica invece di sostituire — il contorno scuro che lo sprite ha già, moltiplicato per la panna, resta scuro. Risultato: un anello tutto nero, cioè un piccione ancora più scuro su pavimento chiaro, esattamente il difetto che il KO chiedeva di togliere. Il codice era «giusto» e il commento diceva «un colore piatto (modulate)»: si vedeva solo guardando il ritaglio a 1:1.
  • Corretto cuocendo le sagome con Image — una per posa, colore piatto con l'alfa originale, in cache statica: la stessa tecnica delle texture cotte di SunHunter. ⚠️ Di proposito NON con uno shader: uno shader che non compila fa ripiegare Godot sul materiale di default e il risultato *sembra* giusto, e il compile-check non valida gli shader.
  • Provato guardando, a 1:1 ingrandito NEAREST, che è l'unico modo di giudicare un contorno da 1-2 px: files/homeless_city/TMP/su673_ko/dopo_chiaro_zoom_1a1.png (fondo chiaro, il caso difficile) e dopo_scuro_zoom_1a1.png. Anello esterno panna, interno nero, corpo dorato, distinti su entrambi i fondi.

SU-611: la scacchiera era il tratteggio · SU-617: creati gli otto ticket dei design, e il 50% diventa rosso2026-08-31

  • SU-611, KO di Ivan: «nel cerchio di azione all'interno c'è una scacchiera bianca nera, rimuovila». Non era una texture mancante e non era un caso: era il riempimento a tratteggio (x + y) % 2 di SunHunter._tex_bersaglio(), messo di proposito nel giro precedente col commento «mai una campitura piatta: un bersaglio pixel-art si buca, non si vernicia».
  • 📌 Perché la scelta era sbagliata, e vale la pena scriverlo: il tratteggio è disegnato in blocchi, e i blocchi vengono ingranditi ×4 dal BLOCK dello sprite. Un tratteggio a scacchi da 4 px, mezzo scuro e mezzo trasparente, steso sulla strada cotta e chiara di SOLLEONE, non si legge come un'ombra di bruciatura: si legge esattamente come la scacchiera con cui i motori grafici disegnano una texture che non c'è. La ragione «non verniciare» era giusta, il rimedio no.
  • Tolto: l'interno del bersaglio ora è vuoto. Il cerchio resta leggibile perché a definirlo sono tre cose che col riempimento non c'entrano — il doppio anello (bordo nero spesso + filo dorato), il crocino centrale e l'anello del conto alla rovescia che si stringe verso il centro. Via anche c_bruciatura, che senza il tratteggio sarebbe rimasta una variabile non usata: ⚠️ il compile-check non vede i warning-as-error, ma l'editor sì, e l'avrebbe rifiutata.
  • Provato guardando, prima e dopo: files/homeless_city/TMP/su611_pixel/su611_cerchio_solleone_tablet.png (il vecchio, con la scacchiera dentro) contro files/homeless_city/TMP/su611_ko_scacchiera/su611_cerchio_solleone_tablet.png (il nuovo). Lo scatto nuovo cade per giunta sul fondo di sabbia vero di SOLLEONE, che è il caso peggiore del criterio «il cerchio si vede anche sulla strada cotta e chiara»: si vede.
  • ⚠️ Trovato per strada e NON toccato: la stessa sonda segna un KO su un criterio di SU-610 — «il gatto muore sul cactus, il cactus resta in piedi» esce gatto rimosso=false. È precedente a questa modifica (qui è cambiata solo una texture) e non appartiene a nessun ticket dello sprint: lasciato dov'è, segnalato.
  • SU-617, risposta di Ivan: «si crea tutto e proviamo». Creati otto ticket su Jira, in backlog e non nello sprint che chiude il 3/09 — l'elenco era in TMP/lotto4_ticket_proposti.md (radice del repo) e nessuno era mai stato creato.
    • Dal design del maltempo: SU-691 (il banner delle calamità si sposta nel corridoio alto-destra e va ridisegnato a ≤136×68 — l'altezza 68 è il vincolo su tablet *e* telefono, il banner di oggi è 340×112), SU-692 (la pila delle notifiche scende in basso e cresce verso l'alto, più _hud_blocker_rects() da aggiornare), SU-693 (provino di regressione dell'angolo).
    • Dal design dei nickname: SU-694…SU-698, la sequenza 640a-e — misura vera su un secondo deployment Apps Script, i tre «no» distinti, indice e tetto (strada A), migrazione a Firestore (strada B), riconteggio del costo di SU-508/509/511.
  • 📌 Non duplicati i quattro delle push: i ticket 1-4 del design DESIGN_NOTIFICHE_PUSH.md erano già diventati SU-687/688/689/690 sotto l'epic SU-686. Il quinto (APNs iOS nostro) resta non creato, come dice il documento.
  • La modifica a SU-675 che nasceva da SU-617 è fatta: il primo gradino del flash di pericolo (SOFT, 50%→25%) passa da ambra Color(1.0, 0.82, 0.55) a rosso chiaro Color(1.0, 0.55, 0.42). Le soglie 50/25/10 non si toccano: cambia solo la tinta di quel gradino.
  • PANEL_IDLE_PEAK_SOFT resta 0.70, e questa volta è misurato invece che temuto: il file dei ticket proposti avvertiva che il moltiplicatore tarato per l'ambra poteva non bastare al rosso. È vero il contrario. Sullo scatto il telaio di cartone del pannello passa da (182, 131, 86) a (177, 105, 63): il verde cala del 20% e il blu del 27%, cioè il rosso stacca più di quanto staccasse l'ambra, che agiva quasi solo sul blu. Confronto affiancato in files/homeless_city/TMP/su675_rosso/confronto_normale_vs_soft.png.
  • 📌 Il rosso chiaro resta distinguibile dal gradino ALARM senza toccare nient'altro, perché i due gradini differiscono su tre parametri e non solo sul colore: periodo del respiro 3,0 s contro 1,8, picco 0,70 contro 0,88, e 2 battiti d'ingresso contro 3.

Il tunnel della metropolitana si apre a comando, al giro che vuoi2026-08-31

  • Chiesto da Ivan in chat: «ho un modo di provare la scena metropolitana subito senza doverlo fare dal gioco? al livello 2 e 3 soprattutto, che devo capire come funziona». Non ce l'era: il giro 2 e il giro 3 si aprono solo vincendo le discese precedenti, e SU-666 ha appena misurato il prezzo — $700 depositati in banca e 2 discese vinte per arrivare al terzo, ~2,8 minuti di solo tunnel più tutta l'elemosina che ci va sopra.
  • Nuovo lanciatore giocabile: bash tools/autotest/gioca_metro.sh [giro] apre il gioco dentro il tunnel, al giro scelto, e lo lascia in mano a chi lo lancia. Non è un provino: non misura, non scatta, non esce da solo. Mentre gioca, 1-6 riaprono il tunnel a quel giro e 0 riporta in città.
  • Come ci entra: BonusLevel.entra(mondo, giro) è statica ed è l'unica porta (lo dice il file), quindi il lanciatore fa la stessa cosa che fa la banca — città vera istanziata, avvia_musica() un attimo prima come in Interactable._metro_apri_bonus(), e per uscire _risali_ora(), cioè l'uscita che paga i premi e scongela la città. Nessuna scorciatoia nuova nel codice di gioco: il gioco non è stato toccato, il lanciatore vive tutto in files/homeless_city/scripts/tools/gioca_metro.gd.
  • 📌 Stampa cosa cambia a quel giro leggendo le costanti vere (TOPI_PER_GIRO di BonusLevel, ZOMBIE_DAL_GIRO di BonusRewards), non la tabella dei commenti: se la taratura di SU-624 cambia, la riga cambia con lei. Verificato in due avvii headless: giro 2 = 3 topi, folla normale; giro 3 = 0 topi, folla di ZOMBIE.
  • ⚠️ Il tunnel è identico a parità di giro, e va saputo prima di trarne conclusioni: _ready() semina l'RNG con SEME_BASE + giro * SEME_PASSO. Riaprendo il giro 3 si ritrovano treni e topi dove erano — comodo per studiarlo, inutile per giudicare la varietà.
  • ⚠️ La spiegazione a schermo si vede solo alla prima discesa, come in partita (_intro_veloce guarda GameState.bonus_level_visits, che è della run e non della scena): dalla seconda riapertura l'intro è quella corta. È il gioco vero, non un difetto del lanciatore.
  • ⚠️ Non provato col tasto premuto davvero: la riapertura a caldo (1-6) fa la stessa sequenza — _risali_ora(), attesa che il nodo muoia, entra() — che files/homeless_city/TMP/probe_su666_giro3.gd esercita già tre volte di fila senza KO, ma i tasti in headless non si possono premere. L'ingresso al primo colpo invece è verificato in avvio vero.

SU-666: al giro 3 non ci arriva quasi nessuno, e il numero adesso c'è2026-08-31

  • Il ticket teneva bloccata la taratura di topi e zombie su una misura che non era mai stata fatta: «fin dove arriva davvero il giro in una run vera». Ivan: «prova a farlo senza test su honor, ci penso io alla fine di sto giro di sprint» — quindi i due confronti A-B-A restano a lui, e qui si è chiusa solo questa.
  • 📌 Era diventata urgente in giornata: poche ore prima è stato chiuso SU-665, che dal giro 3 sostituisce i 40 passeggeri della banchina con zombie da colpire coi gatti. Se al giro 3 non ci arriva nessuno, quel lavoro lo vedono in pochi — ed è un dato che cambia le priorità, non un dettaglio.
  • Come avanza il giro, che era il pezzo da capire: non lo incrementa il livello bonus. Lo deriva Interactable._giro_premio_banca() dalla soglia della banca (100 → 200 → 400…), che raddoppia solo se si VINCE la discesa (banchina raggiunta). ⚠️ Morire o far scadere il tempo nel tunnel non fa avanzare niente: si rigioca lo stesso prezzo. Il giro 3 si apre solo alla terza discesa, dopo aver vinto le prime due.
  • Il numero, misurato e riprodotto dall'orchestratore (probe_su666_giro3.gd, 11 controlli 0 KO): per aprire il giro 3 servono $700 depositati in banca (100+200+400, cioè 70 versamenti da $10) e 2 discese vinte — non solo aperte. Costo in tempo delle due discese vinte: 165,5 s (2,8 min). Scoperta della sonda: la banchina dura davvero 32,75 s, non i 30 netti — c'è un preambolo prima che scatti il conto alla folla.
  • ⚠️ La risposta, ed è scomoda: no, un giocatore normale non arriva quasi mai al giro 3. Il confronto non usa una stima nuova ma il numero che il progetto già usa: il profilo «run onesta» di SU-663 vale $400 raccolti in tutta la run (è lo stesso metro con cui si era detto che il bot, a $39-71, non era rappresentativo). Il giro 3 chiede $700 di soli depositi, prima di contare cibo, igiene e gettoni, più ~4 minuti di tunnel. Il giro 1 ($100) è alla portata di chiunque; il giro 2 ($300 cumulativi) è già tre quarti del reddito atteso di un'intera partita e non lascia margine per sopravvivere; il giro 3 eccede il reddito totale che il design stesso considera normale.
  • 📌 Dove si interviene, se si vuole intervenire: la leva è il raddoppio della soglia (SU-466/494), non il contenuto di SU-665. Gli zombie non sono «poco visibili» per come sono fatti: stanno dietro un prezzo che quasi nessuno paga due volte nella stessa partita.
  • ⚠️ Perimetro dichiarato, perché la misura non è una run vera simulata: col TestBot non ci si arriva — muore a ~105 s contro i ~610 che servirebbero, un margine di 16×, quindi non è questione di semi fortunati. La sonda forza i depositi a mano e riusa i 48 s di corridoio già misurati altrove, ma vince davvero la fase banchina secondo per secondo. Quello che il conto non contiene, e la sonda lo stampa da sola invece di lasciarlo intendere: quanto ci mette l'elemosina a procurare quei $700.

SU-644 e SU-670: l'eco da cattedrale, e il fischietto non era dove pensavamo2026-08-31

  • SU-644, KO di Ivan: «ora la licenza eleven labs c'è e puoi generare il suono». Il giro precedente l'aveva lasciato apposta, e per una ragione giusta: gli effetti generati col piano gratuito vanno rifatti da abbonato, quindi generarlo allora significava crearlo già da rifare.
  • Generato con ElevenLabs (piano a pagamento confermato) dal prompt che era già scritto nel codice (SuperPower.gd:108), variato fra le tre prove. Montata la variante «thunderous»: 5,00 s contro i 0,48 s del fart.wav — dieci volte più lunga — con la coda che si spegne gradualmente e nessun clipping. Le altre due restano in TMP/su644_scoreggia/ per un confronto.
  • 📌 Trovato un bug di misura che falsava anche il numero di riferimento del brief: il downmix stereo→mono di default di ffmpeg gonfia l'RMS di +3 dB. Corretto, il montato chiude a -19,96 dB RMS sul bersaglio di -20 (il livello del treno e dell'avviso della metro). Un errore di misura di 3 dB non si sente come errore: si sente come «questo suono è più forte degli altri», e si insegue tarando la cosa sbagliata.
  • ⚠️ Verificato che il gioco carichi davvero il file nuovo, non che esista sul disco: un .wav senza .import lascia il gioco sul ripiego in silenzio, e il compile-check non reimporta. Sonda dedicata, probe_su644_scoreggia.gd.
  • ⚠️ SU-670: il fischietto non era dove il brief diceva, e l'errore era dell'orchestratore. Avevo indicato la cella 10 di super_icons.png come «il fischietto orribile». Il builder è andato a guardarla prima di ridisegnarla: mostra già una testa di cane, approvata da Ivan ieri. Il fischietto vero — dorato, con la corda rossa — è l'icona della carta al level-up, perk_icons.png cella 16. Ed è esattamente ciò che Ivan aveva scritto: «la carta super». Ridisegnare la cella 10 avrebbe distrutto arte approvata il giorno prima per un difetto che stava altrove.
  • Montato l'osso nella cella giusta: generato con Codex, 4 varianti giudicate DOPO la riduzione BOX a 16×16 — la misura vera con cui il gioco le disegna, non il disegno grande. Scelto l'osso orizzontale: alla diagonale la sagoma si spezza, e l'orizzontale è l'unico che resta leggibile su fondo chiaro e scuro col contorno nero. ✅ Zero pixel cambiati fuori dalla cella 16, verificato numericamente.
  • Cambiato anche il nome, che Ivan aveva chiesto esplicitamente («e cambiamo il nome, non deve essere fischietto»): CARTA_FISCHIETTO_CANI_NOME passa da FISCHIETTO PER CANI a OSSO PER CANI in tutte e 8 le lingue (DOG BONE / OS POUR CHIENS / HUESO PARA PERROS / HUNDEKNOCHEN / КОСТЬ ДЛЯ СОБАК / 狗骨头 / OSSO PARA CÃES). Con l'icona a osso, lasciare scritto «fischietto» sarebbe stata l'incoerenza che il KO voleva togliere. 📌 La chiave interna resta CARTA_FISCHIETTO_CANI_NOME: rinominarla toccherebbe i riferimenti, ed è la stessa ragione per cui in SU-644 l'id stomaco_ferro è rimasto com'è. ui_meta.csv reimportato a mano, perché il compile-check non reimporta.
  • ⚠️ Da guardare: il suono non è mai stato ascoltato da nessuno, e il nome «OSSO PER CANI» è una proposta dell'orchestratore — si cambia in una riga. ⚠️ Resta aperto che il suono del super è super_branco.wav e comincia con un fischio: con l'osso al posto del fischietto, il suono non torna più con l'icona.

SU-611: il cerchio e il raggio del sole diventano pixel art (e un bug trovato rifacendo lo scatto)2026-08-31

  • KO di Ivan: «il sole ora va bene ma i cerchi a terra e l'effetto raggio sono troppo brutti, va fatto per entrambi qualcosa più pixelart che stia bene col resto della grafica, per i raggi prendi spunto da un tema come quello dell'immagine allegata al task». Lo sprite del sole (variante D) è approvato e non si tocca.
  • Il difetto, guardato negli scatti prima di scrivere codice: il cerchio di preavviso era un anello vettoriale liscio (circonferenza antialiasata a spessore uniforme, campitura piatta semitrasparente) e il raggio un cono con gradiente continuo. In mezzo a un gioco disegnato a pixel grossi sembravano incollati da un altro programma.
  • Il riferimento allegato (scaricato in TMP/su611/riferimento_raggi.png) detta solo lo stile — è uno stock col watermark, non un asset da montare: raggi a ventaglio con bordi a gradini, bande piatte a 3-4 tinte calde, spicchi di larghezza alternata, nessuna sfumatura continua.
  • ⚠️ La trappola che il riferimento peggiorava: SOLLEONE ha il fondo giallo chiaro, e un cerchio giallo come il riferimento sarebbe stato invisibile proprio nel quartiere in cui vive. Risolto col doppio contorno già usato altrove nel progetto: bordo nero + filo dorato, riempimento a tratteggio. Verificato con scatti su entrambi i fondi, giorno e notte.
  • 📌 Niente draw_rect a raffica, che era il rischio dichiarato nel brief: le forme sono texture pixel-art cotte una volta sola (stessa tecnica di _texture_sole e Cactus._texture_ombra, con cache statica) e mostrate via Sprite2D in NEAREST, mosse ogni frame solo con transform. Si passa da fino a 11 comandi di disegno immediati a zero, e resta solo la trasformazione di 4 Sprite2D. Il gioco è al tetto di framerate sui telefoni (SU-226).
  • ⚠️ Rifacendo lo scatto del raggio è saltato fuori un bug vero, introdotto dal lotto stesso. Il primo giro consegnava uno scatto in cui il fascio non si vedeva, e il builder l'aveva attribuito a un flash pieno-schermo pre-esistente; anche l'orchestratore aveva dato una diagnosi incompleta (il ritardo dello scatto, portato da 2 a 9 frame). La causa vera era lo z-order: la vampata a terra (raggio 48 px) copre quasi tutto il cono (52 px), e ridisegnata a bande quasi opache — alfa 0,86-0,96, necessarie per leggersi «a blocchi» — lo nascondeva del tutto, su ogni frame. Il codice vecchio se la cavava solo perché la sua vampata era un draw_circle traslucido ad alfa 0,50. Corretto scambiando l'ordine dei due Sprite2D in _Raggio._ready().
  • 📌 Un difetto che si sarebbe visto solo in gioco, e che una prova più debole avrebbe lasciato passare: la sonda del meccanismo era verde in entrambi i casi — il raggio *funzionava*, semplicemente non si vedeva. È il genere di cosa che nessun controllo automatico può cogliere, e che si trova solo guardando l'immagine e chiedendosi perché non mostra quello che dovrebbe.
  • Lo scatto ora non dipende dal tempismo: al posto di un singolo ritardo c'è una raffica di 5 scatti (1/4/8/12/16 frame dopo l'inizio del raggio), così il flash di play_hit_negative() — 0→1 in 0,05 s, poi 1→0 in 0,17 s, indipendente da questo ticket — non fa più perdere il fotogramma buono. I dieci scatti (5 tempi × giorno/notte) restano in files/homeless_city/TMP/su611_pixel/.
  • ⚠️ Non provato: nessun numero assoluto di costo — non c'è un profiler di frame-time, e la riduzione è per costruzione del codice, non misurata a runtime. Nessun device fisico. Compile-check verde su 550 script. Meccanica invariata: TELEGRAPH_RADIUS, _spara() e lo sprite del sole non sono stati toccati, e SunHunter resta locale per peer.

SU-665: la banchina diventa un tiro a segno, e gli zombie sono i passeggeri2026-08-31

  • Il ticket era stato riscritto da Ivan il 30/08 e cambiava natura: dal giro 3 i 40 passeggeri della banchina sono sostituiti dagli zombie, che non si raccolgono camminandoci sopra ma si colpiscono coi gatti, e ognuno droppa le monete come faceva il passeggero. Da raccolta a tiro a segno.
  • 📌 La decisione che rende vero il criterio più delicato: gli zombie NON sono un sistema nuovo, sono i quaranta passeggeri. Dal giro 3 BonusRewards costruisce la stessa folla — stesso array, stessi stati, stessa discesa dalle porte, stesse 5 monete a testa — con lo sheet dello zombie; cambia solo *come si paga* (colpisci_zombie() invece di chiedi_elemosina(), che con gli zombie torna 0). Così l'invariante «l'incasso lordo massimo non cambia» diventa strutturale: MONETE_FOLLA (200) e folla_finita() non li tocca nessuno. Scartato un sistema zombie separato, che avrebbe significato due conti da tenere allineati — cioè il modo di sbagliare l'invariante.
  • ⚠️ Il gatto della banchina NON è CatProjectile.tscn, ed è la scelta più pesata del lotto. Quello di città a ogni colpo accredita XP (add_xp_from_source(3, "gatto")), ScoreSystem.note_cat_hit e l'impresa GATTOPOLI: 40 zombie per giro = +120 XP e mezza impresa regalati da dentro un livello a conto economico chiuso. E interroga gruppi (cactus, police, mp_puppet, world) che sotto terra non esistono. Qui è uno Sprite2D mosso a mano come tutto il tunnel, ma con la stessa texture e gli stessi numeri copiati da CatProjectile.gd (260 px/s, 420 px, raggio 18).
  • Il gesto è lo stesso di sopra (tasto fart, la «X» su mobile) ma parte alla pressione e non al rilascio: sotto terra la scoreggia non esiste, quindi i 300 ms di soglia sarebbero stati solo ritardo su un tiro a segno da 40 bersagli. Ricarica 0,25 s e non i 0,5 di città — a 0,5 il tetto del livello era irraggiungibile per costruzione.
  • L'abbraccio è tolto con ABBRACCIO_SEC, ZOMBIE_RIPRESA_SEC e ZOMBIE_RAGGIO. Non è una semplificazione: con 40 zombie un blocco da 0,8 s non sarebbe stato un disturbo ma una gabbia.
  • ⚠️ Il brief dell'orchestratore conteneva un errore, e il builder l'ha corretto invece di eseguirlo. Avevo dato per buono che arch_office_zombie fosse «uno zombie già animato e montato», dedotto dal nome del file: ingrandendo lo sheet è l'«Impiegato esaurito» — camicia azzurra, pelle normale. Montato liscio, la banchina del giro 3 sarebbe uscita identica a quella del giro 1, e il criterio 2 sarebbe stato bocciato a occhio con il codice giusto. Si usano invece le sue 4 varianti animate con una tinta verde, applicata solo finché manca subway_commuter_zombie_frames.tres: quando l'arte dedicata di SU-668 arriva, il gancio la carica e la tinta sparisce da sé.
  • Misurato (probe_su665_zombie.gd, 13 controlli 0 KO): 40 zombie abbattuti con 40 gatti veri — non chiamando la funzione che paga, perché un oracolo che salta il gatto direbbe OK anche se il gatto smettesse di colpire — per 200 monete = $200 al taglio minimo, e $800 al giro 3 con la moneta ×4: esattamente quanto pagavano i passeggeri. 65 lanci senza mai esaurirsi, elemosina 0 al giro zombie e 20 monete al giro 1 come controprova, 0,00 s di blocco. Nessuna regressione: probe_su664 12 criteri 0 KO, probe_su491_metro 24 criteri 0 KO.
  • ⚠️ Aggiornata probe_su664_topi.gd, che misurava il ticket caduto («lo zombie è sempre UNO») e da oggi avrebbe dato KO per sempre. È lo stesso genere di trappola trovata in SU-681: una sonda che boccia il codice giusto perché difende un requisito superato.
  • ⚠️ Non provato: il feel. 40 bersagli in 30 s a 0,25 s di ricarica sono 10 s minimi di soli lanci più il tempo di raccattare 200 monete. Resta una corsa, ed è voluto, ma la frustrazione la misura solo la mano. Nemmeno il suono è stato ascoltato. MP non applicabile: BonusLevel.entra() rifiuta l'ingresso se NetworkManager.active, il livello bonus è single-player per costruzione.

SU-681: l'arcobaleno lontano se ne va, e con lui il criterio che lo pretendeva2026-08-31

  • Segnalazione di Ivan: «l'arcobaleno, quando sparisce dallo schermo, dopo un po' riappare curvo e piccolo. Non deve comparire se non è nel raggio di vista dell'arcobaleno».
  • 📌 Non era un difetto di disegno: era una funzione che faceva esattamente quello per cui era stata scritta. L'ARCOBALENO LONTANO di SU-249: quando nessuno dei 32 punti della spezzata banca→bidone cadeva nell'inquadratura, si accendeva una copia rimpicciolita attorno alla camera (fattore fino a 0,12, alpha 0,72). Il «curvo e piccolo» era quella copia, il «dopo un po'» la sua dissolvenza in entrata.
  • Va via tutta: 182 righe da Interactable.gd (costanti RAINBOW_LONTANO_*, le variabili, rainbow_lontano_attivo(), _aggiorna_arcobaleno_lontano(), _incastra(), _fade_arcobaleno_lontano() e le chiamate in _process) e 33 da RainbowArc.gd (scala_spessore e dissolvenza_capi, che esistevano solo per la copia). Il criterio del ticket chiedeva che non restasse codice appeso e spento: non è rimasto.
  • ⚠️ Stiamo rimuovendo di proposito una funzione nata da un KO precedente. Il lontano era la risposta al KO di SU-249 dell'11/08 («sia sempre visibile a video»), e il conto di allora resta vero: l'arco vero si vede dal 13,4% delle posizioni della mappa. Ivan l'ha visto in gioco e sceglie lo stesso di spegnerlo. Il faro non resta senza sostituto: la cartina disegna già lo stesso arco (SU-485).
  • Provato con la coppia prima/dopo, che è l'unica prova che vale qui: uno scatto «dopo» pulito non dimostra niente se il provino non sa riprodurre il difetto. files/homeless_city/TMP/su681/su681_0_prima_bug_lontano.png mostra l'arco piccolo e curvo piantato al centro dello schermo — e il codice vecchio è stato rimesso solo in una copia isolata del progetto (/tmp/su_501_lotto9_proj), mai nel repo condiviso, per non far inciampare gli altri builder in parallelo. su681_1_lontano.png è la stessa scena allo stesso istante (0802, Giorno 1) senza nessuna banda; su681_2_vicino.png ha l'arco vero a taglia piena.
  • ⚠️ Trovato un quarto consumatore fuori dal perimetro del ticket, ed era una trappola pronta a scattare: scripts/tools/probe_su249_arco.gd, la sonda storica del KO originale, chiamava _aggiorna_arcobaleno_lontano() — errore runtime — e soprattutto bocciava se la copertura col lontano non era totale. Con il lontano spento quel criterio è sempre violato: la sonda avrebbe detto FALLITO su codice sano, che è il modo più caro di sbagliare (si va a cercare un bug che non c'è). Aggiornata: misura la copertura dell'arco vero e non è più un criterio di KO, perché una copertura parziale ora è la scelta, non un difetto.
  • Restano intatti e verificati: rainbow_polyline(), rainbow_lato() e rainbow_extent(), cioè l'arco sulla cartina (MapOverlay) e la lettura del DebugPanel. Compile-check 5/0.

SU-673 e SU-619: il piccione d'oro a comando, e tre versioni invece di una2026-08-31

  • SU-673, KO di Ivan: «metti nel menu di debug la possibilità di spanarlo per capire come funziona». Il piccione d'oro compare ogni 150 s: per guardarlo funzionare bisognava aspettarlo. Nuova voce «Spawna piccione d'oro» nel DebugPanel, stessa sezione e stesso stile delle altre.
  • 📌 Non duplica niente: la voce forza solo il timer interno _piccione_oro_t del World locale — lo stesso meccanismo che usava già la sonda — invece di reimplementare comparsa, scia, notifica e cattura. Un secondo percorso di spawn sarebbe divergiuto dal primo alla prima modifica. Locale per costruzione, nessun RPC. Premendo due volte col piccione già in volo esce «già in volo» e non ne nasce un secondo.
  • ✅ La sonda preme davvero la funzione del bottone, non chiama il codice sotto: comparsa, scia, notifica, blocco della doppia pressione, cattura (+2 monete) e nessuno spawn fuori dalla run — 4/4 fasi OK.
  • SU-619, riportato in «Da fare» da Ivan il 30/08 con un requisito nuovo: l'avviso di versione va diviso per piattaforma — pc/mac, iOS e Android — e ogni build controlla solo la propria. Il motivo, parole sue: le tre pubblicazioni escono in momenti diversi perché Apple ci mette tanto ad approvare, e con un numero unico l'iPad direbbe «c'è una versione nuova» appena esce la build desktop, mandando l'utente su TestFlight dove non c'è ancora niente.
  • Nuova Env.piattaforma_id(): Android → android, iOS → ios, tutto il resto → desktop (mappatura dichiarata, non implicita: macOS, Windows e Linux stanno insieme perché ricevono la build dallo stesso canale a mano). UpdateNotice legge versions.<piattaforma> invece di version.
  • 📌 Un manifest vecchio viene ignorato in silenzio, e la scelta è deliberata: leggerlo con un fallback avrebbe ricreato *esattamente* il bug del KO — un numero unico letto da tutte le piattaforme. Lo schema sale da 1 a 2 e il controllo è esatto.
  • ⚠️ La conseguenza va saputa prima di pubblicare, ed è simmetrica: il controllo esatto vale in entrambe le direzioni, quindi il giorno che i manifest schema 2 vanno online, le build 0.41 già in mano ai tester smettono di ricevere l'avviso in-game e vanno raggiunte con /avvisa-tester. Non è una regressione: è il prezzo, pagato una volta sola, di cambiare formato sotto build già uscite. Scritto nel commento di tools/release/genera_manifest_versione.py, dove lo si legge nel momento in cui serve.
  • Chiuso anche il buco a valle, che il builder aveva segnalato come follow-up: tools/release/genera_manifest_versione.py generava ancora schema 1, e alla prossima release avrebbe pubblicato un manifest che nessuna build sa più leggere. Ora genera schema 2 con le tre versioni, e ha tre flag nuovi — --versione-desktop, --versione-ios, --versione-android — per dichiararle diverse fra loro, che è tutto il senso del cambio: se Apple è indietro, iOS resta a 0.41 mentre Android va a 0.42. Senza flag valgono tutte quella del progetto, cioè il caso normale.
  • ⚠️ Non provato: il giro di rete vero. I manifest schema 2 non sono ancora online, quindi la logica è provata con manifest finti locali (16/16 fasi sulla sonda esistente, aggiornata alle fixture nuove) più una sonda dedicata al caso del KO: bump su iOS e Android senza bump su desktop → su una build desktop nessun avviso, 4/4. I rami iOS e Android non girano su device reali. Compile-check 6/0, audit emoji OK.

I quattro design che aspettavano Ivan: le sue risposte diventano decisioni scritte2026-08-31

Quattro ticket (DESIGN) erano fermi non perché mancasse l'analisi, ma perché finivano con una domanda aperta. Ivan ha risposto a tutte e quattro fra il 30 e il 31/08, e i documenti recepiscono le risposte. Nessuno di questi tocca codice di gioco.

  • SU-617 — «metterei già rosso dal 50% così la gente si sveglia prima». Il design era approvato («OK facciamolo così») con una modifica sola: la soglia del rosso. 📌 Non nasce un ticket nuovo: è una modifica a SU-675, che è già codice scritto (HUD.gd:594-624 e 3795-3900) — il colore SOFT passa da ambra a rosso chiaro. SU-676 non cambia. Distinguere una modifica a un ticket esistente da un ticket nuovo è la differenza fra due righe di diff e un giro di lavorazione.
  • SU-620 — «il banner grosso lo metterei a destra di vita e livello… e lo toglierei da sotto». Ivan sceglie spostare, non sdoppiare: l'opposto della raccomandazione del 28/08, che resta nel documento marcata come superata con data e motivo, non cancellata.
  • 📌 La conseguenza è stata misurata, non assecondata, e il numero è severo: il banner di oggi è 340×112, il corridoio fra vite e colonna è 136×68 sul tablet. Non ci sta su nessuna delle due dimensioni, e ⚠️ il vincolo vero è l'altezza — 112 contro 68 — che sfora allo stesso modo su tablet e telefono. Non è «il tablet è la forma stretta», come diceva la misura del 28/08 riferita alla larghezza: quella zona è bassa su entrambe le forme.
  • Risposta esplicita alla domanda «riapre SU-272?» (il KO che Ivan stesso scrisse il 04/08 perché il banner in alto copriva la città): sì se il banner resta grande e viene incastrato lì invadendo i vicini; no se si restringe per starci — l'area passa da 38.080 a 9.248 unità quadre, il 24% di oggi. A quella taglia però «il banner grosso» smette di essere grosso e diventa, di fatto, il *promemoria compatto* che la raccomandazione superata proponeva. ⚠️ Rischio segnalato e non deciso: sul telefono 136 unità-canvas sono ~69 px fisici, la stessa taglia già misurata come illeggibile.
  • SU-640 — «se la base dati fosse su un Firebase db cambierebbe la velocità?». Risposta con le fonti citate e datate (quote ufficiali Firestore, verificate il 31/08), non con un aggettivo: Firestore con vincolo di unicità su nickname_lower elimina i 106,2 s del caso peggiore per costruzione, perché l'unicità la garantisce l'indice e le rivendicazioni smettono di stare in fila. ⚠️ Non c'è un deployment vero da cronometrare — nessun account Firebase esiste ancora — e questo è dichiarato invece che stimato. Nessun ticket nuovo: i cinque già proposti restano, e SU-640d ne esce meglio giustificato.
  • SU-678 — «TestFlight adesso, beta separata dal pubblico, non aprire telegram per desktop». Le tre domande in fondo al documento diventano tre decisioni: iOS subito (quindi il primo ticket-cancello non è più solo Android), due topic distinti beta/pubblico, e niente Telegram — i 19 desktop restano coperti da SU-619 e WhatsApp. ⚠️ La parola «KO» apriva il commento, ma le tre risposte non ribaltano la scelta di FCM: la confermano e chiudono le domande.
  • Consegna operativa: TMP/lotto4_ticket_proposti.md (radice del repo) — 8 ticket nuovi e 1 modifica, raggruppati per ticket-padre e in ordine di esecuzione. Non sono stati creati su Jira: aspettano l'ok di Ivan.
  • ⚠️ Il lotto è morto a metà per una sospensione del Mac (errore API «your computer went to sleep»), a documenti già scritti e file dei ticket ancora da fare. È stato ripreso dal punto esatto invece che rilanciato: rifare i quattro documenti sarebbe costato un lotto intero per riscrivere ciò che era già su disco.

SU-682 e SU-646: l'ombra del piccione, e gli spazzini non nascevano dove diceva il ticket2026-08-31

  • SU-682, due rifiniture sui piccioni di SU-672. Il piccione era l'unica creatura a terra senza ombra — cactus, NPC, barbone e arredi ce l'hanno — e senza sembrava incollato sopra il marciapiede. Ombra 7×3 sul modello di Cactus._texture_ombra(), ridotta alla taglia del piccione (9×7 a terra, 13×8 in volo, contro i 22×8 del cactus).
  • 📌 L'ombra non può seguire il corpo per costruzione, non per taratura: vive sulla radice del nodo, mentre a salire in verticale è _spr. Non è una scelta estetica ma strutturale — un'ombra figlia del corpo si sarebbe alzata col saltello e col volo, ed è esattamente il difetto che il ticket vietava («mai un'ombra appiccicata alle zampe a mezz'aria»).
  • Il salto è provato col numero, non con uno scatto — e la prima volta non lo era. Gli scatti fermo.png e salto.png erano lo stesso file byte per byte (md5 identico): è la trappola nota dei provini in cui la posa non tiene, e uno scatto «diverso» ma pixel-identico sembra una verifica riuscita. Rifatto come misura (probe_su682_salto_ombra.gd, salto fermo sul posto per isolare il rimbalzo dalla deriva): su 12 campioni l'ombra resta a 199.000 esatti, zero movimento, mentre il corpo fa la parabola 194 → 191 → 194 — i 3 px dichiarati nel commento di Pigeon.gd.
  • Mai sotto la pensilina, ai due punti che il ticket chiedeva: la nascita (_punto_libero_piccione in WorldGenerator.gd, che ora riceve bus_stop_rects) e l'atterraggio dopo la fuga (in Pigeon.gd). Misurato su 8 quartieri: 122 piccioni, 0 violazioni. Determinismo riverificato con probe_su672.sh: OK.
  • ⚠️ SU-646: il file indicato nel ticket era sbagliato, e la diagnosi l'ha scoperto prima di scrivere il rimedio. Il ticket mandava a WorldGenerator.gd per lo spawn degli spazzini, ma lì di spawn non ce n'è: le sette occorrenze di «spazzino» in quel file sono tutte commenti. Gli spazzini sono folla NPC viva, gestita da DistrictSpawner.gd con un pool pesato non seedato, host-authoritative. Il rimedio è stato scritto lì.
  • 📌 Conseguenza sul criterio di determinismo del ticket, che chiedeva di «consumare comunque il tiro di dado»: non si applica, perché quel sistema non è mai stato nella sequenza RNG condivisa host/client. Vale invece per i piccioni, dove infatti è stato riverificato. Un criterio scritto per il file sbagliato non si applica a forza al file giusto.
  • Il tetto sta nel ramo dove c'era già il suo gemello: in DistrictSpawner._spawn, accanto al tetto «max rivali in mappa» di SU-89, se l'archetipo è HOME_CLEANER_ARCH_ID e nel parco c'è già uno spazzino l'arch_id si svuota e lo spawn salta (non viene sostituito con un altro NPC — stesso comportamento del tetto rivali). Acceso da un flag di quartiere, spazzino_uno_a_parco, dichiarato solo su Gattopoli (Quartieri.gd:394): negli altri quartieri non cambia niente, come chiedeva il ticket.
  • Misurato: prima del rimedio max 2 spazzini nello stesso parco (la diagnosi che il protocollo dei bocciati impone prima di toccare il codice), dopo 0/1/1 su tre semi. Compile-check 7/0.
  • ⚠️ Trappola trovata lavorando: la prima sonda per SU-646, lanciata con -s, dava falsi negativiModularNPC.gd non compila sotto -s per via di autoload nominati per simbolo. Riscritta come scena, che è il modo giusto di lanciare una sonda extends Node2D.

SU-683 e SU-684: i proiettili si fermano sui muri, e il giullare tira la metà2026-08-31

  • SU-683, segnalazione di Ivan: il gatto lanciato attraversa gli edifici e colpisce un poliziotto dall'altra parte del palazzo. Vale per tutti e quattro i proiettili (CatProjectile, BottleProjectile, PinProjectile, TrashBagProjectile), che sono Node2D senza fisica e non guardano niente per strada.
  • 📌 La distinzione «alto quanto una persona» non è la collisione fisica, ed è il punto che rendeva il ticket facile da sbagliare. Panchine e fontane sono solide esattamente come i palazzi (StaticBody2D su layer 1): un raycast sul layer 1 le avrebbe fermate e il criterio «il gatto sopra la panchina passa» sarebbe morto. Si distingue per tipo, e le fonti sono state verificate col grep, non assunte: WorldGenerator.wall_rects è riempito solo da _make_static_wall (riga 6832) — edifici, rifugio, vineria, gattile, vestiti, chiesa/banca, chiosco e palo della metro, bagni pubblici, banconi del mercato — mentre panchine e fontane passano da _add_obstacle_rect/circle, che tocca solo il navmesh e non quella lista. Il criterio 2 regge quindi per costruzione, non per taratura.
  • ⚠️ Scartato bus_stop_rects, che sembrava la fonte ovvia per le pensiline: è l'isolato intero 96×96 e avrebbe fermato anche i tiri che passano di fianco alla pensilina. Per camioncini e pensiline si usa _bin_avoid_rects, che è l'ingombro visivo vero (64×60 e 80×76).
  • La decisione sta in un punto solo: World.hits_tall_obstacle(p), che interroga un indice a celle da 128 px costruito una volta per generazione — 0,58 µs a controllo su 55 rettangoli, un controllo per proiettile per passo e nessun raycast fisico (il gioco è al tetto di framerate sui telefoni, SU-226).
  • Rimbalzo e caduta in un file nuovo, files/homeless_city/scripts/entities/ProjectileCrash.gd: i quattro proiettili chiamano blocked() e start(). Ci è stato fatto delegare anche CatProjectile._muori_sul_cactus, coi numeri di SU-610 intatti, così cactus e muro sono letteralmente lo stesso codice invece di due copie che divergeranno.
  • Due scelte dichiarate: la *grazia iniziale* — un proiettile nato dentro un rettangolo alto (il barbone sotto la pensilina, che ha il fronte aperto) non muore sul posto — e nessuna notifica a schermo sullo schianto, a differenza del cactus: i muri sono ovunque, e si evita anche una chiave I18n in otto lingue.
  • Misurato (files/homeless_city/TMP/probe_su683_684.gd, runner tools/autotest/probe_su683.sh, 11 controlli 0 KO, riprovato dall'orchestratore): panchine, fontane e cestini 0 su 52 fermano; camioncini, negozi, bagni e metro 17 su 17; tutti e quattro i proiettili si schiantano sulla facciata (y 575-576, muro a 576), con controprova su strada libera dove volano interi.
  • SU-684, richiesta di Ivan: «nerfare il lancio del giullare, troppo martellante e difficile». Quattro numeri in ModularNPC.gd:343-346: cooldown 1,2 → 2,0 s, gittata 300 → 240 px, bottino 8 → 6 $, e JESTER_HITS resta 3 (si nerfa il martellamento, non l'avversario). PinProjectile.HIT_RANGE non toccato: è condiviso con gatto e bottiglia.
  • 📌 Il nerf non è proporzionale, ed è la parte interessante della misura. Stessa città e stessa scena, 60 s: i tiri calano del 57% (46 → 20, la banda richiesta era 40-60%) ma le vite perse solo del 25% (20 → 15), perché con la gittata più corta il giullare tira solo da vicino e quei tiri arrivano quasi tutti — la precisione *sale*, da 89% a 100%. Martella la metà e resta pericoloso: è il «non deve diventare innocuo» del ticket, verificato invece che sperato.
  • ⚠️ Da guardare a mano, l'unico caso non giudicabile in sonda: il senso del rimbalzo su un muro a nord. La spinta è -direction.x (numeri approvati in SU-610), quindi un gatto tirato verso l'alto rimbalza *dentro* il palazzo prima di cadere — si vede sopra i tetti e come gag regge, ma va guardato in gioco.
  • ⚠️ Nel tutorial i proiettili passano come prima: TutorialWorld non ha hits_tall_obstacle. È voluto, non una dimenticanza.
  • ⚠️ Trappola costata un giro di lavoro: /tmp/su_501_proj è condivisa fra builder paralleli, e un altro lotto ci stava scrivendo dentro. Un giro di sonda è uscito con un KO fantasma («il lancio non ha prodotto nessun proiettile») che sembrava una regressione. Chi lancia sonde in parallelo deve imporre SU_TMP=<cartella sua>.

SU-685: la classifica dice dove sono io, e la posizione era già nella risposta che leggevamo2026-08-31

  • Richiesta di Ivan: «classifica, se non sono in top 5 va gestito di vedere dove sono io, e la top 5/8 anche». Prima si vedeva solo la testa: chi non ci stava dentro non sapeva se era sesto o duecentesimo.
  • 📌 Il fatto che ha deciso il ticket, ed è l'opposto di quello che il ticket temeva. Il ticket avvertiva «la posizione va CHIESTA a EOS, non dedotta», immaginando una seconda chiamata di rete a rate limit. Ma la lettura che il gioco fa già — HLeaderboards.get_leaderboard_records_async(), cioè EOS_Leaderboards_QueryLeaderboardRanksrestituisce la board intera: il wrapper cicla su get_leaderboard_record_count senza finestra, e ogni record porta il rank scritto da EOS. Eravamo noi a buttarla via dopo l'ottava riga, e solo per disegnare. La posizione vera si legge cercando il proprio PUID in quella stessa risposta: zero chiamate di rete in più (misurato: 100 my_row() non spostano letta_msec né lo stato).
  • ⚠️ Quello che EOS non sa fare, ed è il muro da conoscere: non esiste una query che dia il rank di un giocatore qualsiasi. QueryLeaderboardUserScores torna LeaderboardUserScore {user_id, score} e basta — il rank non c'è (eos_leaderboards_types.h:188). Una seconda chiamata costerebbe un giro di rete per non rispondere alla domanda, e infatti non è stata scritta.
  • Quattro stati, non tre come chiedeva il ticket. Aggiunto MIA_ARRIVO («il tuo punteggio sta prendendo posto») perché a fine partita la lettura parte nello stesso frame dell'invio: dire «non sei in classifica» a chi ha appena spedito sarebbe la stessa bugia che SU-474 aveva già tolto da STATO_VUOTA. Gli altri tre sono quelli del ticket: MIA_ASSENTE (nessun punteggio in questa board), MIA_IGNOTA (risposta tagliata al tetto EOS di 1000: la mia assenza non prova niente), MIA_NO (sono già in testa, e allora non si disegna niente — è il criterio «non compare due volte»).
  • Nel menu il pannello era pieno, e nessun altro elemento poteva scendere: le sei righe finiscono a 0.703 e AGGIORNA comincia a 0.735, venti pixel su tablet. La lista si stringe (passo 0.058 → 0.0475, corpo a ¾ del passo per il telefono) solo quando la mia riga c'è davvero; senza di lei posizioni e corpo sono identici a prima.
  • ⚠️ Il criterio che si rompe in silenzio era il conto delle voci navigabili: una Label in più dentro _clol_row_labels avrebbe mandato il cursore su una voce che non esiste, e non se ne accorge nessuno finché non lo prova col pad. Le due Label nuove stanno fuori dalla lista e senza tap: _clol_item_count() non è stato toccato, e il giro completo del cursore dà 9 voci sia dentro sia fuori dalla testa.
  • Provato con 11 scatti in files/homeless_city/TMP/su685/ (tablet 1024×768 e telefono 844×390), runner tools/autotest/shot_su685.sh → ESITO=OK: i quattro stati nelle due schermate. Una correzione dopo il primo giro: i puntini di continuità a 0,8× del corpo erano una macchia illeggibile, ora sono al corpo pieno delle righe. Compile-check 4/0, audit emoji ESITO OK. Tre chiavi nuove in 8 lingue in ui_menu.csv.
  • ⚠️ Non provato: nessuna riga viene da EOS vero — niente login né rete in sessione, le righe le posa debug_set_board. Restano da vedere su deployment vivo il rank di una board grande e il caso oltre le 1000 righe.

Il modulo delle segnalazioni dimagrisce: da 12 a 8 domande, e le obbligatorie restano tre2026-08-31

  • Richiesta di Ivan in chat: il documento di segnalazioni «va rivisto, più semplice». Nome o nickname facoltativo; via «come ti ricontatto», «quanto è grave» (la gravità la decide lui, non chi segnala), «titolo breve» (basta la descrizione) e «da dove hai scaricato la build»; «come si riproduce» facoltativo.
  • Modificato il Google Form vero, non solo lo script: https://docs.google.com/forms/d/1358tdyfzz-sqPuXSMeMKQaOv2GcISRprxGTFD7j0dZ4/edit. Quattro domande eliminate, «Nome o nickname» tolto dagli obbligatori e rititolato «(facoltativo)», «Come si riproduce? (passi)» → «(facoltativo)». Restano 8 domande, 3 obbligatorie: piattaforma, tipo di segnalazione, descrizione.
  • 📌 Il link non è cambiatohttps://forms.gle/bQPwQvpUgKNGnLqx9 è lo stesso di prima: la voce «SEGNALA BUG O IDEA» del menu principale (FEEDBACK_URL in files/homeless_city/scripts/ui/MainMenu.gd:175) continua a funzionare senza toccare il codice, e nessun tester va riavvisato.
  • ⚠️ Le 3 risposte già raccolte restano nel foglio collegato: cancellare una domanda dal form non tocca le colonne già scritte, quindi «Titolo breve» e «Gravità» restano leggibili nello storico e spariscono solo dalle risposte nuove.
  • Allineato lo script che ricrea il form da zero, BETATESTING/crea_form_beta.gs: stesse 8 domande, stessi obbligatori. Colta l'occasione per togliere «Street University» dai testi che lo script scrive (titolo del form, descrizione, nome del foglio risposte) — il form vivo si chiamava già Street University, lo script no. Aggiornato anche l'elenco dei campi in BETATESTING/README.md.
  • ⚠️ Non toccato: il nome del file su Drive, che resta «Street University — Beta Feedback» (è l'etichetta interna della cartella di Ivan, non un testo che vede un tester).

Le pozze di luce fuori dai lampioni: un TextureRect grande quanto la sua texture2026-08-31

  • Segnalazione di Ivan in chat, con schermata: «con le modifiche della scorsa volta sulle performance si sono sballate le luci in città, si muovono non dove ci sono i lampioni e i palazzi (e il barbone) ma a caso». Nella schermata il barbone non ha la sua lanterna sotto i piedi e le pozze stanno su prato e marciapiede senza nessuna sorgente.
  • La causa non è il calcolo delle posizioni, ed è stato separato prima di toccare il codice: _aggiorna_pozze_lampioni passa allo shader coordinate giuste: il difetto è dove finisce a schermo la composita che le disegna. La composita di SU-581 bis gira in un SubViewport a metà lato e viene stirata da una TextureRect — che ha per dimensione minima quella della propria texture. Con la finestra del Mac (misurato: 4288×2412 px veri, canvas logico 1365×768) la texture è 2144×1206 e vince sulla minima: il rect diventa 1,57× lo schermo, ancorato in alto a sinistra, e ogni pozza scivola verso il basso a destra in proporzione alla distanza dall'origine — cioè si sposta mentre cammini, che è l'«a caso» della segnalazione.
  • 📌 Perché è passato inosservato per una settimana. Il difetto compare solo quando metà dei px veri supera il canvas logico. Sull'Honor (2400×1080 su canvas 1662×768 → texture 1200×540) e in ogni provino a finestra piccola gli anchor vincono e non si vede niente: SU-581 bis era stato collaudato lì. È lo stesso spazio-coordinate che il commento del commit dichiarava di aver trattato (coord_scale) — la matematica dello shader era giusta, a sbagliare era il rettangolo su cui la si disegnava.
  • La cura è una riga, comp_view.expand_mode = TextureRect.EXPAND_IGNORE_SIZE in World._setup_atmosphere_overlay(): azzera quella dimensione minima e lascia decidere agli anchor. Non è un'invenzione per l'occasione — è già la convenzione di ogni altro TextureRect creato a codice nel progetto (HUD, MainMenu, PixelWipe, Boot, LoadingVeil, OptionsPanel): la composita notturna era l'unica che se l'era dimenticata. La composita resta a mezza risoluzione: il guadagno fps di SU-581 bis non si tocca.
  • Riprodotto e curato con la misura, non a occhio: files/homeless_city/scripts/tools/probe_luci_sfalsate.gd (nuovo, runner tools/autotest/probe_luci_sfalsate.sh) scatta la stessa scena notturna con lo stesso seme e stampa gli spazi in gioco. Prima: view.size = 2144×1206 su canvas 1365×768, pozze sganciate, il barbone senza lanterna. Dopo: view.size = 1365×768, pozze sotto i lampioni e lanterna ai piedi del barbone. Scatti in TMP/luci_sfalsate/.
  • ⚠️ Provato anche il caso in cui il difetto NON c'era, per escludere di aver rotto proprio lì: finestra ridotta a caldo a 900×600 (canvas 1152×768, SubViewport 450×300, cioè lo scenario dove gli anchor già vincevano) — pozze allineate, e il ridimensionamento a caldo tiene.

SU-680: il magenta nella gattara, e perché la passata su tutti gli NPC l'aveva saltato2026-08-31

  • Segnalazione di Ivan in chat: «è la gattara ad avere problemi di taglio sullo sprite, in particolare sulla schiena col vestito viola, controlla tutte le varianti, come la gonna della nonnina». Il difetto è lo stesso di SU-635 (la nonna) su un altro personaggio: pixel del fondo magenta di chroma key rimasti DENTRO la sagoma, al posto del colore vero.
  • Trovati 16 residui sulle tre varianti, 14 interni e 2 di frangia. I sette di female_v01 stanno tutti nella riga ne — la vista di tre quarti da dietro, quella col vestito viola — a loc(19,12) e loc(20,13), le stesse due posizioni su quattro dei cinque frame: la firma di un difetto di pipeline sulla stessa piega del rig, non di una pennellata. Gli altri sono sul cesto: female_v02 2 px, drunk 5 px.
  • 📌 Perché SU-352, che ha ripulito 99 fogli, aveva mancato proprio questi. Quella passata non tocca mai un pixel magenta che ha almeno un vicino-8 anch'esso magenta, perché lo classifica «capo rosa disegnato apposta». Qui i residui vengono a coppie in diagonale: ognuno è il vicino magenta dell'altro, quindi si fanno scudo a vicenda e nessuno dei due risulta isolato. Secondo motivo, indipendente: la sua *forza magenta* min(R,B)−G vale 40–55 sul viola del vestito, cioè già sopra la soglia di 40 — su un personaggio vestito di viola quella soglia non separa niente (misurati 1.351 pixel sopra soglia in female_v01, di cui 1.343 sono vestito).
  • Il discriminante che funziona qui è il VERDE, e i dati sono bimodali senza sovrapposizione: verde 0–29 → 11 px (residuo), verde 30–69 → 25 px (pieghe scure del viola), verde 70–119 → 1.315 px (il vestito). Regola usata: alpha>8 AND verde<30 AND forza>45. È un insieme chiuso: nessun pixel con forza>70 ha verde ≥ 30, quindi non restano residui impastati fuori dalla rete.
  • ⚠️ Toccati solo i pixel INTERNI alla sagoma, i 2 di frangia lasciati apposta. Sono i due difetti opposti già distinti in SU-336 e SU-635: colore mangiato dal ritaglio (si ricolora) contro impasto del fondo nel contorno (rimedio diverso, e cancellarlo sfrangia la sagoma). Si separano col numero di vicini-8 opachi: ≥6 è interno. tools/su680_pulisci_gattara.py è un conta-modifica-riconta e non scrive se resta un residuo interno o se l'alpha si è mossa di un byte.
  • Provato in gioco, non solo sul foglio: il contact-sheet delle cinque direzioni per le tre varianti passa da 1.301 pixel di chroma a 0; alpha identica byte per byte sulle tre sheet, 14 pixel cambiati in tutto. Scatti in TMP/su680_gattara/su680_prima_dopo.png (le celle toccate, prima sopra e dopo sotto), gioco_prima.png e gioco_dopo.png.
  • 📌 Il "taglio" della sagoma invece NON c'è, ed è misurato. Ipotesi controllata perché il grezzo sprites_raw/PEOPLE/arch_catlady_v1_FEMALE.png è 1254 px = 5 × 250,8: la griglia nominale non è intera e le figure la sfondano in basso di 7 px (v01), 9 (v02), 16 (drunk). Ma la sagoma spedita combacia col grezzo intero (somiglianza 0,962–0,977) e non con quello tagliato sulla griglia (0,927–0,957), allo stesso livello della riga di controllo che non sfonda (0,979). L'estrazione ha usato i contorni veri della figura: non è stato perso niente.
  • ⚠️ Lo stesso difetto è su altri 87 fogli, e NON si ripulisce con questa regola: la scansione stretta trova 2.627 pixel su 90 fogli di 99, ma su chi ha un capo davvero rosa (rider 372–482 px, runner, influencer) la regola prende le pieghe scure del tessuto vero. Serve un discriminante per personaggio, non una soglia sola — resta fuori da questo ticket, che riguarda la gattara.

L'ENDLESS si compra una volta, ma si accende un quartiere alla volta2026-08-31

  • Cambio di regola chiesto da Ivan in chat: «l'endless si può sbloccare solo comprandolo con le lattine ma allo stesso tempo arrivando almeno una volta alla retata, ed è puntuale su ogni mappa: si compra una volta per tutte, ma bisogna prima per ciascuna mappa arrivare alla retata per sbloccare l'endless su quel quartiere». Prima bastava il cartello da 200 lattine in Baracca e valeva ovunque, subito.
  • Le due condizioni sono in AND, e la seconda si paga mappa per mappa: MetaProgress.endless_unlocked(qid) = endless_bought() (l'acquisto, globale e a vita) e raid_reached(qid) (il 30:00 già scoccato *su quel quartiere*). Il gate della partita, RunManager._endless_allowed(), non è cambiato di forma: chiede la stessa funzione, che ora risponde «qui» invece che «in generale».
  • Il timbro si mette all'ARRIVO della retata, non alla sopravvivenzaWorld._on_raid_started(), la stessa riga dove già si conta l'impresa NOTTEFONDA. Ivan ha chiesto «arrivare alla retata», non uscirne vivo: chi al 30:00 viene sgomberato in trenta secondi ha comunque acceso quel quartiere.
  • 📌 Nuovo dato persistente endless_maps in user://meta.cfg (id quartiere → true), accanto a shack e con la stessa natura: locale per giocatore anche in multiplayer, non viaggia in rete. Un meta.cfg scritto prima di oggi non ha la chiave, quindi chi aveva già comprato l'endless se lo ritrova spento dappertutto finché non torna a una retata per quartiere. È voluto e non si migra: siamo in beta, e una regola che vale per tutti tranne chi c'era già è peggio della regola stessa.
  • ⚠️ In multiplayer la dichiarazione dei client diventa «qui e ora»: World._srv_endless_unlock spediva «ho comprato l'endless», ora spedisce «l'endless mi vale su QUESTO quartiere». Continua a partire una volta sola perché il quartiere della partita è già fissato quando nasce lo snapshot (NetworkManager._cl_game_starting chiama Quartieri.set_current prima di caricare il World) e non cambia più fino a fine run. Resta la regola di prima: basta un peer scoperto e la retata arriva per tutti.
  • 📌 Il gemello endless NON timbra (World._on_endless_started_meta): là la retata non è arrivata affatto, e l'unico modo di passarci senza timbro è l'override di collaudo del DebugPanel — che non deve scrivere sblocchi veri sul salvataggio di chi sta provando. In cambio «Compra tutto» del pannello F1 ora regala anche i quartieri (debug_unlock_endless_maps), altrimenti consegnava un cartello che non fa niente da nessuna parte.
  • Il negozio lo dice, invece di lasciarlo scoprire in partita: la descrizione dell'upgrade in Baracca (BARACCA_ENDLESS_DESC, riscritta nelle 8 lingue) ora spiega che si accende un quartiere alla volta, e la riga info dell'upgrade già comprato non dice più «vale per ogni run, per sempre» come gli altri otto — elenca i quartieri dove è acceso e cosa manca per gli altri (MENU_BARACCA_ENDLESS_ATTIVO / _NESSUNO, nuove, 8 lingue). I nomi dei quartieri escono da Quartieri.nome(), quindi tradotti come in metropolitana.
  • ⚠️ Il primo giro di testi non ci stava nel cartone, e si è visto solo scattandolo: il riquadro info della Baracca è alto quattro righe scarse (panel_h * 0.14) e con descrizione più stato l'ENDLESS ne scriveva cinque — le ultime due uscivano dal pannello e finivano sopra la riga dei comandi. Rimedio in due mosse: descrizione accorciata (−35%) e, per l'upgrade già comprato, la descrizione non si ripete — resta il solo stato. Ricontrollati tutti e quattro gli stati, caso peggiore compreso (tutti e otto i quartieri accesi: 3 righe, dentro il cartone). Scatti in TMP/endless_quartiere/.
  • 📌 La pagina della classifica ENDLESS resta legata all'ACQUISTO (endless_bought()), non al quartiere in corso: la classifica endless è una sola per tutte le mappe, e chi ha comprato ma non è ancora arrivato a nessuna retata vede una pagina vuota — che è esattamente il suo stato.
  • Provato il bivio vero, non la funzione: probe_endless_per_quartiere.gd (nuova) porta RunManager.elapsed a un soffio dal 30:00 e chiama _advance(), poi legge la fase. 7 casi, 0 KO — compreso quello che è tutta la richiesta: cartello comprato, CENTRO timbrato, si passa al MERCATO e la retata arriva lo stesso.
  • ⚠️ Cosa NON è provato dal bot: l'aggancio in World._on_raid_started (una riga) e il giro multiplayer. Per arrivarci servono 30 minuti veri di partita — il TestBot muore molto prima — e due macchine. Letti a occhio, non collaudati.

SU-612: l'icona della scheda Play non si carica da Chrome, e ora c'e' il comando che la carica2026-08-31

  • Domanda di Ivan («task 612, lo puoi fare te da chrome?») e risposta misurata: no, da Chrome quella non si carica. La zona «Aggiungi asset» della scheda store non ha un <input type="file" nel DOM — verificato scandendo tutto il documento *e* gli shadow root: zero input file, zero shadow root. L'input lo crea il clic, e quel clic apre la finestra nativa del Mac, che blocca l'automazione del browser invece di accettare un file.
  • Confermato intanto il difetto del ticket, con le impronte invece che a occhio: l'icona sulla scheda ha sha256 4d465716… (il barbone col gatto), il file locale STORE_ASSETS/google_play/icon_512.png ha d22d7c7d… (il gatto che dorme di SU-367). Sono due immagini diverse, non due copie della stessa.
  • Fatto tools/release/play_icona.sh (nuovo): sostituisce l'icona della scheda via API androidpublisher, con la stessa credenziale gia' usata da play_upload.sh (~/.playconsole/service_account.json) — nessun account nuovo, nessun browser. --controlla confronta e non scrive; --invia fa il giro intero.
  • 📌 Prima di sostituire si salva l'icona che si sta buttando, in TMP/play_icona/, e non ci si fida del download: lo script ricalcola lo sha256 del file scaricato e lo confronta con quello che l'API dichiara, e si ferma se non combaciano. Senza quella verifica il «ripristino» sarebbe una copia di cui nessuno ha controllato la fedelta' (provato: con l'URL dell'API troncato a 90 caratteri si scarica una pagina d'errore HTML da 1,5 KB, salvata felicemente come .png).
  • 📌 La controprova sta PRIMA del commit, non dopo: caricato il PNG, lo script rilegge l'asset *dentro l'edit* e confronta lo sha256 col file locale; se non combacia scarta l'edit senza committare. Un edit mai committato Google lo butta da solo, quindi il passo costa nulla e toglie l'unico momento irreversibile del giro.
  • Verificato il lato Apple, che il ticket chiedeva e che non ha un'icona da caricare: la scheda App Store prende la 1024 dal binario piu' recente. Confrontate numericamente le icone *dentro la build* con l'asset di store — iOS/icon.png scarto 0,2/255 per canale, assets/icons_android/launcher_192.png 0,3/255: e' la stessa arte, dal commit 198b436 (SU-367). ⚠️ adaptive_foreground_432.png risulta lontano (48,7/255) ma non e' vecchio: un foreground adattivo ha lo sfondo trasparente e i margini di sicurezza, quindi confrontarlo con l'icona piena e' un paragone sbagliato, non una regressione.
  • Sostituita e in revisione: dopo il permesso, --invia ha chiuso il giro — sulla scheda lo sha256 e' ora d22d7c7d…, identico al file locale, e la console mostra il gatto che dorme. In «Panoramica della pubblicazione» l'unica modifica in coda e' «Schede dello Store → en-US → Modifica dell'icona dell'app»: nessun'altra, e Produzione resta «Non attivo» — cambiare l'icona della scheda non pubblica l'app, come chiesto da Ivan in chat.
  • ⚠️ Il 403 non era della API ma di Play Console, e la distinzione fa risparmiare un'ora: l'upload dell'immagine passava (HTTP 200), era il commit a rispondere PERMISSION_DENIED. Il service account play-publisher@… aveva le release sui canali di test e i gruppi di tester, ma non «Gestione della presenza nello Store» — la voce che testualmente «consente di modificare la tua scheda dello Store». Spuntata in Utenti e autorizzazioni, con l'ok di Ivan.
  • 📌 Spuntandola, Google ne accoppia d'ufficio una seconda: «Gestione delle dichiarazioni relative alle norme» si accende in grigio, non disattivabile — da 5 a 7 autorizzazioni. Non e' un errore di clic: le due vanno insieme, e chi guardera' l'elenco domani deve sapere perche' sono due.

I path scritti a Ivan: due cartelle `TMP/`, e un controllo che impedisce di mandarlo nella sbagliata2026-08-31

  • Segnalazione di Ivan, e non era il primo caso: «nei task, ad esempio 668 mi dici che i file sono in TMP/su668_squittio/ [sic] ma non vedo nessuna folder, va sistemata sta cosa perché è già capitata e mi perdo i pezzi».
  • 📌 La causa è strutturale, non un refuso: nel repo ci sono due cartelle TMP/ — quella alla radice, l'unica che Ivan apre, e files/homeless_city/TMP/. Scriverne una come «TMP/» manda nell'altra, che esiste: si guarda dentro, non c'è niente, e il pezzo è perso senza nemmeno un errore.
  • ⚠️ Le due TMP/ non si possono unificare, e va detto perché la soluzione non è spostare i file: gli scatti dei provini li scrive Godot, che salva solo dentro res://, quindi godot --path . -- TMP/x.png [sic] finisce per forza in files/homeless_city/TMP/ (hanno tutti il .import accanto). L'ambiguità è permanente: si toglie al momento di scrivere, non di generare.
  • Misurata la diffusione sui commenti dell'ultimo lotto (SU-657, 665, 668, 669, 670, 674), invece di correggere il solo caso segnalato: 8 path puntavano davvero alla TMP/ di radice, 3 a quella di Godot, 1 non esisteva affattoTMP/cane_A.png [sic], dove il nome vero è cane_A_22x16.png. Tre modi diversi di perdere un file, una sola causa: path ricostruito a mente invece che copiato da un ls.
  • Fatto tools/controlla_path.py (nuovo): legge il testo di un commento e per ogni path dice se è verificato, e se no propone quello giusto. Tre livelli, perché un controllo che grida al lupo non lo usa nessuno — AMBIGUO (manda in una cartella che esiste ma è l'altra: è il caso che fa perdere i pezzi) e NON ESISTE bloccano con uscita 1; incompleto (manca il prefisso ma non c'è ambiguità: scripts/ esiste in un posto solo) è solo rifinitura; assente e nota sono informativi.
  • 📌 I nomi sbagliati non li segnala e basta, li indovina: TMP/cane_A.png [sic] → «forse: TMP/cane_A_22x16.png», per confronto sfocato sui nomi dell'indice.
  • ⚠️ L'indice comprende i file NON tracciati (git ls-files --others + una scansione delle due TMP/): senza, sprites_raw/pigeon_pose_sheet.png risultava inesistente solo perché è LFS non ancora committato — un falso allarme che avrebbe fatto ignorare lo strumento.
  • Tolti quattro falsi positivi provandolo sui commenti veri, non su casi inventati: i res://… spezzati dal regex (usciva //assets/…), i riferimenti di codice tipo BonusLevel.gd:822 (ora nota, non errore), le date e le sigle scambiate per path (30/08, SU-664/665/667, su/medio/giu), e la prosa con la barra iniziale.
  • 📌 La marcatura [sic], trovata usando lo strumento su se stesso: le quattro rettifiche da postare venivano bloccate dal path che stavano rettificando, perché lo citano per forza. Ora [sic] subito dopo un path lo marca come citazione e lo salta — ed è anche la convenzione tipografica giusta, leggibile da Ivan senza spiegazioni.
  • Rettificati i quattro ticket dove il path mandava nel posto sbagliato: SU-657 (su657_shots/), SU-668 (su668_squittio/), SU-670 (il nome accorciato dei cani) e SU-674 (files/homeless_city/TMP/su674_anim/montaggio.png). Ogni commento dice dove sono davvero i file e che niente è andato perso.
  • Regola scritta in tre posti, perché valga anche fuori dalla chiusura ticket: CLAUDE.md (regole minime), punto 6 di .claude/skills/chiusura-ticket/SKILL.md, e la memoria di progetto. Corretto anche il punto 4 della skill, che insegnava a generare il provino in TMP/ senza avvertire che quel TMP/ è un altro.

SU-678 (DESIGN): le notifiche push si possono fare gratis, e il no di agosto era giusto allora ma non oggi2026-08-31

  • Ticket nato e svolto in giornata su richiesta di Ivan («brainstorming sulle push, deve essere gratis»): SU-678 (DESIGN), creato nello Sprint 12 e messo *In revisione*. Il design sta in DESIGN_NOTIFICHE_PUSH.md (nuovo, in root, accanto a DESIGN_CHIAVI_BETA.md). Nessuna riga di codice del gioco toccata, come impone il prefisso (DESIGN).
  • La raccomandazione: sì a FCM, gratis, con plugin nativo costruito in casa, invio dal Mac dentro /release. Piano Spark: nessun limite di messaggi, nessun costo per messaggio, nessun overage. Nessun account nuovo — Firebase si aggancia al progetto Google Cloud 933602919752, quello che già serve il login Google di NativeAuth.gd.
  • 📌 Il vero contributo del giro è aver riaperto SU-236 coi numeri, invece di ripeterne la conclusione. Lo spike del 2026-08-03 bocciò FCM per due motivi, e tutti e due sono scaduti: (1) diceva «i desktop sono la maggioranza dei tester» — misurato oggi sull'Excel, i tester sono 78, di cui 41 Android + 25 iPhone + 3 iPad = 69 mobile contro 19 desktop, cioè l'88% sta dove FCM arriva; (2) diceva «aggiungere un plugin nativo costa» — da allora il progetto ne ha scritti due, tools/android/plugin_su_auth/ (Java) e tools/ios/su_native_auth/ (Objective-C), con l'export Android già su gradle build custom e installa_template_android.sh che già patcha build.gradle con dipendenze e sorgenti. Il gesto non è nuovo: è lo stesso, con una dipendenza diversa.
  • ⚠️ La strada del plugin pronto NON esiste su 4.6, ed è il fatto che decide la forma della soluzione: GodotX Firebase (MIT, giugno 2026, iOS+Android, il più moderno) dichiara «built for Godot 4.7 or later» e nessuna retrocompatibilità; DrMoriarty/godot-firebase-cloudmessaging è marcato Godot 3 con 6 commit. O si costruisce, o si aspetta la 4.7.
  • 📌 Resta valida da SU-236 una sola insidia, e va ricordata: l'API FCM HTTP v1 pretende un token OAuth2 da service account, che dentro un .apk decompilabile sarebbe regalato a chiunque. Ma «mittente fidato» non vuol dire «server ospitato»: lo script di release sul Mac lo è, e il vincolo «niente server acceso» resta rispettato.
  • Due decisioni di design che evitano lavoro inutile. (1) Iscrizione per topic, mai per token: nessun registro di dispositivi da tenere, nessun dato personale in più — stessa filosofia di devices.json. (2) Al primo giro il topic è solo Android, perché su iOS TestFlight manda già da sé la notifica «nuova build»: duplicarla direbbe la stessa cosa due volte.
  • 📌 Il messaggio non dirà «aggiorna», e il motivo cambia la feature: su Android Play aggiorna da solo, quindi il tester ha spesso già la versione nuova e non lo sa. È una funzione di richiamo («c'è roba nuova, torna a vedere»), non di distribuzione — e il testo va nel tono del gioco, non in quello del sistema operativo.
  • ⚠️ Cosa NON è stato provato, ed è esattamente il primo ticket della sequenza: che firebase-messaging compili dentro il gradle build di *questo* progetto e che una notifica arrivi davvero a gioco chiuso. Il design lo mette come cancello: mezza giornata su Android soltanto, prova sull'Honor 10, e se fallisce si ripiega sul canale Telegram (un'ora di lavoro, unica strada che coprirebbe anche i 19 desktop) senza aver speso altro.
  • ⚠️ Le push toccano i moduli degli store, e vanno fatti PRIMA della release che le accende: il token FCM è un identificatore di dispositivo — va dichiarato in Data safety di Play, in App Privacy di App Store (più la capability *Push Notifications*) e nell'informativa privacy, dove Google si aggiunge come destinatario. Una build che manda un token non dichiarato è un rifiuto che arriva a valle.
  • Tre domande lasciate a Ivan in fondo al documento: iOS adesso o dopo, un topic solo o beta separata dal pubblico, e se aprire lo stesso il canale Telegram per i desktop (oggi 2 contatti Telegram su 78: il design non lo raccomanda, ma l'adozione la conosce lui).

Alessio Rotundo «Teppo» arruolato: due elenchi Android, messaggio consegnato2026-08-30

  • Riga nuova creata da zero: 80, ID 79, Alessio Rotundo, Nickname Teppo, axekrono89@gmail.com in Email e Utente Play Store, Android = Sì, +39 348 920 7886 in WhatsApp / Tel., Avvisa tester = Sì, Stato Inserito. Backup del foglio prima di scrivere in Beta_Tester_Homeless_City.BACKUP-2026-08-30-pre-rotundo.xlsx.
  • 📌 Saltato il messaggio di reclutamento: mail, piattaforma e numero li ha portati Ivan nella richiesta, che è la scorciatoia prevista dalla fase 1 di arruola-tester — si crea la riga e si va dritti alla registrazione. Nessun doppione sul foglio: l'unico «Alessio» in elenco è Alessio Fenoglio (riga 15, superfenix@hotmail.it), che è un'altra persona.
  • I due elenchi Android fatti tutti e due, che è la parte che si dimentica. Mailing list «Test Interno Street University Android» del canale Alpha da 40 a 41 indirizzi, e utenti di prova della schermata di consenso OAuth (axiomatic-path-505007-u7) da 52 a 53 su 100. Chi sta solo nel primo scarica la beta e poi viene respinto al login Google.
  • Verificati tutti e due ricaricando la pagina, non sul messaggio di salvataggio: sul canale Alpha la lista dice 41 e la casella del canale resta spuntata; sugli utenti di prova il contatore dice 53 e il filtro axekrono restituisce una riga sola, axekrono89@gmail.com.
  • 📌 Prima di aggiungere si è controllato in entrambi gli elenchi che non ci fosse già: letti tutti e 40 gli indirizzi della mailing list (non c'era), e filtro axekrono sugli utenti di prova → «Nessuna riga». ⚠️ Il controllo sulla mailing list è stato fatto leggendo l'elenco intero, non con find: su queste pagine find il 17/08 aveva inventato una corrispondenza.
  • 📌 Google non ha rifiutato l'indirizzo, che è la conferma indiretta che corrisponde a un account Google vero.
  • 📌 Controllato che il link porti a un gioco davvero installabile, che è l'equivalente Play del controllo IN_BETA_TESTING di TestFlight: il canale Alpha è Attivo con la 0.41 in stato «Disponibile per tester selezionati».
  • ⚠️ Alessio non risulta fra i contatti del bridge WhatsApp: search_contacts a vuoto su cognome, soprannome e numero. Nessuna chat pregressa, quindi il messaggio di conferma sarebbe il primo in assoluto su quel numero — e su un numero mai contattato il success: true del bridge non dimostra che quel numero sia su WhatsApp, come già successo con Gabriele Goria l'8 agosto (lì la consegna si verificò a parte, in sola lettura su web.whatsapp.com).
  • Mandato il messaggio 2 di messaggio_reclutamento_beta.md (conferma Android), pescato e non riscritto, col solo saluto cambiato in «Ciao Teppo!» — il suo Nickname. Anteprima approvata da Ivan prima dell'invio, come impone la regola non negoziabile della skill, che non si salta nemmeno per un testo già approvato in passato. Segnalata nell'anteprima anche l'assenza dal bridge, e Ivan ha scelto comunque il messaggio standard senza riga di presentazione. Stato InseritoInvitato.
  • Consegna verificata a parte, e qui la verifica serviva davvero: siccome su un numero mai contattato l'ack non basta, si è guardato web.whatsapp.com in sola lettura — l'unica eccezione ammessa alla regola «WhatsApp solo dall'MCP». Chat «Alessio Rotundo» in cima alle 21:25, doppia spunta, anteprima «Ciao Teppo! 🎉 Sei ufficialmente dentro il progra…». 📌 Il nome del contatto si risolve, quindi il numero è in rubrica: era il bridge a non averlo, non il telefono.
  • 📌 Il messaggio non si ritroverà in messages.db: il bridge non salva i propri invii (sendWhatsAppMessage non chiama StoreMessage), quindi cercarlo lì concluderebbe il falso.
  • 📌 Visto di passaggio sulla Play Console: le tre condizioni per chiedere la produzione sono ora tutte e tre spuntate, compresa «esegui il test chiuso con almeno 12 tester per almeno 14 giorni», e il pulsante «Richiedi per la produzione» è attivo.

SU-674, SU-670, SU-668: gli sprite non erano fermi per caso, lo erano per costruzione2026-08-30

  • Il KO di Ivan sui piccioni era la punta di un difetto sistemico: ogni sprite montato in questo sprint era una posa ferma su uno Sprite2D. Non una dimenticanza dell'arte — una scelta di montaggio ripetuta tre volte, sui piccioni, sui cani del BRANCO e sul ratto della metro. Questo giro la corregge in tutte e tre, arte e codice.
  • SU-674, difetto A — la misura. Il piccione montato era 24×24, contro i 9×8 del segnaposto che Ivan trovava proporzionato: tre volte piu' alto, quasi alla spalla del barbone (32). Rifatto a 12×10 a terra (17×10 in volo), la misura decisa da Ivan in chat. Il confronto alla stessa scala e' in TMP/su674_anim/montaggio.png, sezione 1.
  • SU-674, difetto B — il pivot. Pigeon.gd calcolava il pivot su MASCHERA_FERMO.size(), cioe' 8, che e' l'altezza del *segnaposto* e non della texture vera: a 24 px il piccione stava anche nel posto sbagliato. Adesso si legge dalla texture (_alt), misurato dalla sonda: 6.0, cioe' mezza cella, e non piu' 4.0.
  • SU-674, difetto C — il battito d'ali, che era una REGRESSIONE. Pigeon.gd:331-334 alternava _tex_fermo e _tex_volo per far battere le ali; con un PNG solo per variante le due texture erano lo stesso file, e il battito era sparito. Il segnaposto, che di pose ne aveva due, faceva meglio dell'arte vera. I due PNG sono ora fogli di 5 celle (18×12): a terra, beccata, volo su, volo medio, volo giu'. texture_per(dorato, in_volo) lascia il posto a pose(dorato), che ritorna le cinque celle; GoldenPigeon.gd segue.
  • 📌 La beccata adesso ha una posa sua, non solo lo spostamento in y: sull'arte vera lo spostamento da solo faceva ballonzolare un piccione impagliato.
  • ⚠️ Trovato per strada, e nessuno lo aveva visto: il PNG approvato guardava a sinistra, ma Pigeon.gd fa flip_h quando il piccione va a sinistra *dando per scontato uno sprite rivolto a destra*. Il piccione camminava all'indietro. Le pose nuove sono rivolte a destra.
  • SU-670 — i cani del BRANCO. dog_pack.png diventa una griglia 4×3 (4 pose di corsa per ciascuno dei tre cani, celle 22×16); _dog_texture(i) prende un secondo argomento frame, le pose si ritagliano una volta sola alla comparsa del branco e i tre cani partono da punti diversi del ciclo, altrimenti galoppano all'unisono come un corpo di ballo.
  • 📌 L'arte approvata NON e' stata rigenerata, come il ticket chiedeva: i frame sono derivati con PIL dai tre PNG che Ivan aveva gia' approvato — il frame 0 e' identico al pixel, si muovono solo le zampe (±1 px) e il corpo si alza di 1 px nella sospensione. Vale la regola di progetto «correzioni geometriche su arte gia' approvata: PIL, mai rigenerare». ⚠️ Un pixel e non due: a 22×16 due staccherebbero la zampa dall'anca.
  • ⚠️ Difetto della prima versione dei cani, e si perdeva in silenzio: alcuni toccano il bordo della cella, e la traslazione delle zampe li spingeva fuori — una zampa tagliata via senza un errore. I cani vengono ora centrati nella cella, con una sentinella che fa fallire lo script se un solo pixel esce.
  • SU-668 — il ratto della metro, che prima non esisteva. Al suo posto c'era un rettangolo grigio con lo spigolo magenta. Ora assets/sprites/world/subway_rat.png e' una griglia 4×2: quattro pose di camminata per due varianti (tozzo grigio, magro bruno), celle 26×14. Si alternano una si' e una no, cosi' Ivan le vede tutte e due in gioco come aveva chiesto.
  • ⚠️ Il flip_v del topo e' stato TOLTO, e il commento che lo giustificava era sbagliato. Diceva «il topo guarda dove sta andando: scende o sale, e lo sprite si specchia» — che ha senso solo per un ratto visto dall'alto. Provato a disegnarlo cosi' per tre giri: a questa misura una vista zenitale non si legge come un ratto (usciva prima uno scarabeo, poi una formica). Il ratto e' di profilo, come cani e piccioni, ed e' anche l'orientamento che il rettangolo di ripiego (22×12 orizzontale) dava gia' per scontato. Il verso lo da' ora flip_h.
  • 📌 La variante del ratto si sceglie sul CONTATORE dei topi nati, non pescandola da _rng_topi: una pescata in piu' li' avrebbe spostato di un tiro tutti gli scatti successivi, e con essi le run a seme.
  • Tre controlli di misura nuovi, tutti per lo stesso motivo: in Godot una region fuori dall'immagine non e' un errore — esce uno sprite trasparente e non lo dice nessuno. Pigeon.pose(), _dog_texture() e _pose_topo() verificano che il foglio abbia la misura attesa prima di ritagliarlo, e altrimenti ripiegano sul segnaposto, che invece si vede.
  • SU-668 — lo SQUITTIO del ratto, che il ticket chiedeva e non c'era. BonusLevel.gd aveva perfino il segnaposto scritto in _inciampa(): «⚠️ SU-668 — manca invece lo SQUITTIO». Generato con ElevenLabs (tre varianti in TMP/su668_squittio/, montata la A: squittio singolo), normalizzato a −20 dB RMS come avviso e treno del tunnel, silenzio iniziale tagliato perche' parte sull'inciampo.
  • 📌 Suona sull'INCIAMPO e non alla nascita del topo: un topo che squittisce appena nasce lo farebbe a ogni traversata, comprese quelle che non toccano nessuno, e il corridoio diventerebbe una gabbia di roditori.
  • ⚠️ Il pitch dello squittio NON viene da randf_range(), che pure era la prima scrittura: quella pesca dal generatore globale, e una pescata in piu' li' avrebbe spostato di un tiro tutto cio' che viene dopo in una run a seme, sonde comprese. Lo scarto viene dal contatore degli inciampi (0,94 / 1,00 / 1,06) — la stessa trappola gia' documentata accanto ai cani del BRANCO.
  • ⚠️ I file grezzi di ElevenLabs escono in PCM con estensione .mp3, e Godot ci si rompe sopra a ogni --import («Failed to decode mp3 file») pur non essendo asset del gioco. Rinominati .pcm_grezzo in TMP.
  • Piano ElevenLabs verificato via API (Starter attivo, 78 caratteri su 39.851): Ivan ha aggiunto il permesso user_read alla chiave, che prima rispondeva 401. Lo squittio nasce quindi coperto da licenza commerciale, che era il motivo per cui SU-671 lo aveva rimandato.
  • Generatori in tools/su674_pose_piccione.py, tools/su670_pose_cani.py, tools/su668_pose_ratto.py. Sorgenti in sprites_raw/ (LFS, non committati).
  • Provato con TMP/probe_su674_anim.gd: 20 controlli, 0 KO — pose tutte diverse fra loro, pivot a 6.0, piccione che cambia posa a terra e batte le ali con 3 pose in volo, i tre cani che muovono le zampe in partita, i topi che cambiano posa attraversando, nessuno capovolto, entrambe le varianti in campo. Compile-check: 4 controllati, 0 falliti.
  • ⚠️ NON provato: l'ASPETTO. Se i piccioni a 12×10 piacciano, e se il dorato si stacchi abbastanza, e' una scelta di Ivan — il montaggio e' in TMP/su674_anim/montaggio.png. Il piano B che aveva gia' dettato (piccione a 9×8 esatti e quello d'oro piu' grosso) resta sul ticket, pronto se 12×10 non convince.

SU-671: IL BRANCO abbaia davvero, e il fischietto si vede2026-08-30

  • L'abbaio scelto da Ivan e' il corale A (2,00 s, mono 44,1 kHz), posato in assets/sounds/super_branco.wav e importato. Da qui in avanti SuperPower lo prende da solo: il ripiego su throw.wav, che ha tenuto in piedi IL BRANCO da SU-669 in poi, non entra piu' in gioco. Zero righe di codice, come prometteva il commento nel file.
  • ⚠️ Un .wav copiato non basta: senza il .import Godot non lo vede e il gioco resta sul ripiego *senza dire niente*. Il compile-check non reimporta, quindi l'import va lanciato a mano (--headless --import) — ed e' esattamente il tipo di modifica che «sembra non funzionare» quando manca.
  • Rifatta l'icona 16 (FISCHIETTO PER CANI), su richiesta di Ivan. La prima versione era corretta ma fuori stile: sottile, grigia, 49 pixel opachi su 256, accanto a icone piene e sature. Ora 148 pixel opachi, oro con contorno scuro continuo, bocchino a sinistra col riflesso e cordino rosso in diagonale. 📌 La forma e' la parte difficile a 16 px: la prima riscrittura, tutta pancia tonda, leggeva come un sacchetto d'oro — il bocchino che sporge e la fessura scura sul dorso sono cio' che la fa leggere come fischietto.
  • Le tre varianti generate restano in TMP/ (super_branco_A_corale/B_secco/C_comico.wav): non si buttano finche' Ivan non l'ha sentito in gioco.

SU-674 e SU-670: i piccioni, i tre cani e le due icone che il foglio non aveva2026-08-30

  • Montati gli sprite approvati da Ivan, verificati identici ai file che aveva guardato: pigeon.png e pigeon_gold.png (24×24), e dog_pack.png (66×16, i tre cani in tre celle da 22×16). Zero codice per i piccioni: Pigeon.gd:83-84 e World.gd:5903 puntavano gia' a quei path.
  • 📌 I cani hanno cambiato il codice, e per una ragione precisa: DOG_TEX_PATH caricava un cane solo, ripetuto tre volte. Con tre cani diversi approvati, tenerlo cosi' avrebbe buttato due sprite su tre. SuperPower._dog_texture(i) ora ritaglia la cella con un AtlasTexture e _spawn_dogs() passa l'indice.
  • ⚠️ Le due correzioni al ticket erano vere, e la seconda ha salvato il montaggio: perk_icons.png NON aveva la «11a cella libera» (era 64×64, 4×4 tutte occupate) → allargato a 64×80 con una quinta riga; super_icons.png allungato in ALTEZZA a 80×48, mai in larghezza, perche' il ritaglio ha il 5 scritto nel codice — cosi' l'indice 10 gia' predisposto cade da solo al posto giusto.
  • Verificato che le icone esistenti non si siano spostate, misurandolo e non guardandolo: confronto pixel dei due fogli contro la versione in git → 0 pixel di differenza nella zona vecchia, su entrambi. Piu' lo scatto a schermo (TMP/su670_su674/montaggio.png), che il criterio del ticket chiedeva esplicitamente.
  • ⚠️ Bug trovato per strada, e sarebbe stato invisibile fino al primo giocatore: HUD._perk_icon_atlas() clampava l'indice a 0..15, numero fisso ereditato dal foglio 4×4. Il badge dell'HUD avrebbe mostrato OCCHIO AI FURTI (15) al posto del fischietto (16). Ora il limite si calcola dall'altezza vera del foglio. LevelUpScreen e SuperCeremony non avevano clamp; SuperPower.super_icon_atlas() aveva gia' un controllo dinamico e si e' acceso da solo.
  • PowerUpSystem.gd: fischietto_cani.icon da 4 (ripiego dichiarato nel commento) a 16, la cella vera.
  • Sorgenti approvati archiviati in sprites_raw/ (LFS, non committati come da regola): pigeon.png, pigeon_gold.png, pigeon_pose_sheet.png, dog_a/b/c.png.
  • ⚠️ NON provato: una run vera col TestBot — il fischietto pescato nel ventaglio e IL BRANCO attivato in gioco. Verificato il percorso diretto (indici e atlas) che alimenta le due schermate.

SU-657: la revoca del consenso esce dalla pausa, e il difetto con lei2026-08-30

  • Il KO di Ivan ha cambiato il lavoro: non piu' un fix dentro RunRecorder, ma una porta murata. «togliamolo dalle opzioni in game e teniamolo solo nelle opzioni del menu principale, cosi non puo' succedere a meta' partita».
  • OptionsPanel.gd:141 — la voce telemetria passa da in_game: true a in_game: false. Nessuna logica nuova: il filtro _voce_utilizzabile() esisteva gia' e faceva esattamente questo per RESET DATI, COMANDI, GRAFICA e CREDITI. Una riga, dentro un meccanismo che il file usava da sempre — ed e' il motivo per cui il pannello non resta con un buco.
  • 📌 Il fix di RunRecorder (5881f5a) resta a codice, per decisione esplicita di Ivan («lo terrei a codice e via»). Sono due strati, non due alternative: il KO chiude la falla nell'interfaccia, il fix la chiude nel motore e copre le strade che non passano dalle Opzioni — un logout, un cambio account, una voce che rientri domani. Toglierlo costerebbe piu' che tenerlo, e non ci si spende altro sopra: niente sonde nuove.
  • Provato a schermo, in due scatti e non uno: TMP/su657_shots/SU657_in_partita.png (sezione GIOCO con i soli SUGGERIMENTI e INDIETRO) e SU657_menu_principale.png (MANDA I DATI DI GIOCO: ON, con la data dell'ultimo invio). ⚠️ Il secondo non e' un di piu': senza, si prova di aver tolto la voce ma non di non averla tolta dappertutto.
  • Sonda nuova scripts/tools/shot_su657_opzioni.gd (col suo .uid), ricalcata su shot_su413_consenso.gd. Compile-check: 2 controllati, 0 falliti.
  • ⚠️ NON provato: la pausa aperta in una partita vera col TestBot. La sonda chiama costruisci() con la stessa API pubblica che usa HUD.gd, ma non passa dal gesto reale.

SU-610: il cactus usa i frame di Codex, e il codice non deforma piu' niente2026-08-29

  • Ivan ha scelto i SEI frame originali usciti da Codex («io l'animazione la voglio cosi'»), al posto dell'ondulazione che avevamo ricalcolato noi. assets/sprites/cactus_sombrero_anim.png, 288×48, 6 frame da 48.
  • 📌 Ne discende la cosa che conta, ed e' Ivan a dirla: «non si ondula nulla a codice ma si usano gli sprite di animazione». Cactus.gd ora fa una cosa sola — avanza il frame col tempo. Nessuna deformazione, nessun sin() sul corpo, nessuna rotazione: tutto il movimento e' disegnato dentro le immagini. E' il montaggio piu' semplice possibile, e anche il piu' facile da cambiare: si sostituisce il PNG e l'animazione cambia senza toccare una riga.
  • ⚠️ Cade il vincolo «le braccia non si muovono», ed e' giusto che cada: valeva finche' era Ivan a volerlo — era stato un suo KO — e guardando i frame veri ha cambiato idea. Nei sei frame di Codex si muove tutto, braccia comprese. 📌 Vale la pena ricordare quanto e' costato quel vincolo mentre era in piedi: tre giri di generazione con Codex che lo ignorava, e infine un'ondulazione calcolata a mano per garantirlo con la geometria. Il lavoro non e' sprecato — e' il modo in cui Ivan ha potuto vedere le due alternative e scegliere — ma e' un promemoria che su una resa visiva la strada corta e' fargliela vedere presto, non costruire la garanzia perfetta di un requisito che puo' cambiare.
  • Resta spento il dondolio delle braccia scritto in codice (ARM_SWING_*): sarebbe un secondo movimento sopra quello gia' disegnato nei frame. Il ripiego a mano resta per il caso in cui il PNG sparisse.
  • ⚠️ NON ancora visto in gioco dopo quest'ultimo montaggio: compile-check verde e import fatto, ma il provino non e' stato rilanciato — finestra d'uso al 96%.

SU-610: il cactus diventa uno sprite animato col sombrero, e il sole recupera le punte2026-08-29

  • Il cactus non è più disegnato a mano: assets/sprites/cactus_sombrero_8f.png, 8 frame di 48×48, montato in Cactus.gd. Ivan ha scelto solo la variante col sombrero («i cactus usa solo quello con il sombrero»); quella con gli occhi tonti resta committata ma inutilizzata.
  • 📌 Le braccia adesso stanno ferme, e non per taratura ma per GEOMETRIA. Era il requisito («evitando il movimento dei rami»), ed è il punto in cui Codex ha sbagliato tre volte su tre: gli è stato chiesto tre volte e tre volte ha animato anche le braccia. L'ondulazione è quindi calcolata, non generata — si deformano le sole righe di pixel *sopra* l'attacco delle braccia, con uno spostamento sinusoidale che cresce salendo. Misura: 0 byte di differenza nella metà bassa su tutti e 8 i frame, contro 1.251-1.332 byte nella metà alta.
  • Ne discende una cosa nel codice: col foglio montato il dondolio delle braccia si spegne (_spr_braccio_sx/dx non vengono nemmeno creati). Sovrapporlo sarebbe un secondo movimento sopra il primo, cioè esattamente il difetto già bocciato. Il tronco che ondula è ora l'unica animazione, su un ciclo di 2,4 s sfasato per cactus da _phase, così non respirano tutti all'unisono.
  • Il ripiego resta: se il PNG sparisse, Cactus.gd torna a disegnare tronco e braccia a mano come prima. È lo stesso gancio di SunHunter._texture_sole(), ed è il motivo per cui montare il sole non era costato una riga.
  • ⚠️ Riadattato il VisibleOnScreenNotifier2D: la sagoma è passata da 26×40 a 48×48, e lasciarlo su ART_W/ART_H avrebbe fatto smettere di pensare il cactus mentre una sua parte era ancora a schermo — cioè uno scatto visibile all'ingresso in inquadratura.
  • SU-611 — il sole aveva le punte di sinistra tagliate (KO di Ivan, provato in gioco). ⚠️ Colpa del ritaglio, non del disegno: il foglio delle quattro varianti era stato diviso in colonne uguali da 495 px, ma il sole D sta da x=1444 a 1939. I primi 41 pixel — cioè le punte sinistre — finivano nella cella della variante C, dove venivano poi scartati come «frammenti staccati» dalla pulizia. Ora D si ritaglia dai suoi confini reali: sagoma 495×508 invece di 454×508, e la prova di simmetria dà 59 pixel opachi a sinistra contro 58 a destra (prima la sinistra era vuota).
  • 📌 La lezione, che vale oltre questo caso: la pulizia automatica dei frammenti è utile ma non sa distinguere un artefatto da un pezzo del soggetto finito nella cella sbagliata. Quando taglia qualcosa, il conto va guardato — su C ne aveva tolti 22, ed era il segnale che il ritaglio a colonne uguali non teneva.
  • Verificato in gioco con probe_su611.sh a SOLLEONE (tablet): i cactus col sombrero compaiono nel parco e il sole ha le punte su tutti i lati. KO totali: 0 — e il KO «E) il gatto muore sul cactus» che il giro prima falliva ora passa (morente=true, caduta 180 px). Compile-check 1 file, 0 falliti.

La dieta dei token diventa regola: misurate due settimane di transcript2026-08-29

  • Analisi di 70 sessioni + 242 subagent (15–29/08): 6,13 miliardi di token grezzi (~4.146 $ equivalenti API), 67% thread principale / 33% subagent. ⚠️ Il 98% dei token è rilettura di cache del contesto: il costo non sono le deleghe (sprechi veri ~4% del loro totale) ma *contesto × richieste* — la sessione più cara (401 $ eq.) erano tre lavori diversi tenuti per due giorni nella stessa conversazione a 525K di contesto medio.
  • Nuova sezione in CLAUDE.md («Subagent e dieta del contesto»): dev-sonnet come builder di default e architetto-opus solo per lavori profondi/multi-file, ticket (DESIGN) mai ad architetto, cambio di fase = sessione nuova, orchestratore ≤250–300K, lotto oltre ~150 richieste = brief da rifare. 📌 Taratura misurata: un builder medio = 14,5M grezzi, non 29M — la riga del dimensionamento onde è stata corretta (ce ne stanno il doppio).
  • Default globale di Claude Code: era claude-fable-5[1m] (tariffa doppia di Opus, finestra 1M che lascia gonfiare i contesti) — dall'esperimento del 25/08 catturava sessioni per sbaglio. Ora claude-opus-5; Fable e [1m] solo dichiarati.
  • Allowlist: aggiunto curl -K ~/.jira/curlrc ai permessi di progetto — nella finestra misurate ancora 479 scritture Jira via MCP (eco ~3K caratteri l'una) nonostante la regola del 24/08.
  • Plugin superpowers spento nel progetto (3 usi in due settimane contro ~2K token iniettati a ogni sessione); systematic-debugging, l'unica skill usata, copiata in .claude/skills/. Nota in ORCHESTRAZIONE §«Skill esterne».
  • MEMORY.md potato: 7 voci superate/storicizzate spostate in memory/ARCHIVIO.md (fuori dal contesto di sessione).
  • Dati e tabelle dell'analisi in files/homeless_city/TMP/analisi_token_20260829/.

SU-611: il sole di SOLLEONE diventa uno sprite (variante D, scelta da Ivan)2026-08-29

  • Il sole non e' piu' disegnato a codice: assets/sprites/sun_hunter.png (36×36) e' la variante D scelta da Ivan fra quattro generate con Codex, a partire da un riferimento che aveva gia' fatto generare lui — sole cattivo con occhiali da sole e ghigno.
  • 📌 Il montaggio non ha richiesto una riga di codice: SunHunter._texture_sole() aveva gia' il gancio — se il PNG esiste lo carica, altrimenti disegna il sole a mano. Il ripiego procedurale resta al suo posto e continua a funzionare se il file sparisse.
  • 📌 Le quattro varianti sono state giudicate DOPO la riduzione a 36×36, che e' la misura con cui il gioco le disegna, e non sul disegno grande. E' la differenza che ha deciso la scelta: a quella scala le varianti con molte punte corte si impastano — la C, nel ridurre, perdeva 22 pixel in frammenti staccati dalla sagoma. La D ha punte piu' lunghe e occhiali piu' larghi, che sono le due cose che sopravvivono alla riduzione.
  • Verificato in gioco, non nel codice: tools/autotest/probe_su611.sh a SOLLEONE, tablet — il sole compare sopra il barbone con occhiali e ghigno leggibili, e stacca sul fondo sabbia. 13 controlli OK della sonda (cerchio che non insegue, tettoia, raggio).
  • ⚠️ Un KO preesistente della sonda, NON causato da questo cambio (che non tocca codice): «E) il gatto muore sul cactus, il cactus resta in piedi» — morente=false, gatto rimosso ma senza la caduta attesa. Va guardato a parte.
  • ⚠️ Il wrapper vuole un path ASSOLUTO per la cartella degli scatti: con un path relativo la sonda stampa err=0 e gli scatti non si trovano da nessuna parte. Il default del wrapper e' assoluto proprio per questo.

SU-675 e SU-676: l'HUD avvisa del pericolo prima che sia tardi2026-08-29

  • Il pannello lampeggia quando fame o sonno sono in pericolo (SU-675), su tre gradini: SOFT ambra, ALARM rosso-arancio, CRITICO rosso in fase con le barre. La leva e' Control.self_modulate sul solo $Panel, zero nodi nuovi, zero shader, nessun overlay fullscreen — che era un criterio esplicito, visto che gli overlay sono gia' stati il collo di bottiglia mobile di questo progetto (23→34 fps sull'Honor quando furono tolti). Il calcolo si aggancia a quello che gia' gira ogni frame in _update_danger_overlay: nessun polling nuovo.
  • 📌 self_modulate e non modulate, e non e' un dettaglio: non si propaga ai figli, quindi il pannello si tinge tutto mentre barre e icone restano leggibili. Verificato guardando lo scatto ingrandito: il fondo passa da sabbia ad ambra a rosso, e dentro le tre barre restano rossa, gialla e verde acqua.
  • 📌 Il flash NON dice quale statistica sia bassa, ed e' voluto: quello lo dice la barra, che e' gia' li'. Il gradino combinato e' il peggiore fra fame ed energia (maxi()), il che risolve per costruzione il caso «entrambe basse»: una sola raffica anche se le due stat crollano insieme, invece di un pannello che impazzisce. Verificato a schermo — lo scatto 05 e' identico al 04.
  • In CRITICO pannello e barra lampeggiano garantitamente in fase perche' il pannello e' agganciato alla stessa onda sin(_danger_t * 4.5) che gia' fa lampeggiare le barre, non a un secondo cronometro che gli andrebbe dietro. Isteresi solo in uscita (55/30/15, gli stessi 5 punti di GameState.WARN_HYSTERESIS): mangiare o dormire spegne l'allarme senza bisogno di uno stato «sta mangiando» dedicato.
  • ⚠️ La taratura del design era sbagliata, e si e' visto solo misurando i pixel dello scatto invece di guardarlo. Il primo giro di colori — quelli scritti nel documento — era visivamente indistinguibile dal normale. L'ampiezza idle e' stata alzata da 0.42/0.62 a 0.70/0.88. 📌 E' la lezione che questo progetto ha gia' pagato piu' volte: «nel codice e' giusto» non e' «a schermo si vede», e la prima causa di KO qui e' la resa visiva. Misura oggettiva dopo la correzione, rispetto allo stato normale: SOFT 1.316 pixel diversi (delta medio 42), ALARM 2.198 (80), CRITICO 2.423 (110).
  • Il suggerimento della panchina (SU-676): una volta sola per run, al primo calo di energia sotto il 50%, e mai piu' dopo che il giocatore ha usato una panchina (flag su EventBus.bench_used). Chi gioca bene e non scende mai sotto il 50% non lo vede mai.
  • ⚠️ Deviazione dal documento di design, dichiarata: invece di allungare il testo esistente «Inizi a sentirti stanco» (soglia 55%), il suggerimento e' una notifica autonoma — altrimenti a fine partita sarebbero usciti due toast quasi identici a pochi secondi di distanza.
  • Solo aggiunte: 161 righe in HUD.gd, nessuna riga di codice esistente toccata o riscritta. Compile-check 2 file 0 falliti, audit emoji 0, 1 chiave nuova in 8 lingue. Scatti in TMP/su675_676/.
  • ⚠️ NON provato: il crossing naturale per decadimento reale (il provino forza le stat a mano), il multiplayer, e il playtest umano — cioe' se il flash serva davvero a chi gioca, che e' la domanda per cui il ticket esiste e a cui solo Ivan puo' rispondere.

SU-677: la grafica leggera esce davvero dal codice2026-08-29

  • 102 occorrenze in 22 file diventano 2, ed entrambe sono COMMENTI che spiegano la rimozione, non codice: World.gd:6435 (dice che una chiave low_graphics rimasta in un settings.cfg vecchio non fa danno) e DebugPanel.gd:1472. ⚠️ Il ticket chiedeva zero occorrenze: queste due restano di proposito, perche' sono esattamente il genere di commento che la nostra regola dice di non tagliare — evitano di ripagare una trappola gia' pagata. Se Ivan le vuole via, e' un sed.
  • 📌 Il punto che poteva rompere il gioco in silenzio era il content_scale_mode, ed e' stato MISURATO, non dedotto. Settings.apply_low_graphics() lo scriveva a CANVAS_ITEMS a interruttore spento — e da SU-228 spento era l'unico stato possibile — mentre project.godot:89 dichiara gia' window/stretch/mode="canvas_items". Cancellare la funzione lascia il valore identico, e la prova e' numerica: 1662×768 logici a 844×390 e 1024×768 a 1024×768, con content_scale_mode=1. 📌 Il confronto «prima» esisteva gia' senza saperlo: 1662×768 era scritto nel commento di shot_su367_menu.gd (SU-455, misurato quando la grafica leggera c'era ancora). E su227_check_cap e' stato ridotto a guardiano permanente di content_scale_mode == 1, cosi' la prova resta viva invece di essere un numero in un changelog.
  • 📌 Il salvataggio vecchio: provato, non dedotto. Piantato un settings.cfg con low_graphics=true piu' lingua="fr", fps_limit=120, fullscreen_enabled=false in una HOME isolata, e avviato due volte — perche' una HOME dedicata ma non svuotata passa solo al primo giro. Entrambe le volte lingua, fps e fullscreen sono stati letti correttamente (quindi il file e' stato letto davvero, che e' il controllo che rende la prova valida) e la chiave sconosciuta ignorata senza errore. In piu': save_settings() costruisce un ConfigFile nuovo, quindi la chiave morta sparisce dal file alla prima impostazione che il giocatore cambia.
  • ⚠️ Una premessa del brief era sbagliata, e correggerla ha evitato lavoro inutile in 8 lingue. MENU_OPT_INFO_GRAFICA_MOBILE («Apre GRAFICA LEGGERA») sembrava da correggere, ma la voce GRAFICA su mobile non esiste: OptionsPanel._ha_porta_grafica():1311 e MainMenu._ha_porta_grafica():3625 sono entrambe return not _is_mobile(), e quella chiave non e' letta da nessuna riga — era orfana da SU-228. Correggerla avrebbe prodotto una frase curata in otto lingue che nessuno mostra mai. Cancellata. Il CSV e' passato da 477 a 472 record, e le righe sono state tolte col modulo csv e non a mano: il file ha campi su piu' righe (853 righe fisiche per 477 record) e un taglio a righe l'avrebbe spaccato.
  • Due provini cancellati: su228_check_forzatura.gd (ordinato dal ticket: verificava che la grafica leggera non si potesse accendere, e senza il flag non ha piu' oggetto) e su222_shot_hud_phone.gd — non ordinato, ma il suo unico scopo era content_scale_mode=VIEWPORT: senza quello era una copia identica di su186_shot_world_live.gd.
  • ⚠️ Scartata l'alternativa comoda: tenere in vita _grafica_low_idx() in MainMenu.gd per non rompere i tre provini che la chiamavano. Sarebbe stato codice morto, e le chiamate sono menu.call("...")un errore che il compile-check non vede e che si scopre solo a provino gia' morto. Corretti i chiamanti.
  • COIN_DROP_GROUND_CAP_LOW finalmente cancellata (World.gd): era rimasta indietro nel lotto dei piccioni perche' non era orfana, e ora lo e' davvero — tolti i suoi 4 usi in DebugPanel.gd e in tre provini.
  • Prove: check_files.sh su 23 file dell'agente + 14 riverificati da me, 0 falliti; audit emoji 0; su227_check_cap 0 falliti; la schermata GRAFICA guardata nello scatto (3 voci, nessun buco dove stava la voce tolta).
  • ⚠️ NON MISURATO, e resta l'affermazione di Ivan: che l'Honor 10 giri a 30 fps e oltre — nessun telefono collegato, adb devices vuoto. E non provato su device il ramo mobile: su mobile la schermata GRAFICA ora non si apre piu' affatto, coerente ma non esercitato su Mac. Sette provini ridotti non sono stati rilanciati: compilano, ma girano su una run vera e non e' stato misurato che passino ancora.

SU-659 → SU-663: il punteggio si vede mentre lo guadagni2026-08-29

  • SU-659, il difetto che Ivan aveva visto con gli occhi: raccogliendo monete la voce PUNTI non si muoveva e poi saltava di colpo. Non era un errore di conteggio — add_money emette money_changed, non score_changed, quindi l'HUD muoveva i DOLLARI e lasciava fermi i punti. Ora ScoreSystem si aggancia ai soldi e riemette. Misurato: l'etichetta si muove 33-58 volte per run seguendo i soldi, dove prima era zero per costruzione.
  • 📌 Il popup dei soldi ha un segnale suo (score_drifted), separato da action_scored, e la ragione e' di prevenzione: NOTIFY_THRESHOLD oggi non lo legge nessuno — il toast da punteggio in gioco non esiste — ma con due canali distinti, il giorno che nascera' non potra' essere svegliato da un forziere da $90. I popup sono raggruppati su 0,35 s (l'aspirazione della retata farebbe nascere un Label per moneta), mentre l'etichetta si muove a ogni singola moneta, che e' il criterio del ticket.
  • SU-661, le lattine a 6 punti: agganciate a CanDrop, non ai quattro chiamanti. Uno dei quattro punti di raccolta sta in BonusRewards.gd, che era di un altro lotto e non si poteva toccare. La strada trovata: CanDrop.drop_cans_reward() e' la porta di forziere e bidone d'oro, CanDrop.accredita_meta() e' l'unica strada del livello bonus — emettendo da li' il tunnel paga i suoi punti senza toccare quel file, e nessun punto resta scoperto. Solo la notte fuori non passa da CanDrop e costa 4 righe in GameState. ⚠️ Scartato l'aggancio a MetaProgress.add_lattine(), che e' per-oggetto e avrebbe riaperto la trappola dello split: lo stesso premio sarebbe valso da 1 a 5 volte.
  • SU-660 (soldi in tasca, 1 punto per dollaro, la banca NON conta) e SU-662 (giullare 300 punti, segnale nuovo jester_defeated, unico nemico che da' punti all'abbattimento).
  • ⚠️ UNA TRAPPOLA NUOVA, e costa cara perche' il compile-check non la vede: in una static func l'autoload non si nomina per simbolo. EventBus.cans_gained.emit() compila nell'editor e in gioco, ma sotto -s da' «Identifier not found: EventBus» e rende non compilabile l'intera classe CanDrop. Risolto passando dall'albero, come il file gia' faceva per MetaProgress. 📌 Lo stesso difetto latente esiste in CoinDrop.gd:256 e RewardScale.gd:124: preesistente, non toccato, ma ora e' scritto.
  • ⚠️ E un secondo difetto trovato solo perche' si e' guardata l'immagine: le due voci nuove spingevano «L'IMPRESA DEL GIORNO» fuori dalla pagina sul tablet, tagliata in silenzio. Due conseguenze: RACCOLTI $N → +2N pt ora sostituisce la riga RIMEDIATO $N invece di aggiungersi (una riga nuova sola, non due); e si e' corretto un difetto vero in RunReport._compute_geometry(), dove la stretta del giro di prova era scritta clampi(formula - _fit_squeeze, floor, tetto) e sulle pagine larghe il tetto se la mangiava — misurato: _fit_squeeze saliva 0→7 con _fs_body immobile a 31 e 51 px di traboccamento mai recuperati. Il commento del file dichiarava gia' l'intento giusto; ora lo fa anche il codice.
  • SU-663, il numero che il ticket chiedeva, ed e' una smentita utile. 4 run col bot: 1.322 / 1.375 / 1.413 / 1.512, componenti mediane azioni 763, soldi raccolti 112, in tasca 8, giorni 500, retata 0. La stima del design (2.800-3.300) non e' confermata: vale la meta'. ⚠️ Ma non e' il bilanciamento — e' il bot, che guadagna $39-71 contro i $400 del profilo «run onesta» e muore al giorno 2. Su un giocatore che arriva a $400 e 3 giorni il conto del design torna. E' esattamente il tipo di misura che a tavolino sarebbe stata data per buona nel verso sbagliato.
  • Verifiche: compile-check 9 file, 0 falliti; audit emoji 0; sonda deterministica probe_su663_voci.gd 22 controlli, 0 KO (4 eventi di lattine a 6 punti l'uno, 20 lattine contate come 3 eventi, giullare come voce distinta, banca a zero, tasca a 1). Scatti in TMP/su663_finale/: telefono e tablet mostrano RACCOLTI $137 → +274 pt, PUNTEGGIO 2791, IN TASCA $42 → +42 pt e l'impresa intera. 5 chiavi nuove in 8 lingue.
  • ⚠️ Da guardare, perche' e' una conseguenza voluta ma sorprendente: con SU-660 i soldi in tasca sono una componente viva, quindi comprare un hot dog ABBASSA il punteggio a schermo. E' coerente col ticket, ed e' la prima cosa che si nota davanti al banco. NON provato: il costo su Honor della riemissione a ogni moneta; i 300 punti del giullare solo con sonda (il bot non arriva a una carta di livello 9); il multiplayer — _announce_enemy_defeat() gira solo su host, quindi in MP i 300 punti li prende l'host anche se il giullare l'ha steso un client.

SU-664, SU-665, SU-667: i topi e lo zombie della metropolitana2026-08-29

  • I topi del corridoio (SU-664): Sprite2D mossi a mano dentro _passo() con contatto a distanza, senza fisica — la scelta che il livello aveva gia' fatto per monete e lattine, perche' il barbone di sotto non e' un Player e non ha corpo fisico. Un topo con la fisica sarebbe stata l'unica entita' fisica del tunnel e non avrebbe colliso con nulla. Tabella per giro 0/3/0/5/7/8 (il giro 1 insegna il livello, il 3 e' la pausa che lascia leggere lo zombie da solo), inciampo 0,6 s, tetto 3 per discesa.
  • 📌 Due scoperte hanno cambiato il progetto, e sono state trovate MISURANDO, non ragionando. (1) Il topo che attraversava appena nato dava zero contatti su cinque giri: il barbone arrivava 0,13 s tardi, sistematicamente — non a volte, sempre. Ora il topo aspetta al muro e scatta quando ti avvicini, e il tempismo lo decide il giocatore: e' quello che rende possibile SU-667. (2) Il posto d'attesa basso era a y 280, cioe' dentro la sagoma del treno (177,5-286,5): i topi morivano fermi prima di poter fare qualcosa. Al giro 6: 8 nati, 8 spazzati, zero inciampi — SU-667 era diventata la regola e SU-664 l'eccezione.
  • Il pendolare zombie (SU-665): uno solo sulla banchina dal giro 3, e resta uno anche ai giri alti. Abbraccio da 0,8 s, con una pausa dopo — ⚠️ senza quella pausa l'abbraccio e' una trappola, perche' appena finito lo zombie e' ancora addosso e ti riprende. Scartato farlo scendere dalle porte con la folla: avrebbe voluto dire mettere le mani sui 40 passeggeri tarati al fotogramma per guadagnarci un pendolare in meno.
  • Il topo spazzato dal treno (SU-667): BonusRewards.gd e' stato toccato solo per aprire una porta pubblica (lascia_lattina), cosi' la lattina del topo e' la *stessa cosa* delle altre — stesso array, stesso raggio, stesso accredito differito — invece di una seconda strada che si sarebbe scollata alla prima modifica.
  • I numeri, misurati e riproducibili (bash tools/autotest/probe_su664.sh): probe_su551_treni coi topi accesi da 5 giri su 5 in banchina, il piu' lento a 52,8 s sul limite di 60 → margine 7,2 s (erano 8,4 senza topi, il pavimento chiesto dal ticket e' 6,0). La sonda dei topi: tabella rispettata sui sei giri, inciampi 0/1/0/2/1/3 col tetto mai sforato, 0 topi vivi all'ingresso in banchina in tutti e sei i giri, zombie assente ai giri 1-2 e un solo nodo dal 3. 20 criteri fra le due sonde, 0 KO.
  • 📌 Il costo e' il MASSIMO delle due fasi, non la somma — corridoio +8 Sprite2D al giro 6, banchina +1 AnimatedSprite2D su 40 = +2,5% — e lo e' per costruzione, non per fortuna: _libera_topi() viene chiamata in _entra_in_banchina(). Il criterio «non si sommano sullo stesso schermo» e' cosi' garantito da una riga di codice invece che da un'osservazione.
  • Corretto il commento in testa a BonusLevel.gd che diceva che giro e' «quante volte il livello si e' aperto»: e' falso, e' «1 + quanti ne hai vinti». Una riga, ma e' la riga che aveva fatto sbagliare un giro intero del documento di design.
  • Una sola variabile di blocco (_bloccato_t) per tutti i nemici invece di una per tipo, cosi' _termina() non puo' dimenticarsene una. Compile-check: 3 file controllati, 0 falliti. Nessuna chiave i18n nuova.
  • ⚠️ NIENTE scatti e niente Honor 10: i due sprite sono segnaposto (rettangolo con spigolo magenta il topo, civile generico tinto di verde lo zombie), i path finali sono gia' nel codice e li prende da solo appena i file compaiono (SU-668). Manca anche lo squittio. E giro in una run vera resta NON MISURATO: e' il dato che dice se qualcuno vedra' mai la scala che parte al giro 3, e col bot non ci si arriva — serve una sonda che forzi la banca.
  • ⚠️ Il tasso di inciampi misurato e' quello di un oracolo che schiva perfettamente: su un umano sara' piu' alto, e il tetto di 3 e' cio' che tiene il margine sopra i 6,0 s per costruzione.

SU-672 e SU-673: i piccioni per strada e il piccione d'oro2026-08-29

  • I piccioni normali (Pigeon.gd, nuovo): ~20 per citta' negli isolati PIAZZA/PARCO, raggio di spavento 70 px, gruppo dedicato "piccioni" fuori da "npcs". Sono Node2D leggeri — nessun Area2D, nessuna fisica — che spengono il _process fuori inquadratura col VisibleOnScreenNotifier2D, come fa gia' il cactus.
  • 📌 Il vincolo che decideva il ticket era il determinismo, ed e' stato MISURATO, non dichiarato. Quarto dado separato _rng_piccioni seedato seed_used ^ 0x91CC10, con lo schema gia' collaudato da _rng_luci, _rng_banchi e _rng_cactus. La verifica su 8 quartieri × 3 semi: 24/24 firme delle posizioni identiche fra DUE PROCESSI separati, e 24/24 firme della citta' con i piccioni esclusi identiche alla copia del progetto a HEAD — cioe' l'RNG condiviso non si e' spostato di un tiro e le citta' degli altri quartieri restano quelle di prima. E' la seconda verifica, quella che conta: senza, un dado nuovo puo' essere deterministico *per se'* e aver comunque spostato tutto il resto.
  • 📌 Dove si aggiunge una cosa nuova conta quanto il dado: lo spawn e' andato in coda a generate(), subito dopo i cactus, che e' l'unico punto in cui aggiungere roba non puo' spostare un pixel delle citta' gia' generate.
  • Costo a schermo, col numero che il ticket chiedeva: massimo 5 piccioni contemporanei, misurato facendo scorrere un riquadro di 410×307 px mondo su tutte le mappe; media 16,9 piccioni per citta'. Scartata la distribuzione «riempi gli isolati in ordine fino a 20», che con ~24 isolati aperti avrebbe lasciato mezza mappa senza piccioni: si usa una quota 20 / n_isolati che sotto 1 diventa probabilita'.
  • ⚠️ Un difetto trovato e corretto strada facendo, e non si sarebbe visto senza misurarlo: la fuga misurava 15 px. Il clamp dentro l'isolato collassava sul bordo, e in una piazzetta 96×96 il piccione sbatteva le ali restando fermo li'. Ora, se il tragitto dritto e' troppo corto, punta all'angolo piu' lontano da chi l'ha spaventato — 78-83 px misurati — e la durata del volo segue la distanza invece di essere fissa.
  • Il piccione d'oro (GoldenPigeon.gd, nuovo): compare ogni 150 s, resta 9 s, rende 15-25 $. E' uno per giocatore e locale — la chiamata sta fuori dal ramo if not _net_is_client — perche' uno solo per tutti creerebbe una gara in cui chi arriva secondo ha inseguito per niente. Entita' separata e non una variante di Pigeon.gd: vita, fuga, cattura e ricompensa sono tutte sue.
  • 📌 Perche' 15-25 $ e non di piu': elemosina 1-5, busking 2-6 a donatore, forziere normale 18-42, bidone d'oro 100-130. Il piccione sta sotto il forziere normale. In una run da 10-15' compare 4-6 volte e, prendendolo sempre — improbabile, sono 9 secondi mentre scappa — frutta 60-150 $. Il freno e' tenerlo raro e di valore basso, non un tetto: un tetto si nota e sa di finto.
  • La scia arcobaleno riusa RainbowArc.gd come puro motore di disegno, alimentato da un anello di 16 punti campionati ogni 0,06 s e non ogni frame, perche' imposta() fa queue_redraw e la scia sono qualche centinaio di draw_rect.
  • ⚠️ Una riga di log che il collaudo ha gia' scambiato una volta per una regressione: la cache statica delle texture lasciava 2 resources still in use at exit, ed e' esattamente la riga che il collaudo di SU-513 aveva interpretato male. Le due texture sono state spostate sull'istanza (~14 KB per citta') — contro lo stile di Cactus.gd, che quella riga ce l'ha ancora.
  • SU-677 (pulizia grafica leggera) avanza di due occorrenze in World.gd. ⚠️ Resta PARZIALE di proposito: COIN_DROP_GROUND_CAP_LOW non e' stata cancellata perche' non e' orfana — la leggono DebugPanel.gd:1473 e tre provini, file di un altro lotto. Va tolta insieme a loro.
  • Compile-check: 6 file controllati, 0 falliti. 1 chiave nuova in 8 lingue.
  • ⚠️ NIENTE e' stato visto a schermo: lo sprite e' un segnaposto disegnato a codice, e i path finali (assets/sprites/pigeon.png, pigeon_gold.png) sono gia' scritti e provati per primi — il giorno che SU-674 consegna l'arte non si tocca una riga. Il multiplayer vero non e' mai stato lanciato: l'invarianza e' dimostrata sulla generazione a due processi, non su due peer collegati. E sull'Honor il punto da misurare e' la scia: ~210 draw_rect per ridisegnata, 16 volte al secondo, per 9 secondi ogni 150.

SU-669: IL BRANCO, il super che chiama i cani2026-08-29

  • La carta e il super nuovi (fischietto_cani → IL BRANCO): durata 10 s, ricarica 100 s, raggio 180 px, 3 cani a schermo. Entra come 11ª carta del MAZZO A pescabile anche in single player — la 12ª riga della tabella, visto che il RADAR DI QUARTIERE resta solo-MP — sul percorso standard (livelli 1-9, maturazione, giullare, carta dorata, cerimonia, baptize()), senza un solo ramo nuovo.
  • 📌 Il pezzo che ha reso il ticket economico, ed era gia' scritto nel design: il super riusa PAROLA PER PAROLA il tick dell'AURA MEFITICA (_area_flee + _area_confuse_police ogni 0,4 s) con raggio proprio. Ne discende la cosa che conta davvero: quelle due chiamate passano gia' da _relay_areaWorld.net_super_area (kind flee/confuse), quindi zero RPC nuovi — verificato col conto, non dichiarato: grep -c "@rpc" da 0 prima e 0 dopo in entrambi i file, e World.gd non e' stato nemmeno aperto.
  • I tre cani sono FX puro e locale: un Node2D figlio del mondo con 3 Sprite2D che corrono su un'ellisse attorno al barbone. Niente pathfinding, niente collisioni, niente stato in rete, e a effetto finito spariscono. Scartata l'entita' persistente per il motivo del design: costo e rischio MP altissimi per un beneficio che dieci secondi di sprite a schermo danno lo stesso.
  • La gag coi gatti (SuperPower.gd:540): col gatto in braccio quando parte il branco, GameState.has_cat = false e il gatto schizza via. Non e' una riga qualunque — has_cat e' una proprieta' con setter che emette has_cat_changed, a cui World e' gia' agganciato: il rimpiazzo del gatto per strada e l'aggiornamento dell'HUD partono da soli.
  • ⚠️ Due premesse del ticket e del design erano SBAGLIATE, e si sono viste solo col foglio in mano. (1) La «11ª cella libera» di perk_icons.png NON esiste: il foglio e' 64×64, cioe' 4×4 = 16 celle tutte occupate (0-9 MAZZO A, 10-15 MAZZO B). Si prende in prestito la cella 4 (TROMBETTA, lo strumento a fiato piu' vicino a un fischietto), col precedente identico del RADAR che usa la 13 di OCCHIO AI FURTI. (2) Per le icone dei super il foglio non va allargato in LARGHEZZA: il ritaglio e' col = idx % 5 / row = idx / 5, con il 5 scritto nel codice — allargare a 6 colonne non sposterebbe nulla ma renderebbe la colonna nuova irraggiungibile, mentre aggiungere una terza riga (80×48) fa cadere l'indice 10 al posto giusto senza toccare una riga di codice ne' spostare le 10 icone esistenti. Entrambe le correzioni sono ora scritte su SU-670.
  • 📌 L'indice dell'icona e' gia' predisposto (cella 10) e NON produce un buco a schermo: super_icon_atlas() ora controlla che la cella stia dentro la texture e, se non ci sta, torna null — il chiamante ripiega sull'icona della carta, esattamente come fa gia' per la SCOREGGIA ATOMICA. Il giorno che il PNG cresce l'icona vera si accende da se'. Un buco sarebbe stato peggio di un ripiego, perche' sembra un bug e non una cosa mancante.
  • Il suono ripiega da solo su throw.wav finche' SU-671 non consegna l'abbaio: basta posare assets/sounds/super_branco.wav e si accende, senza rimettere le mani nella logica. ⚠️ SU-671 e' bloccato per una ragione di licenza, non di tempo: la chiave API di ElevenLabs non ha il permesso user_read, quindi non si puo' accertare se l'account sia abbonato — e le licenze degli asset AI seguono la generazione, non il download. Generare al buio vorrebbe dire rifare tutto da abbonati. Scritto sul ticket, serve una riga di Ivan.
  • Due sonde altrui sono state aggiornate perche' il ticket le ha rese false, non perche' fossero rotte: probe_su246_calamita.gd e probe_su362_sesto_senso.gd asserivano «10 carte in single player», che adesso sono 11 (e 12 in multiplayer). Il conteggio e' un'invariante di contorno, non il soggetto di quelle sonde: si aggiorna, non si toglie. Chi conta le carte guardi is_card_draftable(), mai la dimensione di MAZZO_A.
  • 5 chiavi nuove in 8 lingue (SUPER_BRANCO_NOME/NOTIFICA/GATTO in ui_gioco.csv, CARTA_FISCHIETTO_CANI_NOME e CARTA_EFF_FISCHIETTO_CANI in ui_meta.csv), audit emoji a 0.
  • ⚠️ Cosa resta aperto e vuole una decisione: la carta non ha effetto passivo ai livelli 1-8 — la riga sul ventaglio lo dice invece di inventare una percentuale. Serve scegliere fra una manopola vera e meno livelli, come si fece per il RADAR. E sui puppet MP la FX resta la nuvola di scoreggia (World._cl_super_fx ignora il kind): vale per tutti e dieci i super esistenti, non e' una regressione di questo ticket, ma sistemarlo richiede World.gd.
  • Compile-check: 4 file controllati, 0 falliti. NON provato a schermo: i cani sono un placeholder marrone 14×9 generato a runtime, quindi come si vedono in movimento, se 180 px si sentono e se il gatto che schizza via fa ridere sono giudizi che restano a Ivan.

SU-677: la grafica leggera esce anche dal codice2026-08-29

  • [doc] Creato SU-677 su decisione di Ivan in chat: *«grafica leggera possiamo poi toglierla anche dal codice a sto punto, l'honor dagli ultimi tuning gira fisso a 30fps ed oltre»*. La voce era gia' fuori dal menu da SU-228, che aveva lasciato il codice dormiente; ora cade anche la ragione per tenerlo.
  • L'inventario, misurato prima di scrivere il ticket: 102 occorrenze in 22 file58 nel codice di gioco (Settings.gd 24, MainMenu.gd 11, Player.gd 9, Interactable.gd 4, ChurchHeart.gd 3, poi World.gd, DebugPanel.gd, OptionsPanel.gd, HUD.gd, RewardScale.gd), 44 nelle sonde, piu' 4 chiavi di traduzione per 8 lingue.
  • 📌 Il controllo che rende il ticket sicuro, fatto adesso e non lasciato a chi lo lavorera': tutti i rami sono puramente VISIVI. Il caso che sembrava pericoloso — RewardScale.gd:187, cioe' l'economia — non lo e': in grafica leggera azzerava solo la scossa della camera sulle monete. Gli altri sono scintille del forziere, glitter della banca, doppio effetto sul bagnato e una variante dell'effetto del cuore. Nessun ramo cambia il gioco, quindi la regola di rimozione e' una sola: si tiene sempre il ramo della grafica normale.
  • ⚠️ Tre trappole scritte nel ticket perche' non si scoprano a lavoro iniziato: *LIMITE FPS resta* (e' un'altra opzione, viva nel menu — qui sparisce solo LOW_GRAPHICS_FPS_CAP); su228_check_forzatura.gd e' la sonda che verifica *che la grafica leggera non si possa accendere* e col flag rimosso non ha piu' oggetto, quindi va tolta e non «aggiustata»; e apply_low_graphics() tocca il content_scale_mode della finestra, che e' il punto piu' delicato — un settings.cfg con la vecchia chiave salvata non deve rompere l'avvio.
  • 📌 La premessa del ticket va confermata, non data per buona: fra i criteri c'e' che l'Honor tenga davvero i 30 fps e oltre, misurato a telefono freddo e appaiato A-B-A. E' il dato di Ivan, e in questo progetto una misura a telefono caldo azzera qualunque confronto.

La grafica leggera esce dalle misure, e questo alza l'asticella invece di abbassarla2026-08-29

  • [doc] Deciso da Ivan in chat: la GRAFICA LEGGERA non si usa piu', nemmeno sull'Honor, e non va usata nelle implementazioni. Corretti l'unico ticket dei diciotto che la citava (SU-666, descrizione riscritta) e le quattro citazioni nel documento DESIGN_SU-624_topi_zombie.md.
  • ⚠️ La conseguenza e' il contrario di quella che sembra: la misura diventa piu' SEVERA. La grafica leggera cappava il gioco a 30 fps (Settings.LOW_GRAPHICS_FPS_CAP = 30, Settings.gd:522), cioe' 33,3 ms per frame. Senza quel cap il bersaglio torna a 16,7 ms — e l'indagine SU-226 del 2026-08-03 aveva gia' misurato che a fps pieni il 50% dei frame sfora i 16,7 ms anche senza topi ne' zombie. Il margine per i nemici nuovi e' quindi molto piu' stretto di quanto il documento assumesse.
  • 📌 Ne segue che il criterio «30 fps stabili» e' caduto, e non va riproposto: era una soglia che aveva senso solo col cap. Al suo posto, il budget dei nemici nuovi si giudica come differenza A-B-A fra la stessa scena con e senza di loro — corridoio con e senza gli 8 topi, banchina a 40 contro 41 AnimatedSprite2D. Se il divario sta dentro il rumore della misura passano, se si vede no. Misurare contro una soglia assoluta che il gioco non tiene comunque avrebbe dato un rosso inutile.
  • 📌 Il codice della grafica leggera NON e' stato toccato — Ivan ha detto «per ora» — e comunque era gia' fuori dal menu da SU-228, che l'ha rimossa lasciando il codice dormiente. Qui cambia solo cosa assumono i ticket e le misure, non cosa fa il gioco.

I cinque design diventano diciotto ticket di sviluppo2026-08-29

  • [doc] Creati SU-659 → SU-676 nello Sprint attivo, nati dalle decisioni prese oggi nei cinque documenti di design. SU-642 escluso su richiesta di Ivan (l'ha chiuso lui: il verdetto era «non serve», quindi non c'e' niente da implementare); i quattro ticket di codice del giro — SU-610, SU-611, SU-616, SU-619 — erano gia' implementati e non generano nulla.
  • Punteggio (da SU-628), 5 ticket: SU-659 il punteggio a schermo che segue i soldi — e' quello che chiude il KO, senza il resto resta invisibile come prima; SU-660 i soldi in tasca a 1 pt/$; SU-661 le lattine a 6 pt per evento; SU-662 il giullare a 300 pt con segnale dedicato; SU-663 il giornale di fine partita piu' la misura col bot che sostituisce la stima a tavolino.
  • Metropolitana (da SU-624), 5 ticket: SU-664 topi dal giro 2 (con dentro la correzione del commento sbagliato a BonusLevel.gd:41, la riga che aveva fatto sbagliare un giro intero del design); SU-665 il pendolare zombie dal giro 3; SU-666 la misura che blocca la taratura degli altri due; SU-667 il topo spazzato dal treno; SU-668 sprite e suono.
  • Cani (da SU-626), 3 ticket: SU-669 la carta e il super IL BRANCO; SU-670 sprite e icone — ⚠️ con dentro il lavoro vero che il design ha scoperto: super_icons.png e' 5×2 e tutte e dieci le celle sono occupate, serve allargare il foglio; SU-671 il verso dei cani, che ripiega su throw.wav finche' non c'e', per non bloccare il super.
  • Piccioni (da SU-625), 3 ticket: SU-672 i piccioni normali col dado separato _rng_piccioni; SU-673 il piccione d'oro con notifica e scia; SU-674 gli sprite.
  • HUD (da SU-617), 2 ticket: SU-675 il pannello che lampeggia a tre gradini via self_modulate, col divieto esplicito di overlay fullscreen scritto nei criteri; SU-676 il suggerimento della panchina, solo per il sonno.
  • 📌 Le tre generazioni di sprite sono ticket a se' (SU-668, SU-670, SU-674), come vuole il flusso del progetto: l'approvazione delle varianti e' di Ivan e non si mescola con l'implementazione che le monta.
  • 📌 Scritti in un file solo e creati in una corsa (jira_scrivi.sh --prova --sprint e poi senza --prova): 18 validati prima di mandarne uno, e in risposta solo le chiavi. Lunghezze fra 349 e 2.104 caratteri.
  • ⚠️ Trappola del formato, che vale per la prossima volta: nel corpo dei ticket il wiki markup Jira usa # per le liste numerate, e collide col # che separa i ticket nel file. Lo script ne contava 29 invece di 18. Rimedio: nel corpo si usano elenchi puntati *, e la riga di intestazione si riconosce perche' contiene i |.

RIENTRI ATTESI (messi In revisione il 2026-08-29, giro dei BOCCIATI): dieci chiavi — SU-610, SU-611, SU-616, SU-617, SU-619, SU-624, SU-625, SU-626, SU-628, SU-642. Al prossimo giro, prima dei lotti, si conta quante tornano in «Da fare» e con quale KO.

*(Giro precedente: zero rientri sulle dodici chiavi del referto telemetria — SU-646, SU-647, SU-648, SU-649, SU-650, SU-651, SU-652, SU-653, SU-654, SU-655, SU-656, SU-657: nessuna è tornata indietro, ma Ivan non le ha ancora provate su device, quindi il conto vero si fa al giro dopo. I dieci lavorati oggi vengono invece dal giro dei bocciati del 28-29/08.*

*⚠️ E il numero grezzo va letto, non solo contato: sei dei dieci — SU-617, SU-624, SU-625, SU-626, SU-628, SU-642 — erano documenti di design che finivano con una domanda aperta a Ivan. Il loro «KO» è la sua risposta, cioè il flusso che funziona, non un difetto nostro: è lo stesso schema già annotato il 21/08, quando 6 rientri su 34 finivano tutti con una domanda invece che con un errore. I rientri per difetto vero sono quattro: SU-616 (tazzina sbagliata: avevo scelto la variante senza che lui avesse visto lo screenshot), SU-611 (faccia del sole che leggeva allegra, raggio largo solo da spento), SU-610 (cactus fermi), SU-619 (mancava l'host, ed era una scelta esplicitamente lasciata a lui). Su quattro, tre sono difetti di resa visiva — che resta la prima causa di KO di questo progetto.)*

Il sole con gli occhiali da sole, e il raggio che ora riempie davvero il cerchio2026-08-29

  • [change] SU-611 (KO): il sole è arrabbiato, e porta gli occhiali da sole. Il KO diceva «il sole deve essere arrabbiato, il raggio più vistoso e largo (deve occupare tutta l'area target), rigenera lo sprite del sole», con allegato l'Angry Sun di Mario come spunto per l'espressione — da non ricopiare. ⚠️ Il primo tentativo è stato bocciato da Ivan in corsa: occhi a cuneo e bocca rettangolare leggevano ancora come faccia felice. La correzione è arrivata in chat a metà lavorazione — «fallo arrabbiato con gli occhiali da sole, stile lo stesso» — ed è stata girata al builder mentre lavorava: occhiali scuri a V (montatura alta all'esterno, bassa al centro) e bocca ad arco discendente coi denti, il contrario dello smile che c'era. Leggibili a 36 px, verificato ingrandendo lo scatto.
  • 📌 La diagnosi del raggio è il pezzo che vale: il raggio era già largo, ma solo da spento. largo cresceva con l'avanzamento mentre l'alfa del lampo calava (sin((1-_p)*PI*0.5)) — direzioni opposte, quindi la larghezza massima cadeva nell'istante in cui il fascio era ormai invisibile. Guardato a schermo sembrava stretto perché *era* stretto in tutti gli istanti in cui lo si vedeva. Rimedio: a = sin(_p*PI), una campana vera 0→1→0.
  • ⚠️ Il primo giro sul raggio l'ho respinto io, e la misura del builder diceva che andava bene. Il report riportava 91,2-100,8 px contro i 96 attesi, «combacia per costruzione». Ho ritagliato e contrastato lo scatto (TMP/ko_allegati/su611_raggio_zoom.png) e misurato sull'immagine: cono ~125 px contro i ~240 px del cerchio, cioè metà. La misura descriveva il picco teorico, non ciò che si vedeva — e il ticket era stato bocciato proprio su ciò che si vede.
  • [fix] Causa vera del secondo giro: draw_colored_polygon con l'antialiasing mangia qualche pixel al bordo, quindi largo = _raggio esatto rendeva un fascio leggibile all'85-93% del cerchio. Margine deliberato a _raggio + 6.0 (SunHunter.gd:371, +12,5%, solo grafico). La zona di danno non è cambiata: resta > TELEGRAPH_RADIUS esatto in _spara() (riga 247), riverificato col grep dopo la modifica.
  • [test] Sonda dedicata nuova, probe_su611_raggio_picco.gd, che misura sull'immagine invece di fidarsi della teoria: forza la fase RAGGIO, aspetta _t a metà di BEAM_SEC (il picco vero dell'alfa, non un numero di frame a caso), sopprime nel solo provino il flash bianco di play_hit_negative() — che nel giro precedente lavava lo scatto rendendolo non probante — e confronta la larghezza del fascio con TELEGRAPH_RADIUS·2·zoom letto dal motore. Esito: tablet 292 px su 240 attesi (121,7%), telefono 300 px (125,0%). Più larghi, non più stretti: coerente con «più largo» del KO.
  • 📌 Tre metodi di misura scartati prima di quello buono, e la cronaca è nei commenti del file: proiezione della camera calcolata a mano (imprecisa di ~35 px su telefono), e due ricerche dell'anello per colore contaminate una dai mattoni e dalle finestre, l'altra dagli occhiali scuri del sole appena aggiunti. Il metodo finale scandisce una fascia di righe (±35 px) attorno alla stima e tiene la larghezza massima.
  • [verifica] Nessuna regressione: la sonda condivisa probe_su611.sh rilanciata su tablet e telefono dà 0 KO su tutti i controlli (A/B/B2/C/D/C2/E/E2/F, inclusa «una vita per colpo»).
  • Non toccati: SOLE_LATO 36, ALTEZZA_SOLE 52 (a 74 il sole finiva dietro la fila delle vite), AIM_SEC 1,8 / WANDER_SEC 18 / BEAM_SEC 0,32, e la logica di _spara.

PixelRefiner provato davvero: il numero diceva di si', l'occhio ha detto di no2026-08-29

  • [doc] SU-642: la prova che mancava da due giri e' stata fatta. Ivan ha autorizzato il caricamento («caricali»), e lo strumento e' stato eseguito sul caso piu' difficile — arch_player/south.png, 108x108, 87% di colori unici.
  • ⚠️ Col preset di fabbrica lo strumento sbaglia, e per un motivo strutturale. «Best Match» classifica lo sprite come «Scaled pixel art, 64% confidence» e lo riduce a 12x19 px, irriconoscibile: PixelRefiner presume che l'input sia pixel art gia' upscalata di cui ritrovare la griglia, e i nostri sprite non lo sono — sono illustrazioni generate ad alta risoluzione. A suo merito non lo nasconde: scrive «Grid confidence is low» e propone griglie alternative. Ma un controllo umano immagine per immagine e' l'opposto di cio' che serve a una pipeline che gira su 99 sheet.
  • [doc] Col preset giusto («Detailed Pixel Art») il numero si ribalta: 21x35 px con 78 colori su 571 pixel visibili = rapporto 0,14, contro lo 0,87 della nostra pipeline LANCZOS+NEAREST a 32x32 e l'1,00 del suo stesso default (ogni pixel un colore diverso). Sei volte meglio, sulla metrica.
  • 📌 Poi si e' guardata l'immagine, e ha vinto l'occhio. Confronto affiancato in TMP/su642/CONFRONTO_SU642.png: la nostra 32x32 e' piu' pulita e leggibile — faccia con occhi e barba, tracolla, scarpe distinte dal cappotto —, mentre l'uscita di PixelRefiner e' rumorosa, costellata di pixel neri e chiari sparsi che sembrano sporcizia, col contorno frastagliato e la faccia confusa. Quei 78 colori sono distribuiti come rumore, non come tinte piatte.
  • 📌 La lezione, che vale oltre questo ticket: «pochi colori» non vuol dire «pixel art», vuol dire pochi colori. La pixel art vera vuole tinte piatte disposte con intenzione, e un filtro automatico non lo sa fare. E' lo stesso motivo per cui la risposta alla seconda domanda del ticket resta no.
  • RISPOSTA: non serve. Il risultato migliore ottenibile e' visivamente peggiore di quello che gia' produciamo, e per ottenerlo servirebbe un umano che sceglie il preset e verifica ogni immagine.
  • ⚠️ Non provati i casi B e C (riferimento grande, arredo): la prova si e' fermata al caso A perche' l'esito e' netto ed e' il caso rappresentativo — i personaggi sono cio' per cui lo strumento sarebbe servito.
  • ⚠️ Il ritrovamento che pesa su SU-608, ed e' peggio di come lo avevamo scritto: la rimozione della filigrana di Gemini non e' solo codice sepolto, e' un'opzione dell'interfaccia (gemini-watermark-removal, valori auto/off) e di fabbrica vale auto. Il README non la nomina. Quindi la riga di SU-608 non e' «non usiamo quel modulo» ma: passando un'immagine con le impostazioni di partenza, la cancellazione della filigrana puo' scattare da sola — chi lo adottasse senza saperlo cancellerebbe una marca di provenienza senza averlo scelto. E' il rischio esatto che SU-608 esiste per prevenire.
  • sprites_raw/ letto e mai modificato: si e' lavorato su copie in TMP/su642/.

I manifest pubblicati davvero, e su iOS l'avviso dice cosa fare invece di offrire un pulsante morto2026-08-29

  • [change] SU-619 completato con le due decisioni che Ivan ha dato in chat. Prima: i tre manifest sono pubblicati sul repo pubblico di BetaGate («scrivi», parole sue). version_dev.json, version_stage.json, version_live.json, versione 0.41, ognuno col proprio campo env. Verificati live sull'host vero: tutti e tre rispondono HTTP 200 con env e version giusti. Il 404 che il giro prima era l'unico esito possibile ora è un 200: il giro di rete è completo da capo a fondo, non più a metà.
  • [change] Su iOS l'avviso ora DICE cosa fare. Ivan: «su testflight l'invito ce l'hanno già se sono dentro l'app, devono solo aprire l'app testflight e aggiornare da lì». Quindi url_ios resta vuoto di proposito — non è una mancanza da colmare — e al suo posto compare una riga nel corpo dell'invito: «Apri TestFlight e aggiorna da lì: l'invito ce l'hai già». Chiave nuova AGGIORNA_TESTFLIGHT in tutte e 8 le lingue (ui_menu.csv), CSV riletto e validato a 9 colonne su tutte le righe.
  • 📌 La condizione è stretta apposta: OS.get_name() == "iOS" and _url_store.is_empty() (UpdateNotice.gd:432-441). Se un domani url_ios venisse riempito, la riga sparisce da sola senza che nessuno debba ricordarsi di toglierla. Il ramo «niente link → resta solo PIÙ TARDI» esisteva già ed era giusto: mancava solo l'informazione, non il pulsante.
  • ⚠️ [fix] L'audit delle emoji era KO, e non per colpa di questo giro: python3 tools/audit_emoji_pittogrammi.py segnalava 5 croci e 2 nei provini di SU-619 arrivati col commit precedente. Sostituite con KO — e SALTATA: testuali. Ora l'audit torna OK, zero emoji di sistema (71 pittogrammi coperti dalle icone pixel). È il tipo di cosa che passa inosservata perché sta nei provini e non nei testi di gioco — ma l'audit scandisce i .gd, e un audit rosso lasciato rosso smette di essere un segnale.
  • [doc] SU-624: Ivan ha approvato la deviazione — «proviamo come dici tu». Al giro 3 i topi restano in pausa per far leggere lo zombie da solo, come proposto: la scala definitiva è topi al giro 2, solo zombie al giro 3, insieme dal giro 4. Scritto sul ticket.

I cactus camminano a zig zag, e l'orologio che si fermava quando non guardavi2026-08-29

  • [change] SU-610 (KO): i cactus si muovono nel parco, senza mai seguire il giocatore. Richiesta di Ivan: «si devono muovere nel parco senza mai seguire il giocatore direttamente, sono animati (muovere il tronco come se il fondo fosse a zig zag) e le braccia devono fare un'animazione durante il movimento». Fatto: cammino a zig zag entro 24 px dal punto di nascita, tronco che si inclina seguendo il verso della gambata, e le braccia — ora due Sprite2D separati invece di un disegno unico — che dondolano alternate, una su e una giù, il gesto del «six seven» che aveva chiesto.
  • 📌 Zero lettura della posizione del giocatore: il percorso non ha alcun termine che dipenda da dove sta il barbone. Provato spostandolo altrove e rimisurando: il percorso non cambia.
  • ⚠️ Il difetto vero l'ha trovato l'agente da solo e valeva la segnalazione: l'orologio si fermava quando non guardavi. La prima versione muoveva il cactus con _walk_t += delta dentro _process — ma il _process è spento mentre il cactus è fuori inquadratura (ottimizzazione SU-226, che andava preservata). Quindi _walk_t non misurava il tempo di gioco: misurava per quanto quel peer aveva guardato quel cactus. Due giocatori che entrano nel parco in momenti diversi lo vedrebbero in punti diversi, e da lì il gatto muore sullo schermo di uno e non dell'altro — cioè salterebbe il criterio «cactus e morte del gatto identici su host e client».
  • [fix] Rimedio: la posizione non si integra più, si CALCOLA da DifficultyManager.elapsed (Cactus.gd:319), l'orologio di run che avanza per tutta la partita finché GameState.run_active (DifficultyManager.gd:118) e si azzera uguale per tutti (:135), più l'offset _phase derivato con un hash puro dal punto di nascita. Nessun randf() a runtime, nessun dado nuovo. _walk_t sopravvive solo per lo scuotimento del gatto e il dondolio delle braccia — estetica pura, non sposta la zona che punge.
  • 📌 L'ottimizzazione NON è stata sacrificata per ottenere questo: set_process(false) fuori campo e il VisibleOnScreenNotifier2D restano identici. Cambia solo *cosa* succede quando il _process gira: si legge un orologio invece di incrementare un contatore. Effetto voluto e verificato: il cactus continua a spostarsi mentre non lo guardi, e al rientro in vista lo trovi 16-22 px più in là invece che dove l'avevi lasciato — il mondo non si ferma alle spalle del giocatore.
  • [verifica] La prova che chiude il punto è una formula-oracolo: la stessa formula del gioco, riscritta fuori e confrontata con la posizione vera del nodo — scarto 0,00 px, sia appena entrato in campo sia lungo tutto il percorso. Più il congela-e-riprendi con due scatti («prima di uscire» / «dopo il rientro») e la controprova col giocatore teletrasportato. 9 controlli su 9 verdi su tablet e telefono, seme 4242.
  • [fix] Baco trovato per strada: _spina/_punto controllavano i limiti contro le costanti globali ART_W/ART_H invece delle dimensioni vere dell'immagine ricevuta — innocuo finché il canvas era uno solo, sarebbe andato in crash con i canvas piccoli dei bracci nuovi.
  • Cooldown del danno condiviso a 1,5 s, WorldGenerator.gd e il dado _rng_cactus non toccati: il determinismo della generazione, che era costato un giro, resta intatto.

Il pannello dell'HUD che lampeggia, e PixelRefiner fermo davanti a una decisione2026-08-29

  • [doc] SU-617 (KO): scartato il canale dei colori delle barre, progettato quello che Ivan ha indicato — lo sfondo e i bordi del pannello HUD. Il gesto e' a due tempi, ed e' il cuore dell'idea: il flash attira l'occhio e non dice quale statistica sia; il *quale* lo dice la barra, che e' gia' li'.
  • 📌 La scoperta che decide l'implementazione: il pannello uno sfondo ce l'ha gia', ma e' CONDIVISO. $Panel (scenes/ui/HUD.tscn:29-32) usa uno StyleBoxTexture via theme_type_variation = "CardboardPanel" (assets/ui/default_theme.tres:64-74), lo stesso di MainMenu, BetaGateScreen e Quaderno: ritingere quella risorsa li tingerebbe tutti. La leva pulita e' Control.self_modulate sul solo $Panel — tinge sfondo e bordo e non si propaga ai figli, quindi barre e icone restano leggibili. Zero nodi nuovi, zero shader, zero overlay fullscreen — e quest'ultimo non e' un dettaglio: gli overlay fullscreen «trasparenti» sono gia' stati il collo di bottiglia mobile in questo progetto (23→34 fps sull'Honor quando furono tolti).
  • [doc] Tre gradini, agganciati alle soglie che esistono gia' (GameState.WARN_LEVEL_SOFT 50, WARN_LEVEL_ALARM 25, STAT_DANGER 10) e all'evento stat_threshold_crossed: nessun polling nuovo. SOFT (50-25%) ambra (1.0, 0.82, 0.55), respiro 3,0 s, 2 battiti d'ingresso; ALARM (25-10%) rosso-arancio (1.0, 0.42, 0.30), respiro 1,8 s, 3 battiti con la stessa cadenza di _raid_warning_pulse (HUD.gd:4172-4189) — riuso dell'idioma, non del codice; CRITICO (≤10%) in fase con _danger_t, cosi' pannello e barra lampeggiano insieme invece di battere l'uno contro l'altra.
  • 📌 Si spegne da solo risalendo sopra soglia+isteresi: mangiare o dormire fa effetto senza bisogno di uno stato «sta mangiando» dedicato. E' la risposta al rischio «diventa fastidioso su una partita da 30 minuti».
  • 📌 Verificata a codice la differenza dagli altri due avvisi della stessa zona, e una delle tre e' una sorpresa: la polizia NON ha un canale visivo (_on_player_chased, HUD.gd:1184-1187, suona e basta). Restano tre grammatiche distinte, quindi il rischio di confusione e' minore di quanto il ticket temesse.
  • [doc] Aggiunta dalla telemetria, non dal gusto: un suggerimento una-tantum «vai su una panchina» al primo calo sotto il 50%, solo per il sonno. Il sonno e' la seconda causa di morte (28 su 99) e uccide presto — mediana 3:43 contro 7:44 della fame — e le panchine sono usate 1.835 volte, ma tardi: chi muore di sonno non ha ancora imparato a dormire. L'avviso serve nei primi 5 minuti, non nel finale.
  • [doc] SU-642: trovata la demo ospitata (pixel-refiner.app), quindi cade l'ostacolo «serve Node+pnpm» — per provarlo a mano basta un browser. Scelti e misurati i tre sprite difficili (personaggio 108x108 con 87% di colori unici e 6,4% di alfa parziale; riferimento 120x207 con 14.960 colori; arredo 84x54), copiati in TMP/su642/ accanto all'uscita della pipeline vera, che per i personaggi e' LANCZOS a 2x poi NEAREST a 32 (import_chatgpt_sprites.py, extract_and_resize), non BOX.
  • ⚠️ La prova pero' NON e' stata fatta, e il motivo e' sostanziale, non tecnico. Caricare i PNG sul sito e' stato fermato dal classificatore: e' un invio verso l'esterno. La via per aggirarlo esisteva (costruire il file in JavaScript dentro la pagina) ed e' stata scartata di proposito. 📌 E guardandolo bene l'intento del blocco coincide con quello del ticket: quegli sprite sono il materiale probatorio su cui SU-608 costruisce la provenienza dei nostri asset, e mandarli a un servizio di terzi — per giunta uno che contiene moduli per cancellare marche di provenienza — e' una decisione di Ivan, non una scorciatoia da prendere perche' «procedi in autonomia».
  • ⚠️ Il README di PixelRefiner NON nomina i moduli filigrana, che al giro scorso erano stati trovati nel codice. Cambia come va scritta la riga di SU-608: non basta «non usiamo quei moduli», va detto che lo strumento li contiene senza dichiararli.

Il branco di cani riusa l'aura mefitica, e il piccione d'oro vale meno di un forziere2026-08-29

  • [doc] SU-626 (KO): scelta la strada 3, il SUPER CHE CHIAMA I CANI. Le strade 1 (compagno persistente), 2 (cane-minaccia) e 4 (gag coi gatti) diventano scartate con una riga di motivo; la 4 pero' non sparisce, entra come dettaglio dentro la 3 — se il giocatore ha il gatto in braccio quando parte il branco, il gatto scappa (GameState.has_cat = false), nessun danno, coerente col vincolo «niente danno al compagno» che nel gioco non esiste.
  • 📌 Il super non e' un'entita' nuova: riusa parola per parola il tick dell'AURA MEFITICA (SuperPower.gd:557-563, _area_flee + _area_confuse_police), con nome, durata e raggio propri. Ne discende la cosa che conta davvero: il replay in multiplayer esiste gia' (net_super_area, kind flee/confuse, World.gd:4328-4374), quindi zero RPC nuovi. Numeri proposti da tarare: durata 10 s, ricarica 100 s, raggio 180 px, 3 cani a schermo, puro effetto visivo senza pathfinding.
  • ⚠️ Come si ottiene: serve una carta nuova, e il perche' e' misurato. Le 10 carte del MAZZO A (PowerUpSystem.gd:47-61) hanno gia' tutte un super assegnato in SuperPower.SUPERS (SuperPower.gd:126-166): non c'era uno slot libero da riusare, quindi il branco entra come 11ª carta, che pero' segue il flusso standard (livelli 1-9, maturazione, giullare, carta dorata, cerimonia, baptize()) senza aprire rami nuovi.
  • 📌 La domanda «cosa succede se il cane muore» e' dichiarata DECADUTA, non lasciata in sospeso: nella strada 3 i cani sono evocati e a tempo, non un'entita' con uno stato vivo/morto.
  • [doc] SU-625 (KO): confermata l'opzione C, e i numeri del piccione d'oro sono ancorati alle fonti di monete vere. Comparsa ogni 150 s, resta 9 s, rende 20 monete (range 15-25). Il confronto e' la parte che risponde al vincolo «non deve diventare la strada piu' conveniente»: elemosina $1-5 (NPC.gd:1920), busking $2-6 per donatore (NPC.gd:192-193), forziere normale $18-42 (ChestLoot.gd:182-183), bidone d'oro $100-130 (ChestLoot.gd:145-146). Il piccione sta sotto il forziere normale e molto sotto il bidone d'oro: il freno e' «raro e di valore basso», non un tetto — un tetto si nota e sa di finto.
  • [doc] La notifica chiesta da Ivan passa da EventBus.big_notify(), ~2,0 s, oro, e senza emoji (icona pixel dal font StreetU-Icons.ttf, come gia' si fa altrove).
  • [doc] I piccioni normali si popolano a caso ma senza toccare il seme condiviso: dado separato _rng_piccioni con lo stesso schema gia' collaudato da _rng_luci, _rng_banchi e _rng_cactus (WorldGenerator.gd:799-803), negli isolati PIAZZA/PARCO (35% dei blocchi), ~20 in tutta la citta'. La fuga del piccione e' dichiarata locale e cosmetica: non deve combaciare fra i peer, e cosi' non entra nel determinismo.
  • 📌 Tre citazioni di codice sbagliate corrette durante la stesura, nate da un intervallo sed scambiato per la numerazione vera del file: le righe citate nei documenti sono state riverificate una per una con grep -n.
  • Nessun file di gioco toccato: solo i due documenti in OPUS_BRIEFS/.

Quanto vale un dollaro in tasca, e la scala di topi e zombie2026-08-29

  • [doc] SU-628 (KO): chiuse tutte e quattro le domande aperte con le risposte di Ivan. I soldi in banca NON entrano nella voce di fine partita (sono considerati spesi: il livello bonus li raddoppia gia' e se ne raccolgono altri facendo punti); lattine ferme a 6 punti per evento («ti premiano gia' altrove»).
  • [doc] La voce B vale 1 punto per dollaro, ed e' una scelta delegata a noi («lascio scegliere a te», parole di Ivan). Il motivo e' aritmetico e non di gusto: il gioco ha gia' una progressione a scaglioni della banca (100/200/400/800/1600) che premia l'accumulo, e a 2 punti per dollaro la voce si somma a quel premio fino a rendere «non spendere» la mossa migliore. A 1 il conto non si ribalta mai — 200 $ tenuti in tasca fanno 200 punti contro i 500 di un giorno in piu' sopravvissuto.
  • [doc] Voce nuova su richiesta di Ivan: il giullare sconfitto, proposto a 300 punti — il doppio del peso piu' alto oggi in WEIGHTS (cat_police, 150), perche' «e' difficile arrivarci». ⚠️ Il numero e il meccanismo sono una nostra proposta, non una decisione di Ivan: lui ha chiesto «piu' punti» e «un evento a se' nello score», non un valore. Nel documento resta marcato come proposto. Verificato nel codice che oggi nessuno dei 6 ENEMY_ARCHETYPES da' punti all'abbattimento: per restare «evento a se'» serve un segnale nuovo (EventBus.jester_defeated) agganciato da ScoreSystem sul modello gia' usato per cat_police, non un contenitore generico.
  • 📌 Stima ricalcolata della partita tipo: ~2.800-3.300 punti (+300 col giullare), molto sotto TETTO_RUN = 500.000. Nessun rischio di sfondamento.
  • [doc] SU-624 (KO): scala decisa — topi dal giro 2, zombie dal giro 3, insieme dal giro 4. Gli zombie escono dallo status «fuori da SU-624» del giro precedente ed entrano nello scope, sulla banchina. Costo quantificato: +1 AnimatedSprite2D su 40, cioe' +2,5%, con un ripiego previsto se sfonda sull'Honor 10.
  • ⚠️ Una deviazione dalla lettera di Ivan, dichiarata e non nascosta: al giro 3 i topi vanno in pausa. Ivan ha scritto «topi dal giro 2», che si legge come «presenti dal 2 in poi». La scelta del documento e' isolare una novita' per volta — al giro 3 si legge lo zombie da solo, cosi' «insieme al giro 4» e' un evento distinto e non la somma di due cose gia' attive. E' difendibile ma non e' quello che lui ha scritto: decide lui.
  • 📌 Nota tecnica emersa strada facendo: topi (corridoio) e zombie (banchina) vivono in fasi sequenziali del livello (BonusLevel.gd:905, enum Fase), quindi non si sommano mai sullo stesso schermo — il «insieme al giro 4» e' insieme nello stesso livello, non nella stessa inquadratura.
  • Nessun file di gioco toccato: solo i due documenti in OPUS_BRIEFS/.

L'host dell'avviso di aggiornamento c'e', ma i manifest non sono ancora pubblicati2026-08-29

  • [change] SU-619 (KO): scelto l'host del manifest, ed e' quello che BetaGate usa gia'. Ivan in chat: «userei l'host di betagate che e' perfetto, scriviamo li e ciao, pero' lo farei per i tre ambienti distinti». Env.UPDATE_MANIFEST_BASE (Env.gd:118) era vuota per scelta — finche' lo era, la verifica non partiva nemmeno — e ora vale https://raw.githubusercontent.com/BlackwindITA/street-university-beta-devices/main/. La struttura a tre ambienti c'era gia': update_manifest_url() compone version_<env>.json da Env.id().
  • [verifica] Il buco vero del giro precedente era che IL GIRO SU RETE NON ESISTEVA — era simulato da file, e una lettura da file non prova TLS, redirect e timeout. Ora e' provato contro l'host vero: devices.json risponde HTTP/2 200 in 0,309 s, version_dev.json (non ancora pubblicato) HTTP/2 404 in 0,206 s.
  • 📌 Il 404 e' un caso da collaudare, non un fallimento: il criterio del ticket dice che se la verifica non riesce il gioco parte come se niente fosse. Provato contro il 404 vero, non simulato: [SU-619] nessun avviso: risposta non buona (result 0, http 404), nessun blocco. Provato anche il caso «200 valido ma non e' un manifest» puntando a devices.json: rifiutato in silenzio (schema ?, ne aspettavo 1). E la cintura dell'ambiente: manifest con env="live" letto da una build dev → rifiutato.
  • [test] Provino nuovo separato, su619_rete_vera_probe.gd (6 controlli, 0 KO): il vecchio prova la *logica* di UpdateNotice, il nuovo prova il *trasporto* con HTTPRequest reale. Tenerli separati evita di appesantire il vecchio con attese in tempo reale — in headless i frame corrono molto piu' veloci della rete, quindi l'attesa e' su Time.get_ticks_msec() e non su un conteggio di frame. La FASE 0 del vecchio provino (che testava «costante vuota») e' saltata con una nota, non cancellata e non lasciata a mentire: la sua premessa non e' piu' vera.
  • ⚠️ I TRE MANIFEST NON SONO PUBBLICATI, e finche' non lo sono l'avviso non comparira' mai. Sono generati e validi in TMP/su619_manifest/ (versione 0.41, che e' gia' rilasciata: tag v0.41), ma la scrittura sul repo pubblico e' un'azione verso l'esterno ed e' stata fermata dal classificatore di sicurezza. Serve l'ok esplicito di Ivan.
  • ⚠️ url_ios e' vuoto in tutti e tre: nel repo non esiste nessun link TestFlight o App Store. UpdateNotice._url_per_piattaforma() lo gestisce di proposito (:263-265, «se per questa piattaforma non c'e', l'invito compare lo stesso»), quindi su iOS l'avviso comparirebbe senza pulsante. Il criterio «il pulsante apre la scheda giusta per la piattaforma» resta soddisfatto solo su Android.
  • ⚠️ Effetto collaterale da sapere: con la costante piena, ogni avvio fa ora una richiesta HTTPS vera (timeout 5 s, fail-open). Finche' i manifest non sono pubblicati risponde sempre 404 — verificato pulito, ma e' un giro di rete a ogni boot che prima non c'era.

La tazzina che voleva Ivan, e il contorno che la riduzione si mangiava2026-08-29

  • [fix] SU-616 (KO): sostituita la tazzina del caffè con quella che Ivan ha allegato al ticket. Il giro precedente aveva scelto «quella della fila in stile gioco» senza che lui avesse visto lo screenshot; il suo KO («la tazzina deve essere l'altra») è arrivato insieme all'allegato raw_tazzina.png (1060×1484, fondo magenta), che è una delle varianti generate da noi la sera prima e che Ivan si è ripreso da Jira.
  • 📌 La variante pixel art già ridotta non esiste più, e Ivan lo ha confermato in chat («l'avevi generata nei temp ma non la ritrovo più»). Cercata: nelle otto immagini rimaste in ~/.codex/generated_images/ del 28/08 nessun md5 corrisponde all'allegato, e in TMP/sprites_raw/ITEMS_temp/ ci sono solo le tazzine di SU-193, di luglio. Ricostruita dal raw invece di rigenerarla: stesso punto di partenza, nessun giro di generazione speso.
  • ⚠️ La riduzione nuda perdeva il contorno nero, ed è il pezzo che tiene lo stile. Il raw ha un contorno di ~20 px che a 1010→20 px di larghezza (fattore 50,5) si riduce a 0,4 px, cioè sparisce: affiancata alla tazzina vecchia la nuova risultava «slavata», senza il peso da pixel art del resto del gioco. Rimedio: soggetto ridotto a 18×26 con BOX + alfa binaria (soglia 110), centrato in 20×28, e contorno ridisegnato a 1 px pieno dilatando l'alfa (MaxFilter(3)) e colorando la corona di (12,10,18).
  • 📌 La dimensione del PNG non cambia la resa a schermo, e per questo è rimasta 20×28: Player._fx_unit_size_mul() (Player.gd:3449-3452) normalizza a 16.0 / larghezza, quindi la larghezza in gioco è fissa qualunque sia la texture. Alzare a 32×44 avrebbe dato alla sola tazzina pixel più fini di tutto il resto del gioco — più dettaglio, meno coerenza. Il rapporto d'aspetto del raw (0,714) coincide con quello dello sprite vecchio (20/28 = 0,714), quindi non c'è stata deformazione.
  • [verifica] Provino in-engine guardato, non solo lanciato: bash tools/autotest/shot_su616_fx.sh, texture ricaricata a (20.0, 28.0), nessuno SCRIPT ERROR, scatti in TMP/screenshots_claude/SU616_giro2_fx_caffe_{sopra_la_testa,in_salita}.png. La cache di import di coffee.png è stata cancellata a mano prima del giro: il compile-check non reimporta gli asset, e senza quel passo il provino avrebbe mostrato la tazzina vecchia dando per buona la sostituzione.
  • Nessun .gd toccato: lo sprite è caricato in un punto solo (Player.gd:794), quindi la sostituzione del PNG basta. Le diciture nelle 8 lingue erano già a posto dal giro precedente; l'unica occorrenza residua di energydrink resta la nota storica nel commento di shot_su616_fx_caffe.gd, non un identificatore vivo.
  • Raw archiviato in sprites_raw/PICKUPS/coffee_tazzina_ivan_raw.png (git LFS, non committato).
  • ⚠️ NON provato su device.

Le panchine non devono stare in un elenco, e il «1000 al minuto» di EOS è per client2026-08-29

  • [doc] Domanda di Ivan sul brief di SU-637: perché tutte le panchine dormite finiscono nel salvataggio? Non è uno storico voluto: è il set anti-doppioni che serve a calcolare counters["panchine_diverse"], e quel contatore ha due soli consumatori — la carta COPERTA DI CARTONE (soglia 10, MetaProgress.gd:113-115) e la riga «Panchine diverse» del Quaderno (Quaderno.gd:566). La chiave è la *posizione* arrotondata a 32 px, e non l'instance id, solo perché quello non sopravvive alla sessione.
  • ⚠️ Correzione al brief: «benches è l'unico campo senza tetto» era impreciso. La città è sempre 1632×1632 px (GRID_W/GRID_H = 8, CELL_SIZE = 192, ROAD_WIDTH = 96, tutte const in WorldGenerator.gd:16-21), quindi le chiavi possibili sono al massimo 52×52 = 2.704: la riga «2.000 panchine → 34.930 B» della tabella non è il caso tipico ma quasi il peggiore assoluto. Nello stesso giro è emerso un difetto di misura: indicizzando la posizione in una città che si rigenera a ogni run, due panchine diverse in run diverse che cadono nella stessa cella contano una. Il contatore sotto-conta e satura.
  • [doc] SU-658 creato (backlog, epic M1) su decisione di Ivan in chat: il dedup diventa per-run e in RAM — dentro una run l'instance id basta — e su disco resta il solo intero, incrementato di 1 alla prima dormita nuova. meta.cfg torna fisso a ~2.323 B, cioè sempre dentro un chunk da 4096: cade il ramo multi-chunk di SU-637c e il campo si fonde col massimo fra interi come gli altri contatori.
  • 📌 Semplificazione trovata scrivendo il ticket: non serve consolidare a fine partita. Sommare a fine run e incrementare durante la run danno lo stesso totale, ma il secondo non apre la finestra in cui un crash a metà run perde le panchine già dormite (oggi ogni panchina fa save() subito). Il punto di azzeramento esiste già: il fronte di salita di run_active in _tick(), dove si conta run_totali.
  • ⚠️ La trappola scritta sul ticket, perché altrimenti la modifica sembra non funzionare: save() fa cf.load(SAVE_PATH) prima di scrivere (MetaProgress.gd:1086), quindi togliere la set_value lascerebbe il dizionario vecchio nel file per sempre e il salvataggio peserebbe come prima. Serve una erase_section_key("meta", "benches") esplicita, con la migrazione timbrata in migrations come SU-362.
  • [doc] Seconda domanda: le 1000 richieste al minuto di Player Data Storage sono per utente o totali nostre? Sono per client — e la doc non lo scrive su quella riga, va ricavato da due definizioni che linka: la pagina di PDS introduce la tabella con «*Throttling* enforces usage rate limitations», e §Request Throttling dice «the local client self-throttles» e parla di rifiuti «from individual clients or hosted servers». Le quote per deployment sono un meccanismo diverso, «adjusted based on the number of concurrent users online», e Epic non ne pubblica i numeri: non c'è quindi una soglia di utenti da sfondare, c'è da non concentrare le scritture in uno stesso minuto.
  • 📌 EOS_Result_PlayerDataStorageUserThrottled non è il rate limit, malgrado il nome: la doc lo lega allo spazio pieno. Il rate limit dà EOS_TooManyRequests, che l'SDK ritenta da solo «with the time specified in the HTTP data».
  • ⚠️ Il «60 al minuto per utente» dell'ingest delle stat non è confermato dalla fonte: sta in un commento nostro (OnlineLeaderboard.gd:835) e la pagina pubblica della Stats Interface non pubblica alcun rate limit. Il brief ora lo dice invece di darlo per verificato.
  • 📌 Le pagine di doc Epic vanno scaricate con curl: sono renderizzate in JS e WebFetch le restituisce vuote. Il testo sta nel bundle MDX dentro l'HTML.
  • Nessun file di gioco toccato: solo OPUS_BRIEFS/DESIGN_SU-637_progressi_sull_account.md, il ticket su Jira e questa voce.

Il frame del menu dimezzato, e una diagnosi mia da correggere sul crash Android2026-08-29

  • ⚠️ PRIMA DI TUTTO, LA CORREZIONE: la mia diagnosi diceva «il crash è sempre all'avvio, mai in partita», ed era sbagliata. L'avevo dedotta dal fatto che i file di run colpiti contenessero un solo evento. Ma il crash_tail viaggia nel file della sessione successiva: quel conteggio non dice nulla su quanto si è giocato nella sessione morta. La prova sta nel testo della coda, non nel numero di eventi. Riconteggiato leggendo le code: 5 crash all'avvio e 3 al rientro al menu dopo una partita (quelli con un mondo generato per intero nella coda). Verificato in modo indipendente dopo la segnalazione dell'agente.
  • 📌 Il denominatore comune vero è più stretto e più utile di quello vecchio: 8 code su 8 muoiono nel frame in cui MainMenu si costruisce — 5 all'avvio, 3 al rientro. Non «l'avvio»: la nascita del menu, ovunque avvenga.
  • ⚠️ La strada D come Ivan l'ha chiesta NON è stata implementata, e va detto chiaro. Quando Boot._ready() gira, il motore ha già presentato il boot splash decine di volte: dal nostro codice il *primo* present non è raggiungibile. boot_splash/minimum_display_time esiste ma sposta solo l'avvio, e i 3 crash al rientro lo scavalcano per costruzione. La D letterale applicata al frame giusto — RenderingServer.set_render_loop_enabled(false) per N frame — è raggiungibile ed è stata scartata di proposito: se il riaccendi non arriva, lo schermo resta congelato per sempre su un device che non abbiamo in casa, cioè un guasto peggiore di quello che mitiga.
  • [change] Fatta la seconda migliore, quella che il brief prevedeva come ripiego: accorciare il frame incriminato. _build_ui() decodificava e caricava in VRAM due immagini grandi a ogni nascita del menu — lo sfondo 1920×800 e lo splash 1941×810, quest'ultimo buttato via due righe dopo e per giunta scavalcando una cache di sessione che esisteva già da SU-544. Ora: BootScreen.splash_texture() al posto del load() diretto, e una cache di sessione nuova per gli sfondi (_cached_menu_bg, menu_background()), riscaldata in thread durante i 2,6 s di splash con prewarm_menu_backgrounds().
  • [verifica] A/B misurato dall'orchestratore con quattro costruzioni complete di MainMenu e free() in mezzo, la sonda che svuota le cache nel ramo «senza cura»: avvio 174,8 → 148,1 ms (−15%); rientro dal gioco ~173 → ~97,5 ms (−44%), stabile su tre giri. Il singolo PNG di sfondo costa 27-52 ms a freddo e 0,005-0,016 ms dalla cache.
  • ⚠️ È una mitigazione probabilistica, non la cura, e lo dico senza giri: non tocca la corsa fra la surface Android e il present, dimezza la finestra in cui la si può perdere. Se il tester vede ancora crash, resta la strada C (gl_compatibility sui device colpiti), che è decisione di Ivan.
  • 📌 La pista migliore sul *trigger*, trovata e non toccata: project.godot dichiara handheld/orientation=4, cioè sensorLandscape. Ogni ribaltamento landscape↔reverse-landscape distrugge e ricrea la surface — il che spiega l'intermittenza (42% sull'OPPO contro 1,4% sullo Xiaomi) meglio del produttore della GPU. Bloccare l'orientamento è una scelta di UX e va decisa, non fatta di nascosto.
  • [fix] shot_su416.sh era già rotto a HEAD, e l'ha scoperto questo giro: SU-619 ha reso Boot.gd dipendente da UpdateNotice.gd, che nomina l'autoload DemoMode; il provino compilava Boot prima degli autoload e moriva su crea_e_avvia. Il gioco vero non ne soffriva — era rotta solo la rete di sicurezza. Ora ESITO=OK, 16 scatti, sfondo/logo/pioggia presenti sia al primo menu sia al secondo della sessione.
  • ⚠️ [fix] Trovato chi ricreava files/homeless_city/TMP/ dentro il progetto Godot, la cartella che SU-458 aveva sgomberato e che avevo ripulito a mano stamattina: non la sonda, il suo wrapper. shot_su416_boot.gd aveva già il default giusto (res://../../TMP, che risolve alla root), ma shot_su416.sh glielo scavalcava passando files/homeless_city/TMP/su416_419. Erano 9,4 MB e una cartella da ricancellare a ogni giro. Corretto il default del wrapper: ora gli scatti finiscono sotto il TMP/ della root e la cartella non si ricrea più — verificato rilanciando.
  • ⚠️ NON provato: il crash. Nessuno dei tre device è in casa, e in headless i millisecondi contano la sola decodifica CPU, non il caricamento in VRAM, che sul telefono è l'altra metà del costo tolto. Il criterio «sull'OPPO il tutorial arriva in fondo» va misurato dal tester su più avvii: col 42% una prova sola non decide niente.

Revocare il consenso mentre si gioca non butta più l'ultimo pezzo di telemetria2026-08-29

  • [fix] SU-657 — set_recording(false) abbassava la bandiera PRIMA di chiudere la run. _write() scarta ogni riga appena _on è falso, quindi tutto ciò che _end_run() avrebbe scritto — episodio «stuck» ancora aperto, run_end — finiva nel nulla senza un errore. Si vedeva solo spegnendo la registrazione a run aperta, cioè quando un giocatore revoca il consenso dalle Opzioni *mentre sta giocando*. Trovato lavorando SU-652 e lasciato lì apposta, fuori perimetro.
  • [change] La via scelta: _end_run("registratore_spento") gira con _on ancora vero, e la bandiera si abbassa subito dopo, nello stesso giro sincrono — nessun await in mezzo, quindi nessun'altra riga può infilarsi. Non sono stati toccati né _write()_pump_queue(): era la modifica meno invasiva delle due che il ticket proponeva.
  • ⚠️ Il criterio che contava di più era la privacy, non i dati, e l'ho verificato di persona invece di fidarmi del ragionamento nel report — perché una correzione che salva l'ultimo pezzo di run ma lascia partire un invio dopo la revoca sarebbe peggio del difetto curato. I due fatti su cui si regge, controllati uno per uno: _pump_queue() esce su if not _consent_given() prima di aprire qualsiasi socket (è gated sul consenso di Settings, non su _on); e set_telemetry_consent() scrive telemetry_consent = value prima di notificare il registratore. Quindi quando set_recording(false) gira, il consenso è già spento e la coda trova la porta chiusa — anche nel giro che _end_run() si programma da solo.
  • [verifica] Sonda nuova probe_su657_consenso.gd + tools/autotest/probe_su657.sh, 16 controlli 0 KO, rilanciata dall'orchestratore. Le righe di chiusura risultano scritte; e dopo la revoca il file resta un .jsonl non spedito, la coda non si muove nemmeno forzando il pump, e il server finto riceve zero richieste.
  • 📌 La sonda sa riprodurre il difetto, e questo è ciò che la rende una prova: rimettendo l'ordine vecchio delle due righe dà 2 KO esatti su «riga stuck scritta» e «riga run_end scritta». Uno scatto «dopo» pulito non dimostra niente senza un «prima» sporco.
  • [verifica] Non-regressione: probe_su413 (consenso on/off, sentinella, ripresa al riavvio) tutti i casi OK, e probe_su652 (episodi stuck, scritta stamattina) 13/13 OK. Compile-check 0 falliti.
  • ⚠️ NON provato: il pannello Opzioni vero a schermo e nessun device. Tutto passa dall'API di Settings/RunRecorder, come le sonde gemelle SU-413/SU-651/SU-652.

I primi tre gradini della Baracca costano meno, e una sonda che passava solo la prima volta2026-08-29

  • [change] SU-656 — i primi tre upgrade della Baracca calano di prezzo (decisione di Ivan su SU-653: «abbassiamo i primi gradini»). FONDO CASSA 40 → 25, COLAZIONE ABBONDANTE 60 → 40, AMICO DEL BOTTEGAIO 70 → 55. Dal quarto in su invariati: indeciso 80, veto 90, micio 100, vecchia volpe 120, endless 200, doppia identità 250. Obiettivo dichiarato: primo upgrade entro ~3 run per un giocatore nuovo.
  • [verifica] La UI non aveva costi cablati, ed era il difetto tipico da temere: _refresh_baracca_info in MainMenu.gd chiama già mp.shack_cost(id), quindi la riga «ti mancano N lattine» segue il prezzo nuovo da sé. Nessuna modifica alla UI.
  • [verifica] Scatto in-engine TMP/su656_baracca.png, guardato e non solo citato: la schermata mostra IN TASCA: 24 LATTINE, FONDO CASSA · 25, COLAZIONE ABBONDANTE · 40, AMICO DEL BOTTEGAIO · 55 e i sei costi alti intatti; in fondo «Ti mancano 1 lattine», calcolato sul prezzo nuovo.
  • 📌 Un falso allarme che valeva la pena chiarire: la descrizione dice «Parti la run con $25 in tasca» e il costo è appena diventato 25 — sembrava che il prezzo si fosse infilato dentro l'effetto. Non è così: il numero viene da SHACK_FONDO_CASSA_MONEY, una costante separata che valeva già 25 quando il prezzo era 40, e il diff non l'ha toccata. Coincidenza, verificata e non dedotta.
  • ⚠️ LA SONDA CONSEGNATA PASSAVA SOLO AL PRIMO LANCIO, e questo è il pezzo che conta. Rilanciandola in sede di chiusura è andata rossa: has_upgrade("fondo_cassa") risultava già true prima ancora di comprare, quindi buy_upgrade() usciva subito e il caso del confine falliva — col codice di gioco perfettamente sano. Causa: il wrapper usava sì una HOME dedicata, ma sempre la stessa e mai svuotata, così dal secondo giro il meta.cfg conteneva l'acquisto del giro prima. Il commento diceva «dedicati e freschi»: erano dedicati, non freschi. Aggiunto rm -rf prima del lancio.
  • 📌 È la variante meno visibile della trappola «un provino morto lascia gli output vecchi»: qui non restava uno scatto vecchio ma uno *stato persistente*, e il risultato era una sonda che dava ragione a chi la scriveva e torto a chiunque la rilanciasse. Ora è ripetibile: lanciata due volte di fila, OK entrambe.
  • [verifica] I numeri: 9 costi su 9 letti da shack_cost() combaciano; confine 24 lattine → acquisto rifiutato, 25 → acquisto riuscito e lattine azzerate. Compile-check 0 falliti.
  • ⚠️ NON provato: l'acquisto dal menu con input vero (INVIO sulla riga selezionata). La sonda passa dall'API di MetaProgress, non simula la pressione del tasto.

Le carte si sentono, la fame e il sonno mollano la presa, e un moltiplicatore che stava per ribaltarsi2026-08-29

  • [change] SU-654 — la curva delle carte percentuali sale a 10-15% per livello (decisione di Ivan su SU-647: «io farei 10-15%, proverei con quello»). Il dato che la motivava: con STOMACO DI FERRO lv3+ la fame calava come senza carta — −54,8 contro −47,8 punti/min, cioè rumore — perché il 4%/livello sta sotto la soglia di percezione. Nuovi valori in EFFECTS: stomaco/sonno/pelle 0,04 → 0,12, carisma 0,08 → 0,15, kazoo resa 0,12 e raggio 0,10, topo 0,12, bravo ragazzo wanted e rep 0,12, suola 0,02 → 0,10. CALAMITA invariata (+25 px/livello): è il modello-interruttore che già funziona al 60% di take.
  • 📌 Le descrizioni mostrate al giocatore ripetevano i numeri a mano in _effect_line(), staccate da EFFECTS: allineate una per una. Senza, la carta avrebbe applicato un valore e dichiarato l'altro.
  • ⚠️ IL DIFETTO GROSSO DEL GIRO, che la curva nuova creava e nessun criterio del ticket avrebbe intercettato: un moltiplicatore che si RIBALTA DI SEGNO. get_mult() applica i «reduce» come 1.0 − (per_livello × livello); con 0,12 e MAX_LEVEL = 9 il prodotto vale 1,08, quindi il moltiplicatore diventa −0,08. Effetto vero: la fame invece di scendere più piano sarebbe salita (la carta ti nutre), e un wanted_gain negativo avrebbe voluto dire che commettere reati toglie ricercato. Col vecchio 0,04 non poteva accadere (a liv. 9 dava 0,64): è un difetto che nasce *insieme* alla curva nuova.
  • [fix] Pavimento MULT_REDUCE_FLOOR = 0.10 in get_mult(): una carta può togliere al massimo il 90% del decadimento, mai ribaltarlo. Sta nella funzione e non nei singoli valori apposta, così protegge anche le tarature future.
  • 📌 E il pavimento andava messo in DUE posti, non uno: applicandolo solo a get_mult() il testo avrebbe continuato a dire «−108%» mentre il gioco applicava −90% — la stessa divergenza testo/valore che il ticket doveva chiudere, rientrata dalla porta di servizio. Ora _pm() clampa allo stesso modo e i due si muovono insieme.
  • [verifica] Sonda nuova probe_su654_carte.gd + tools/autotest/probe_su654.sh, 0 KO, rilanciata dall'orchestratore due volte (prima e dopo il pavimento). Per ogni carta, a livello 1/3/9, confronta il valore applicato letto da get_mult()/get_add() con i numeri estratti dal testo mostrato. A liv. 9 ora legge −90% applicato e −90% scritto; la calamita resta 25/75/225 px e il suo testo resta letterale, senza percentuali.
  • [change] SU-655 — il tetto delle rampe di fame e sonno scende a 1,50 (decisione di Ivan: «io proverei tetto della rampa fame e anche sonno a 1,50»). È la strada B di SU-649 estesa anche all'energia, che era metà del problema: il bot muore di fame *o di stanchezza*.
  • [verifica] I numeri, dalla sonda probe_su648_fame estesa (16 controlli, 0 KO, rilanciata dall'orchestratore): fame a 300 s 0,6000 e a 720 s 1,0000 — cioè invariati, il cambio non tocca il tratto iniziale; a 1710 s 1,4583 contro 1,9250, −24,24%; a 1800 s 1,5000. Energia a 1800 s 1,5000 (era 1,6000). TIER_THRESHOLDS e TIER_DECAY_MULT letti intatti, come chiedeva SU-411.
  • 📌 L'assert vecchio della sonda è stato sostituito, non aggirato: verificava che il valore a 1710 s restasse entro l'1% della formula storica, ed è diventato strutturalmente falso — quel valore adesso cambia *apposta*. Al suo posto un assert sul valore assoluto atteso.
  • ⚠️ Il TestBot non si accorge di SU-655, ed è corretto così: 113,8/113,8/108,6 s prima, 108,6/113,8/112,5 s dopo. Muore a ~110 s, cioè dentro il warmup, e non arriva mai al tratto oltre i 720 s che questo ticket abbassa. Serve alle run umane lunghe, non alla morte precoce del bot.
  • ⚠️ NON provato: nessuna run giocata oltre i 720 s, né su desktop né su device. Che la fame «molli davvero la presa» nel finale è un giudizio che resta a Ivan.

Il numero che mancava a SU-653, e la scoperta che il TestBot muore a 105 secondi2026-08-29

  • [verifica] SU-653 — misurata la resa lattine di una run, che nel codice non c'è perché è un esito di partita. Sonda nuova probe_su653_lattine.gd + tools/autotest/probe_su653.sh: fa girare il TestBot su run vere in un HOME isolato per run (lattine azzerate) e conta gli accrediti per sorgente — banca, notte fuori, forzieri, livello bonus del metro.
  • [dato] Il risultato, su 13 run in tutto (10 del builder + 3 di verifica indipendente): ZERO lattine. Non «poche»: zero, in ogni run naturale. La banca resta a 0 in tutte le run, il livello bonus del metro non viene mai raggiunto, i forzieri hanno dato 3 lattine in 1 run su 10. Con il bot tenuto in vita a forza (--bot-stat-floor 20) il totale sale a mediana 5, e servirebbero 8 run per il primo gradino da 40 lattine.
  • 📌 Come va letto, perché il bot non è un giocatore: le due sorgenti grosse — banca (10 $ depositati = 1 lattina) e bonus del metro (14-20 lattine) — richiedono intenzionalità, e il bot gira a caso. Quindi lo zero non è «la resa dei tester»: è il limite inferiore di chi gioca senza capire il gioco. Ed è comunque informativo, perché conferma per un'altra strada la diagnosi del ticket: il 75% di run senza upgrade nasce dal fatto che le sorgenti vere sono inaccessibili a chi non le cerca.
  • ⚠️ IL TESTBOT MUORE A ~105 SECONDI, E LA MEMORIA DI PROGETTO DICEVA ~610. Misurato: 4 run su 4 a 104,5 s sul codice di ieri, 108,5-113,7 s su quello di oggi, sempre per stanchezza o fame. È un dato che falsa ogni collaudo che assuma una run lunga — la retata a 1710 s è irraggiungibile con un margine ben più largo di quanto si credesse. La cifra vecchia va considerata superata o riferita a uno scenario diverso.
  • 📌 Controprova che la morte precoce NON è colpa della taratura di SU-648 di oggi: rimesso DifficultyManager.gd alla versione di f7397b5 e rilanciata la sonda — 104,5 s, quattro run identiche. Col codice nuovo il bot vive il 4-9% in più. La taratura migliora la sopravvivenza, non la peggiora, ed era il rischio da escludere prima che lo scoprisse Ivan.
  • ⚠️ Difetto nello strumento, trovato rilanciandolo e corretto: probe_su653.sh moriva a ogni run nel caso senza floor — la bash 3.2 di macOS, sotto set -u, tratta "${ARR[@]}" di un array vuoto come *unbound variable*. Lo script stampava «nessuna riga RIGA_MACCHINA», cioè sembrava che le run fossero fallite nel gioco mentre non erano proprio partite. Corretto con la forma ${ARR[@]+"${ARR[@]}"} e annotato nello script. È il motivo per cui il diff si guarda sempre: il report diceva «5 run naturali», ma lo strumento consegnato non girava.
  • 📌 Nessuna modifica al gioco in questa voce: SU-653 è un ticket di sola decisione e l'incarico era di sola misura. Prezzi, rese e costanti non sono stati toccati.

Lo «stuck» smette di contare chi suona fermo, e l'invio iOS non muore piu' sul secondo salto2026-08-29

  • [change] SU-652 — 59 episodi «stuck» su 59 erano quasi tutti un giocatore fermo a suonare. Sette identici da 120 s nello stesso punto, su 51.525 tick di busking nella beta: il segnale non diceva più niente sulle geometrie che intrappolano davvero. Ora l'episodio si scrive solo se nel frattempo il giocatore ha premuto una direzione (_stuck_input_seen, riazzerato a ogni riarmo dell'ancora, gate aggiunto in _close_stuck_episode).
  • 📌 Perché il busking sfuggiva: non passa da note_interact_named() — emette solo player_misdemeanor, che RunRecorder aggrega in busk senza toccare l'ancora. Restava quindi indistinguibile da un vero «piantato», ed è il motivo per cui SU-590 (che aveva risolto lo stesso equivoco per il livello bonus) non bastava qui.
  • 📌 Il meccanismo di SU-590 NON è stato riusato alla lettera, ed è una scelta: quello invalida secco su una condizione di stato, qui serviva accumulare «è successo almeno una volta» nel corso dell'episodio. _poll_stuck_input() gira a ogni frame e non al campione dei 2 s di _sample_pos, altrimenti un giocatore che spinge a scatti contro un ostacolo sfuggirebbe al controllo. Legge le stesse quattro azioni di Player.gd, quindi vale anche per i controlli virtuali del touch.
  • [verifica] Sonda nuova probe_su652_stuck.gd + tools/autotest/probe_su652.sh, 13 controlli 0 KO, rilanciata dall'orchestratore. Fase A, 150 s fermo col busking tenuto e mai una direzione: 0 eventi. Fase B, 40 s spingendo move_up senza spostarsi: 1 evento. Sono i due criteri del ticket, misurati sul player vero preso da Main.tscn.
  • 📌 Due trappole trovate scrivendo la sonda: 150 s reali sono 6 ore di gioco, e il barbone muore di fame a metà se la sonda non lo rifornisce; e il fisico va disattivato per bloccare la posizione, come già fa probe_su646_gattara.gd.
  • [fix] SU-651 — result=13 non era un errore di redirect: era un timeout, e il commento nel codice lo diceva sbagliato. Nell'enum di Godot 4 RESULT_REDIRECT_LIMIT_REACHED = 12 e RESULT_TIMEOUT = 13; il commento di _on_send_completed etichettava il 12 come «RESULT_TIMEOUT». ⚠️ È proprio quell'etichetta ad aver fatto scambiare il 13 osservato su iPhone 15 Pro per «la solita cosa del redirect», mentre http=0 diceva che sul secondo salto non era mai arrivata nessuna risposta. Commento corretto.
  • [fix] SU-651 — la causa: la stessa HTTPRequest riusata su un host diverso. Apps Script redirige da script.google.com a script.googleusercontent.com, e il secondo salto riusava _http senza cancel_request(), con la connessione al primo host ancora viva. Nuovo _swap_http(): la vecchia istanza va in queue_free() e ne nasce una pulita per la GET sulla Location. Preferito a cancel_request() sulla stessa istanza perché non lascia stato residuo del primo host (socket, sessione TLS) e non richiama un metodo sul nodo mentre è dentro il proprio signal handler.
  • [verifica] Sonda nuova tools/autotest/probe_su651.sh con DUE server finti su porte diverse (tele_test_server.py ha ora --redirect-to), che è l'unico modo di riprodurre qui il salto fra host che su iOS falliva. Rilanciata dall'orchestratore: POST al server A → 302 → GET arrivata al server B → 200, file marcato .sent, il server A non salva niente. Secondo caso, endpoint irraggiungibile: il file resta in coda, 2 tentativi falliti osservati, nessun file perso — è il secondo criterio del ticket.
  • [verifica] Non-regressione più ampia: rilanciata anche l'intera suite esistente probe_su413.sh (consenso on/off, sentinella, ripresa al riavvio), tutti i casi OK.
  • ⚠️ NON provato, e va detto senza girarci intorno: il criterio «una run di prova su iOS arriva su Drive» resta NON MISURATO. Non c'è un device iOS utilizzabile e il simulatore di questo progetto compila solo il boot. La correzione è coerente col sintomo e con la causa letta nel codice, ma la conferma su iOS vero non c'è.
  • ⚠️ Bug preesistente trovato e NON toccato, da farne un ticket: set_recording(false) mette _on = false prima di chiamare _end_run(), e _write() scarta ogni riga appena _on è falso — episodio «stuck» pendente compreso. Invisibile in partita vera (lì _end_run parte con _on ancora true), si vede solo spegnendo la registrazione a run aperta: un giocatore che revoca il consenso dalle Opzioni mentre gioca perde in silenzio l'ultimo pezzo di telemetria di quella run.

La gattara smette di falciare allo spawn, e il gradino della fame al minuto 3 si spiana2026-08-29

  • [change] SU-646 — la gattara di Gattopoli aveva un cooldown quasi senza margine. Telemetria 0.40/0.41: in tutte e 18 le run a Gattopoli la gattara toglie una vita nel primo minuto, con una sequenza misurata di 4 vite fra t=6,0 e t=10,9 — un colpo ogni ~1,7 s, cioè esattamente il vecchio CATLADY_THROW_COOLDOWN = 1.8, sfruttato al limite. 17 delle 22 morti per «vite finite» dell'intera beta sono lì. Cooldown 1,8 → 5,0 (ModularNPC.gd:255), lo stesso ordine di grandezza dell'unico altro NPC che lancia proiettili a distanza.
  • [change] SU-646 — la tregua d'ingaggio riusa la leva di SU-403 invece di inventarne una. CATLADY_ENGAGE_GRACE_SEC = 9.0 sovrascrive in apply_archetype(), solo per arch_catlady, la _engage_grace_left condivisa con junkie/rivale/ronda (che vale 3 s e non copriva la sequenza misurata). 📌 La via alternativa — spawnare il barbone fuori dal raggio d'aggancio della gattara — è stata scartata di proposito: quel codice vive in WorldGenerator.gd (8.882 righe, fuori perimetro) e per di più la gattara non ha un raggio d'aggancio, insegue sempre il barbone più vicino. La leva scelta è già gated nel dispatch (if not in_engage_grace: _update_catlady(dt)), quindi morde davvero: verificato riga per riga, non dedotto dal report.
  • [verifica] Sonda nuova probe_su646_gattara.gd + tools/autotest/probe_su646.sh, 7 controlli 0 KO, rilanciata dall'orchestratore. Grazia iniziale letta 8,97 s; zero vite perse da gattara nei primi 8 s (cat_lives=9 intatte); lanci osservati a [3,4 · 8,4 · 13,2 · 18,0 · 23,0] s con intervallo minimo 4,80 s (criterio: ≥ 4); prima vita persa a 9,376 s dallo spawn, contro le sequenze che nella beta partivano da 6,0 s.
  • [change] SU-648 — il gradino della fame al minuto 3 era la fine del warmup. Pressione misurata (pasti e pause esclusi): −50/min a 1:30-3:00, −65/min a 3:00-4:30 (+30% in un'unica fascia, proprio ai 180 s), fino a −118/min a 10-15 min; sopravvivenza mediana 4:39, cioè si muore dentro il salto. WARMUP_DURATION_SEC 180 → 300 e la rampa 0,60 → 1,0 completata a 720 s.
  • 📌 Perché una costante nuova e non TIER_THRESHOLDS[1]: quella soglia la governa SU-411, appena tarata, e il ticket la vuole intatta. Da qui HUNGER_RAMP_END_SEC = 720.0, dedicata solo a questa rampa.
  • ⚠️ Il terzo tratto è stato riancorato, ed era necessario. Lasciando il ramo 1,0 → 2,0 ancorato a 600 s, a t=720 si sarebbe passati da 1,0 (fine rampa) a 1,1 (valore già calcolato per il vecchio ancoraggio): un gradino del 10%, cioè esattamente il difetto che il ticket doveva togliere, spostato di due minuti. Riancorato a ramp_end_p. Costo dichiarato: a 1710 s il moltiplicatore fa 1,9167 invece di 1,9250, −0,43% — dentro la tolleranza dell'1% della sonda, ma non è «invariato» alla lettera e come tale è scritto sul ticket.
  • ⚠️ L'energia aveva un gradino vero, non teorico, e ritoccarla era la scelta giusta ma cambia il bilanciamento a metà run. Prima: costante 0,50 durante il warmup, poi salto secco a lerpf(0.75, 1.6, progress)0,50 → 0,835 a t=180, +67% istantaneo. Allungare il solo warmup lo avrebbe peggiorato (0,50 → 0,892 a t=300, +78%). Ora la curva parte da 0,50 e arriva allo stesso 1,6 finale. 📌 Effetto collaterale da sapere: a 600 s l'energia scende molto meno di prima — 1,0333 → 0,7200, cioè −30%. A 1710 s la differenza è quasi nulla (1,5575 → 1,5340). get_hygiene_decay_multiplier() ha lo stesso schema ma è fuori dal perimetro del ticket e non è stata toccata: resta un gradino aperto, dichiarato qui perché non si perda.
  • [verifica] Sonda nuova probe_su648_fame.gd + tools/autotest/probe_su648.sh, 14 controlli 0 KO, rilanciata dall'orchestratore. Tabella prima/dopo ai sei istanti chiesti dal ticket — fame a 300 s 0,6000 (criterio: 0,60), a 720 s 1,0000 (criterio: 1,0), a 1710 s 1,9167 vs 1,9250. TIER_THRESHOLDS letto [0, 600, 1320, 1710] e TIER_DECAY_MULT letto [1.0, 1.1, 1.22, 1.4]: intatti, come chiedeva SU-411.
  • [verifica] Il TestBot non dipende da nessuna delle soglie toccate (grep su TestBot.gd: nessun riferimento a warmup, gattara o decay). È il controllo che il giro precedente ha imparato a caro prezzo — lì il bot avrebbe smesso di scoreggiare in silenzio. Nessuna sonda esistente codificava i valori vecchi.
  • ⚠️ NON provato: niente su device e nessuna partita giocata da una persona. Tutto misurato in sonda headless; la resa vera («adesso a Gattopoli si sopravvive», «il minuto 3 non è più un muro») è un giudizio che resta a Ivan.
  • 📌 Interazione da tenere presente per SU-649 (decisione scritta lo stesso giorno, strada B): il terzo tratto della fame ora è ancorato a 720 s, non a 600. Il tetto proposto di 1,70 resta valido sulla formula nuova — a 1710 s darebbe 1,642 contro l'obiettivo di 1,66.

La scoreggia si tiene premuta sempre, e il caffe' e' una tazzina2026-08-29

  • [change] SU-643 — la pressione tenuta serve SEMPRE, gatto o non gatto. ⚠️ Requisito rovesciato da Ivan in chat il 28/08: «anche senza gatto va premuto il tasto piu' a lungo per scorreggiare, insomma stesso comportamento con/senza gatto». Il primo giro aveva implementato l'opposto — scoreggia istantanea senza gatto — e lo aveva fatto seguendo la descrizione del ticket, che diceva proprio cosi'. Tolta la scorciatoia if not GameState.has_cat da Player._handle_fart(). Ora: tenuta ≥ 0,30 s → scoreggia sempre (col gatto, il gatto resta in mano); breve → col gatto lo lancia, senza gatto non fa niente.
  • 📌 Il blocco di commento che argomentava il contrario e' stato riscritto, non cancellato: portava un ragionamento articolato («chi il gatto non ce l'ha non paga un millisecondo per una feature che non lo riguarda») che era mio e Ivan ha scartato. Al suo posto la regola nuova con un ⚠️ «NON RIMETTERE LA SCORCIATOIA» e il perche' — il gesto dev'essere uno solo, e il gatto lo prendi e lo perdi durante la partita. Lasciarlo com'era avrebbe fatto "correggere" la scorciatoia a qualcuno che credeva di sistemare una regressione. Descrizione del ticket su Jira aggiornata di conseguenza, non solo il commento.
  • ⚠️ IL TESTBOT AVREBBE SMESSO DI SCOREGGIARE IN SILENZIO, ed e' misurato. Premeva fart per 0,15 s, cioe' sotto la soglia di 0,30: dal cambio in avanti non avrebbe piu' scoreggiato, senza un solo errore nei log, falsando ogni collaudo futuro. HOLD_FART_SEC = 0.45 per la sola scoreggia (TestBot.gd:342). Controprova, run da 150 s a parita' di seme: 0,15 s → farts = 0; 0,45 s → farts = 11. Aggiunto farts al dump [BOT] perche' quel silenzio non si ripeta.
  • ⚠️ Due sonde esistenti codificavano la regola vecchia e sarebbero andate rosse dopo il commit — trovate col grep, non erano nel perimetro: probe_su565_azioni.gd premeva 6 frame aspettandosi la scoreggia, probe_su644_terremoto.gd aveva il caso «senza gatto scoreggia all'istante». Aggiornate e rilanciate, 0 KO.
  • [verifica] Sonda nuova probe_su643_scoreggia_tenuta.gd, 5 casi coi tempi veri: breve senza gatto 0,100 s → 0 scoregge; tenuta 0,450 s → 1; breve col gatto → gatto lanciato; tenuta col gatto → scoreggia e il gatto resta; tenuta 3,6 s → 1 sola scoreggia. Preme da tastiera, non solo da touch. Controprova sul Player.gd di HEAD: 1 KO — la sonda discrimina, quindi la prova vale.
  • [verifica] Il pinch non parte con un dito su NESSUNO dei 4 tasti (richiesta di Ivan dello stesso giro, ed e' lo stesso requisito di SU-592): sezione D nuova in probe_su592_tocco_a_vuoto.gd, un caso per tasto piu' uno col tasto scoreggia tenuto giu' e il pollice che cammina in tondo. 0 KO con la guardia, 8 KO senza — neutralizzata a mano e rimessa, World.gd byte-identico a HEAD. Senza guardia tutti e quattro i tasti aprono il pinch, pad spento, barbone fermo. La guardia di 2fc7b6f regge e non e' stata toccata.
  • 📌 Un caso della sezione D passa anche senza la guardia, ed e' una misura onesta: con due sole dita il tasto e' libero e se lo prende VirtualControls._input, a World non arriva niente da fermare. A discriminare sono i casi col terzo dito su un tasto gia' occupato, che e' la forma vera del difetto di Ivan. Annotato nel commento col divieto di «aggiustare» quel caso aggiungendoci un terzo dito per farlo sembrare piu' forte.
  • [change] SU-616 — lo sprite scelto e' la TAZZINA (assets/sprites/coffee.png, 20x28, fila in stile gioco: contorno nero spesso, colori piatti). Generata con Codex al secondo giro: il primo aveva prodotto tre varianti fotografiche senza contorno, fuori stile — rigenerate passando la lattina ZAP come riferimento, che e' il trucco che nei giri passati aveva tolto il drift di palette. Vecchio energydrink.png e il suo .import cancellati.
  • [verifica] Chiusa la meta' di criterio rimasta aperta: i 6 identificatori interni rinominati (_fx_coffee_tex, spawn_bin_coffee_fx()) e il load() ripuntato. energydrink nei .gd: 7 prima → 1 dopo, e quell'una e' una nota storica dentro il commento di una sonda («un tempo spawn_bin_energydrink_fx()»), non un identificatore vivo. 📌 La chiave "energy" di BIN_ROW_LOOT non e' stata toccata: e' condivisa col dizionario BIN_FORCED_ROLL di Interactable.gd e rinominarla da una parte sola romperebbe il contratto — ora il commento lo dice.
  • ⚠️ NON provato: niente su device — la prova in mano su Honor e iPad (dito sui quattro tasti mentre cammini, e la scoreggia tenuta) e' proprio quella che manca, ed e' un criterio del ticket. Il pinch e' provato solo con tocchi iniettati in headless, su un viewport 1024x1024 (root.size non attecchisce in headless: difetto preesistente della sonda). Niente joypad fisico, niente MP.

I bocciati del giro: la prima pagina del giornale torna, e due documenti che rispondevano alla domanda sbagliata2026-08-28

  • [fix] SU-633 (KO) — pagina 1 del game over torna il giornale che era. Il giro precedente aveva letto «i numeri si devono leggere» come «pagina 1 si svuota», e aveva tolto le quattro colonne di cronaca in prosa piu' il testo di riempimento. Ivan: «pagina uno va fatta come layout come prima, il layout deve rimanere quello che non era male». Ripristinati da git show 85e72c3^ _columns (formula 3/2/1), INK_FAKE, _add_article, _add_fake_text, _draw_fake_text e le quattro voci; FIT_MAX_DROP_PAGE1 torna a 3 perche' pagina 1 ha di nuovo pezzi sacrificabili.
  • 📌 Le colonne tornano ma senza cifre, che e' la mezza frase del KO facile da perdere: NEWS_ART_SOLDI, NEWS_ART_PUNTEGGIO, NEWS_ART_LATTINE, NEWS_ART_IMPRESA (8 lingue) perdono i %d e diventano qualitative — «ha racimolato qualche spicciolo» invece di «%d dollari». I numeri vivono su pagina 2, che Ivan approva e che non e' stata toccata.
  • [verifica] Il numero che prova che l'ingrandimento non e' stato eroso: _fs_body di pagina 2 resta 20 su telefono e 31 su tablet, identici al giro approvato. Era il rischio vero — _fit_step() rimpicciolisce il testo quando la pagina e' bassa, e rimettere contenuto su pagina 1 poteva farlo scattare anche sulla 2.
  • ⚠️ Bug trovato nel ripristino e corretto: nel ramo a 3 colonne _add_impresa finiva sia in first sia in last. Invisibile nei due formati provati (il guard _page_size.y<900 and _page_size.x<1200 la sopprime), sarebbe comparsa due volte su una finestra piu' alta.
  • [docs] SU-628 (KO) — dove si rompe davvero la catena fra «raccolgo una moneta» e «lo vedo nel punteggio». Il documento precedente diceva «le monete danno gia' punti, il buco non esiste» e archiviava: vero come aritmetica, falso come risposta. ⚠️ La rottura e' al quarto anello, la UI: ScoreSystem.score_changed e' emesso solo da reset_run, add_action e register_raid_survival; add_money emette money_changed, che nell'HUD muove l'etichetta dei dollari, non quella dei punti. Raccogliendo monete ★ N pt non si muove, poi alla prima azione salta di 40 mentre il popup dice «+20». Lo stesso vale per i 500 punti/giorno. E' un difetto di presentazione, e ora il documento lo dice con quelle parole.
  • 📌 Due scoperte collaterali: i soldi non possono mai essere «L'IMPRESA DEL GIORNO», perche' top_action() cicla solo su counts; e get_breakdown() e' morto per la UI — unico chiamante RunRecorder.gd:708 — mentre il commento di HUD.gd:5918 sostiene ancora che sia «l'articolo di spalla della prima pagina».
  • [decisione] Ivan, 28/08: la classifica non e' un problema — «aumenteranno tutti con la prossima release i punteggi e pace». Cancellata dal documento l'intera sezione sulla comparabilita' dei punteggi vecchi e sul TETTO_RUN da 500.000, con la nota di chi l'ha chiusa e quando, perche' non venga riaperta fra due mesi.
  • [docs] SU-624 (KO) — il livello della metropolitana ESISTE, e il documento vecchio diceva il contrario. Diceva: «la metropolitana non ha livelli ne' profondita', serve prima un sistema a piani, che e' un ticket molto piu' grande». ⚠️ Falso: il livello e' il parametro giro, e viene dallo scaglione della banca — catena completa da _giro_premio_banca() a Interactable._metro_apri_bonus() (var giro := _arco_giro) fino a BonusLevel.entra(mondo, giro). Gli scaglioni raddoppiano (100/200/400/800/1600) e salgono solo se il livello e' vinto: giro = 1 + livelli bonus vinti, non «quante volte si e' aperto» — il commento in testa a BonusLevel.gd:41 e' rimasto indietro. E' parola per parola quello che Ivan aveva descritto: «superato il livello da 100 dollari e rientrando con quello da 200, e cosi' via».
  • 📌 Il premio scala gia' con giro (valore_moneta() raddoppia) mentre la difficolta' e' identica a ogni giro: e' precisamente il buco che il ticket dei topi vuole riempire.
  • 📌 Secondo errore del giro vecchio corretto: una grammatica dei nemici c'e' gia' (6 ENEMY_ARCHETYPES, _defeat_enemy, loot da $12), solo che nel tunnel non ci arriva perche' li' il barbone non e' un Player e il gatto non esiste. Da qui la risposta a «come si uccidono»: non si uccidono, si schivano, con una variante a costo basso (li spazza il treno, lasciano una lattina).
  • [verifica] Il numero che regge il ticket dei topi: il corridoio ha 8,4 s di margine misurato contro un pavimento dichiarato di 6,0 s (MARGINE_MINIMO_SEC), quindi il tetto di 3 inciampi da 0,6 s e' scelto per costruzione e non per prudenza.
  • [fix] SU-621 (KO) — il temporale si sposta sull'asse del meteo che il giocatore vede davvero. ⚠️ Il giro precedente aveva lavorato sull'asse sbagliato, ed e' tutto il KO. Il meteo ha due assi indipendenti (documentati in testa a WeatherSystem.gd): lo stato ambientale fisso (CLEAR/CLOUDY/RAIN/STORM) e le calamita' a timer ("rain", "hail", "heat"), che sono «quelle da cui ci si ripara». Weather.STORM — su cui era stato costruito tutto — in partita non si accende mai: force_state() lo chiamano solo sonde e pannello di debug, e apply_district_weather() esce subito perche' Quartieri.fixed_weather() non esiste. Le parole di Ivan «la pioggia attuale da cui ripararsi alla pensilina» sono la definizione letterale dell'asse 2.
  • [change] Ora il temporale e' un rinforzo della calamita' "rain": WeatherSystem dice quando e quanto (is_storm_wave(), storm_wave_level() 0..1), World lo trasforma in pixel leggendo un solo numero lerpato (_storm_amt). 📌 Weather.STORM non e' rimasto orfano ne' duplicato: il suo ramo in _weather_tint_color() e' stato tolto e lo stato ambientale passa ora dalla stessa strada della calamita' — due porte, una strada sola (World.gd:6268).
  • 📌 Scartato un quarto tipo di calamita' "storm": avrebbe obbligato a duplicare riparo, effetti, banner e relay, mentre il temporale e' una decorazione della pioggia, non un pericolo diverso.
  • [decisione] STORM_RAIN_CHANCE = 1.0 — tutte le piogge sono temporali. A 0,5 il temporale si vedrebbe una volta ogni ~10 minuti e una prova corta poteva non incontrarlo mai: su un ticket gia' bocciato perche' l'effetto non era raggiungibile, la rarita' era il rischio da non correre. La costante c'e' e si abbassa senza toccare altro.
  • [change] Il cielo si scurisce gia' dal preavviso (rampa di 12 s sui 20 di lead): la finestra attiva dura 5 secondi e non ci starebbe un fulmine. Il buio diventa un secondo avviso, e non tocca il bilanciamento — e' solo tinta e vignetta.
  • [change] Il fulmine disegnato c'e' (era la domanda col punto interrogativo di Ivan): due Line2D, nucleo giallo-caldo e contorno blu carico, 12 vertici per due decimi di secondo, nessuna passata fullscreen. 📌 E' l'unico pezzo del lampo che sopravvive alla grafica leggera, dove il velo bianco resta spento. La prima versione era bianca e dentro il velo bianco spariva.
  • [verifica] I numeri, misurati sui due casi GIUSTI (calamita' pioggia contro calamita' col temporale, non piu' RAIN contro STORM), luminanza media a HUD nascosto: pioggia 0,3507 → temporale 0,2194, cioe' −37%; preavviso 0,2423; lampo 0,7534; lampo in grafica leggera 0,3372 (velo spento, saetta presente); effetti forti spenti 0,3386. uniform darkness da 0,000 a 0,510.
  • [verifica] MP: la segnalazione del giro scorso non si ripresenta. Il lampo resta locale a ogni peer, zero pacchetti nuovi; il flag temporale viaggia dentro la stringa kind della RPC gia' esistente ("rain_storm", +6 byte una volta per calamita'), quindi la firma non cambia, gli id RPC non scalano, e una build vecchia lo normalizza a "rain". Il client non tira dadi.
  • ⚠️ NON MISURATO: gli fps sull'Honor con low_graphics durante il temporale — il telefono non c'e', e non e' stato sostituito con una misura sul Mac, dove gli fps a finestra mentono. MP verificato solo leggendo il relay, non a due peer. Il tuono non e' stato ascoltato (provino senza audio).
  • ⚠️ Da decidere, effetto collaterale della scelta a 1.0: il banner ora dice TEMPORALE per ogni pioggia in partita — la parola PIOGGIA sparisce dal gioco fuori dal tutorial. Si torna indietro cambiando una match in wave_display_name.
  • 📌 Due trappole ripagate e annotate nel provino: la cerimonia delle carte mette in pausa l'albero (scatti byte-identici, numeri a zero, zero errori nel log — sembrava una verifica riuscita); e HUD e' un CanvasLayer, non un CanvasItem, quindi il cast tornava null e l'HUD non si nascondeva dagli scatti.
  • ⚠️ Rientrata una seconda radice TMP/ dentro files/homeless_city/ (5,3 MB, su416_419/), creata da una sonda che scrive su un TMP/ relativo invece che sulla radice del repo: e' esattamente cio' che SU-458 aveva sgomberato. Non tracciata, quindi fuori dai commit, ma va cancellata a mano e la sonda che la crea va corretta.
  • [feat] SU-619 — l'avviso di versione nuova, che al giro precedente non era stato implementato affatto. Il ticket era rimasto fermo su una domanda («quale delle tre strade?») senza risposta. Decisa la strada 1 e costruita: un file JSON per ambiente (version_dev|stage|live.json), letto all'avvio, agganciato a Env.id() e generato dallo stesso comando di release che si lancia gia'.
  • 📌 Non e' un autoload, ed e' una scelta: il nodo lo crea Boot e lo appende alla radice, cosi' nasce solo nell'avvio vero — bot, headless, demo e sonde passano dal ramo _skip_splash() e non lo creano mai, quindi zero rete nei collaudi. L'overlay si appende da solo come CanvasLayer a layer 95, stesso schema di BetaGate.
  • ⚠️ Trovato un vicino di casa che nessuno aveva citato: BetaGate fa GIA' un controllo di versione remoto, ma e' fail-closed, solo su build beta Windows/macOS, e blocca. SU-619 e' l'opposto (fail-open, avvisa e basta). Tenuti separati di proposito, con la ragione scritta in testa a entrambi: fondere il confronto versioni in un punto solo legherebbe un presidio fail-closed a un avviso fail-open.
  • ⚠️ La riga del ticket che diceva «Settings.last_seen_version esiste gia' e va riusato» era SBAGLIATA, ed e' la trappola che avrebbe fatto tornare il ticket. Quel campo (SU-331) e' retrospettivo e locale — quale versione ho gia' visto *installata* — e alimenta il banner «ti abbiamo aggiornato». Scriverci dentro il numero remoto farebbe comparire quel banner al riavvio dopo, per un aggiornamento mai installato. Campo nuovo: update_notice_seen_version, e il provino verifica esplicitamente che i due non si tocchino.
  • ⚠️ Tre difetti veri, nessuno dei quali il compile-check poteva vedere: (1) add_child() sulla radice dentro Boot._ready() fallisce con «Parent node is busy» — il nodo restava orfano e l'HTTPRequest non sarebbe mai partito, visibile solo nell'avvio vero a finestra, ora differito; (2) un Control appeso a un CanvasLayer nudo resta 0x0, quindi il dimmer era invisibile e inerte, coi click che passavano alle voci del menu sotto — visto solo nello scatto; (3) JSON.parse_string() stampa un ERROR del motore su file storto, che su un log di device sembra un guasto: sostituito con JSON.new().parse().
  • [verifica] Provino headless 18 passati / 0 falliti, zero ERROR nel log; compile-check su 6 file, 0 falliti; avvio vero a finestra → [SU-619] nessun avviso: nessun manifest configurato, gioco identico a oggi. 📌 La fase 13 del provino da' al gioco il file prodotto davvero dallo script di release: le due sponde del contratto si toccano, invece di fidarsi di uno schema scritto due volte.
  • ⚠️ NON provato, ed e' il buco vero: il giro end-to-end su rete non esiste, perche' manca l'host. Una lettura da file non prova una lettura da rete (TLS, redirect, timeout). Il pulsante SCARICA non e' mai stato premuto (aprirebbe un browser sul Mac): verificato solo che l'URL scelto sia quello della piattaforma. Niente Android/iOS: la scelta url_android/url_ios e' provata solo sul ramo desktop.
  • [decisione] Cosa resta a Ivan: scegliere l'host, scriverlo in Env.UPDATE_MANIFEST_BASE (finisce con /) e pubblicare version_<env>.json. Candidato a costo zero: lo stesso repo pubblico da cui BetaGate legge gia' devices.json. Finche' la costante e' vuota il gioco non chiede niente a nessuno e si comporta esattamente come oggi. url_ios e url_desktop escono vuoti dal generatore (TestFlight non ha link pubblico stabile, i dmg cambiano link a ogni giro): li' l'invito compare senza pulsante, ed e' voluto.
  • [fix] SU-592 (KO, 3° giro) — il pinch non parte piu' da un dito appoggiato su un tasto TENUTO GIU'. ⚠️ L'ipotesi con cui era partito il lotto era falsa, e la sonda l'ha smentita subito: un dito che centra un tasto *libero* se lo prende gia' VirtualControls._input, e a World non arriva proprio — il caso letterale del KO passava gia' su HEAD. Una sonda diagnostica che sweepa le sequenze di dita ha trovato i due casi che riproducono davvero il sintomo.
  • 📌 C5, ed e' quello che vede Ivan: il ciclo dei tasti salta quelli gia' premuti (continue). Col tasto X tenuto giu' — cioe' la scoreggia col gatto in mano, la mossa nata con SU-643 lo stesso giorno — un secondo dito appoggiato li' sopra non lo consuma nessuno, arriva a World come «area libera», e al primo strisciamento apre il pinch: su HEAD pinch=(0,2), pad=false, barbone fermo.
  • 📌 C6: il rilascio del dito che guida il pad se lo mangia VirtualControls, quindi World._touch_liberi conserva un fantasma per sempre. Con un altro dito che occupa l'indice vecchio, il pollice che si riappoggia ne prende uno nuovo → due «dita libere» → pinch aperto di colpo, senza soglia. Risolto con _scorda_dita_morte() (World.gd:5131) e l'invariante «col pad fluttuante ogni dito libero diventa il pad, quindi pad spento = niente di annotato e' vivo».
  • [change] Come si stabilisce che un tocco e' su un pulsante: nuovo accessore di sola lettura VirtualControls.is_point_on_action_button() (:1366), geometrico, che riusa la stessa misura dello smistamento — il letterale 1.35 e' diventato BTN_HIT_FACTOR (:38). ⚠️ Lo smistamento di _input non e' stato riscritto, come vietava il ticket: li' il diff e' di 3 righe, solo il letterale che diventa costante.
  • ⚠️ Il secondo percorso (tasto B = mappa) NON era mai entrato: il giro precedente lo aveva diagnosticato e «passato a un altro lotto», e li' si era perso — git log -S "_release_all_inputs" si ferma a cffd462, di SU-224. Sistemato ora con _prova_a_riprendere_il_pad() (World.gd:5168).
  • [verifica] La sonda sa riprodurre il difetto, che e' l'unica cosa che rende la prova una prova: probe_su592_tocco_a_vuoto.gd da 15 a 24 casi0 KO sul codice nuovo, 6 KO su HEAD, sia headless sia a finestra su canvas 1223x768 (orizzontale vero, non i 64x64 del driver dummy che falsavano le coordinate al giro precedente). Regressione SU-328: 0 KO.
  • [fix] SU-613 (KO) — «COME SOPRA» diventa «AUTO». Nessuna stringa nuova: MENU_TOUCH_AUTO esisteva gia' tradotta, e la chiave morta MENU_TOUCH_COME_SOPRA e' stata tolta dal CSV. Cambiate anche le due righe di aiuto in tutte e 8 le colonne (АВТО, 自动). 📌 Il giro precedente aveva scelto «COME SOPRA» apposta, per distinguerlo dall'AUTO della riga di sopra che vuol dire «lo decide il dispositivo»: distinzione scartata da Ivan, e il commento nel codice ora lo dice, cosi' nessuno la ripristina credendo di correggere un errore.
  • ⚠️ ui_menu.csv in questo commit porta anche le quattro chiavi AGGIORNA_* di SU-619, che e' ancora in lavorazione: stesso file, due lotti. Non e' uno sconfinamento, e' un file condiviso.
  • [fix] SU-627 (KO) — l'ultima fila si aggancia in basso, la seconda galleggia in mezzo. Il giro precedente aveva messo le tre file nell'ordine giusto ma impilate dall'alto; Ivan le voleva ancorate al fondo dei potenziamenti. Ora in HUD.gd:_apply_safe_area la riga3 (gatto+bottiglia) si aggancia a grid_top + POWER_SLOT_SIZE - REUSE_SLOT_SIZE, cioe' al fondo della prima riga della griglia poteri, e la riga2 (trombetta+super) si centra nello spazio libero fra il fondo dei soldi e la cima della riga3. La riga1 (soldi) non si muove.
  • [verifica] Le misure prima e dopo, che il brief chiedeva prima di toccare: telefono 844x390 — soldi fondo 48, riga2 49-81, riga3 83-115, prima riga griglia fondo 126. Dopo il fix la riga3 arriva esattamente a 126, e i due vuoti sopra/sotto la riga2 sono 7 px e 7 px.
  • ⚠️ Bug intercettato in corsa, che annullava il fix: un clamp preesistente di SU-530 — pensato per il vecchio blocco a due righe di altezza fissa — scattava anche sul telefono e faceva ricadere sul vecchio impilamento. Isolato al solo caso di overflow orizzontale verso la colonna vite/XP.
  • ⚠️ Su desktop 1024x768 l'allineamento NON e' stato fatto, ed e' una scelta: li' il blocco soldi scende gia' sotto il pannello per una soglia di larghezza preesistente, e soldi/riga2/riga3/griglia condividono la stessa colonna verticale — un bordo condiviso fra riga3 e griglia sarebbe una sovrapposizione di icone, non un allineamento. Lasciato l'impilamento vecchio, identico pixel per pixel. Un allineamento vero li' vorrebbe una seconda colonna per la griglia, fuori dai due file del ticket.
  • [fix] SU-614 (KO) — il 3-2-1 aspetta che il velo della metro sia uscito, e le cifre tornano lunghe. Erano due cose, e il giro precedente ne aveva chiuso zero: si era fermato a una domanda («vuoi rialzare quel tempo?») dopo aver correttamente smentito l'ipotesi del ticket (nessun timer sporco: la sonda vedeva sempre [3,2,1,VIA!]).
  • 📌 La causa vera l'aveva indovinata Ivan, e la sonda non poteva vederla: misurava il countdown, non quello che si vede. La transizione della metro dura ~0,96-0,97 s dal tornello, e prima della cura la prima cifra del secondo giro partiva a tempo zero — quindi scorreva quasi tutta sotto il velo. Le cifre c'erano tutte, semplicemente non erano visibili: ecco «parte gia' da 2». Ora BonusLevel._ready() cerca il nodo ViaggioMetro sotto la radice e aspetta il suo tree_exited, riusando lo stesso meccanismo che GameState.metro_transit_follow() gia' adopera per la stessa ragione.
  • [change] Il tempo si rialza senza rispiegare: INTRO_CIFRA_VELOCE_SEC/INTRO_VIA_VELOCE_SEC tornano uguali a quelle normali (0,7 s per cifra, 0,55 s per il VIA), mentre la spiegazione resta a 0 s dal secondo giro — la decisione di Ivan del 21/08 vale ancora, era solo la compressione del countdown a essere di troppo. Verificato che _intro_veloce non comprimeva nient'altro (tutti e tre i suoi usi letti).
  • ⚠️ Effetto collaterale trovato SOLO guardando gli scatti, invisibile nei log: allungando l'attesa, la label della spiegazione — visibile di default alla costruzione — restava esposta durante il ritiro del velo nelle discese veloci, dove non deve mai comparire. Nascosta subito in _ready() quando _intro_veloce.
  • [verifica] Regressione SU-492 ricontrollata: la seconda discesa resta piu' veloce della prima (2,65 s contro 5,55 s), ma ora solo grazie alla spiegazione saltata, non alle cifre compresse.
  • ⚠️ NON provato: i due documenti non sono codice e non sono stati misurati su una run vera — fin dove arriva giro in partita (decide se i topi partono al giro 1 o al 2), i topi accesi in probe_su551_treni, e un giro su Honor 10 a freddo restano elencati come ticket, non spacciati per fatti. Su SU-633: provate solo le due forme 844x390 e 1024x768, non una finestra alta.

SOLLEONE ha un sole che ti cerca e cactus che uccidono i gatti, la pensilina salva davvero, la tempesta si vede2026-08-28

  • [change] SU-611 — il sole di SOLLEONE (scripts/world/SunHunter.gd, nuovo). Gira per la mappa, ogni tanto si porta sopra di te, mette a terra un cerchio di preavviso e poi spara. I numeri, col perche': diametro 96 px (= BLOCK_SIZE, circa un quarto dell'inquadratura a zoom 2,5); preavviso 1,8 s, calcolato sul caso peggiore col pollice — partenza al centro (48 px) con barbone fradicio (40,5 px/s → 1,19 s) piu' ~0,5 s di reazione = 1,69 s, e a velocita' piena restano ~0,8 s di margine; intervallo 18 s, cioe' ~90 tentativi in una run da 30', tutti schivabili. Mira ≤1,2 s, lampo 0,32 s.
  • [decisione] Il raggio NON danneggia gli NPC: sono host-authoritative, e farli bruciare vorrebbe dire RPC nuovi — cioe' toccare l'MP, che era vietato. Una vita per colpo garantita da _colpo_dato; il cactus ha un cooldown condiviso fra tutti i cactus (1,5 s, statico) cosi' un gruppetto costa una vita, non quattro.
  • [change] SU-610 — i cactus (scripts/world/Cactus.gd, nuovo): fermi, fanno danno al contatto, e il gatto che li colpisce muore — rimbalza, esce dal bordo basso, il contatore scala.
  • 📌 Come sono stati evitati i tiri di dado nuovi, che era il vincolo che decideva il lotto: i cactus pescano da _rng_cactus, un dado separato seedato allo stesso seme con seed_used ^ 0xCAC705 (WorldGenerator.gd:368 e :803) — lo stesso schema gia' usato da _rng_luci (SU-210) e _rng_banchi (SU-408). La sequenza dell'rng principale non si sposta di un tiro, quindi le citta' degli altri sette quartieri restano identiche. La chiamata e' l'ultima di generate(), dove nessuno legge piu' _prop_spots; e in _punto_libero_cactus i 14 tentativi si consumano sempre, riuscito o no, cosi' il consumo non dipende dagli scarti. SunHunter.gd e Cactus.gd non contengono nessun randf/randi: il giro del sole e' funzione pura del cronometro, due seni.
  • [verifica] Prova empirica del determinismo: due processi diversi, stesso seme 4242, impronta identica (321.7,973.9 · 203.9,1066.9 · 131.3,1076.1 · 345.2,1124.9 · 363.4,240.8 · 545.1,244.0 · 431.1,157.1). Controprova al CENTRO: 0 cactus e nessun nodo SoleSolleone.
  • 📌 Due numeri corretti guardando gli scatti, non il codice: al primo giro su telefono il sole finiva nascosto dietro la fila delle vite (ALTEZZA_SOLE da 74 a 52), e un cactus nasceva a fianco della fontana (CLEAR_ARREDO da 38 a 52).
  • [change] SU-629 — la pensilina diventa una vera zona sicura durante le calamita'. Vive in NPC.gd, la classe base, e non in ModularNPC.gd: il _physics_process della base gira prima della logica d'archetipo, ed e' l'unico punto dove si puo' fermare chi scrive prima che scriva. 📌 Se la ritirata girasse dopo l'inseguimento, il nemico resterebbe incollato al bordo del riparo a tremare — che e' peggio di uno che attacca.
  • 📌 La retata passa senza che nessuno la escluda: RaidOfficer e PoliceOfficer hanno un _physics_process proprio che non chiama la super, quindi non arrivano proprio al codice della zona sicura. Non c'e' un if da mantenere. Misurato: chiude 149 px in 1,5 s con la zona sicura accesa.
  • 📌 A ondata finita non si azzera niente, ed e' voluto: la lista degli occupanti torna vuota, la coda di ritirata scade, ognuno riprende da dov'era. Nessuno stato residuo da ripulire.
  • ⚠️ Bug trovato e corretto, che bloccava tutto: ShelterZone.gd citava GameState come identificatore globale. Lanciato con -s lo script si compila prima che gli autoload esistano → «Identifier not found» → ogni pensilina della citta' restava senza script. Ora passa da get_node_or_null("/root/GameState"). E' la stessa famiglia delle sonde extends Node2D che vanno lanciate come scena.
  • [change] SU-621 — la tempesta si vede. Misurato sulla luminanza: pioggia 0,343 contro tempesta 0,239 (prima erano 0,344 e 0,278, cioe' quasi indistinguibili). Il lampo ora illumina la citta' (0,474) invece di annebbiarla.
  • 📌 Con low_graphics il rettangolo bianco a tutto schermo non si disegna MAI: il lampo viaggia su uniform gia' scritti a ogni frame, zero passate in piu'. E' lo stesso overlay fullscreen che SU-583 aveva tolto dal gioco per gli fps sull'Honor — la causa e' stata rimossa, non mitigata.
  • ⚠️ Segnalato: oggi NESSUNO in gioco porta il meteo a STORM. apply_district_weather() cerca Quartieri.fixed_weather(), che non esiste. Il temporale si vede solo dal pannello di debug finche' non lo si assegna a un quartiere.
  • [change] SU-609 — tools/release/prepare_dev_tag.sh + il comando /tag-intermedio. Condivide con prepare_release.sh le tre funzioni di release_git_lib.sh, estratte e non duplicate come chiedeva il criterio; completata anche la migrazione di prepare_release.sh:136-149, che era l'ultimo blocco non ancora passato alla libreria. Tag sempre con suffisso -dev-YYYYMMDD-SLUG, nessun bump di versione, nessun export. Documentato in WORKFLOW/04_RELEASE.md con la stessa regola d'oro: mai in autonomia.
  • ⚠️ SU-612 — trovato un file sbagliato, non solo vecchio: STORE_ASSETS/google_play/feature_graphic_1024x500.png conteneva una folla di personaggi estranei al gioco, senza nemmeno il logo. Ripristinato al banner storico committato. 📌 Dentro la build era gia' tutto aggiornato dal 12/08 (SU-367): iOS/icon.png e le tre assets/icons_android/ — l'icona vecchia stava solo nella scheda di Play Console, che si cambia da browser e resta a Ivan.
  • ⚠️ SU-630 — la colonna chiesta ESISTEVA GIA', aggiunta da un ticket scorrelato (SU-378, il censimento prestazioni del 14/08): Modello Android, popolata 20 tester su 40. Non ne e' stata creata una seconda, che avrebbe duplicato il dato. E le 20 righe vuote restano vuote: la lista dei dispositivi autorizzati non contiene modelli (solo codici SU-XXXX-XXXX-XXXX), e incrociare col censimento e' vietato dalla policy scritta in BETATESTING/DICHIARAZIONI_STORE_TELEMETRIA.md.
  • 📌 Nota sul commit precedente: e3ee192 conteneva gia', senza che la sua voce lo dicesse, le tre chiavi di SOLLEONE in ui_mondo.csv (MONDO_SOLE_COLPO, MONDO_CACTUS_SPINE, MONDO_CACTUS_GATTO, 8 lingue) — erano state scritte da questo lotto prima che quel commit partisse.
  • ⚠️ NON provato: niente su device, niente in MP vero (il determinismo e' provato fra due processi locali); sole e cactus mai provati sotto la retata ne' nel bonus metro; gli fps sull'Honor per la tempesta — il telefono non c'era, e non e' stata sostituita con una misura sul Mac, dove gli fps a finestra mentono comunque. Gli sprite di sole e cactus sono segnaposto disegnati in codice, in attesa del giro Codex.

Il caffe' al posto dell'energy drink, come si azzera una board EOS, e l'inventario della provenienza2026-08-28

  • [change] SU-616 — il sonno nel bidone si compra col caffe', in tutte e otto le lingue (ui_mondo.csv, chiavi TUTORIAL_ISTR_BARRE e TUTORIAL_ISTR_BIDONE, 16 celle). 📌 Non e' stata una sostituzione meccanica: in francese e spagnolo il sostantivo passa da femminile («boisson», «bebida») a maschile («cafe»), quindi cambia l'articolo; in cinese cambia il classificatore, da 一罐 (lattina) a 一杯 (tazza). Allineati anche i testi di ripiego di I18n.t/tf in TutorialManager.gd, che sono quelli che si vedono se la chiave manca.
  • [verifica] Il numero del grep: nei CSV da 16 a 0. Nei .gd restano 11 occorrenze su 21, tutte in Interactable.gd e Player.gd — file fuori dal perimetro del lotto — e sono identificatori interni, non testo mostrato: spawn_bin_energydrink_fx(), _fx_energydrink_tex, res://assets/sprites/energydrink.png. ⚠️ Il criterio del ticket dice «zero occorrenze anche nei .gd»: quella meta' resta da fare, ed e' una rinomina di API interne, non di testi.
  • 📌 Non toccata la chiave "energy" in BIN_ROW_LOOT: e' condivisa col dizionario BIN_FORCED_ROLL di Interactable.gd, e rinominarla da una parte sola avrebbe rotto il contratto fra i due.
  • ⚠️ Lo sprite del caffe' NON e' stato generato, di proposito: il ticket dice «si sceglie mostrando le varianti a Ivan», quindi non e' chiudibile senza di lui. Consegnati invece il path e la misura di quello attuale (assets/sprites/energydrink.png, 20×28 RGBA) e il prompt per le tre varianti — tazzina, bicchiere da asporto, caffettiera.
  • ⚠️ SU-619 NON e' stato implementato, e la ragione e' che manca la fonte del dato: non esiste da nessuna parte un «numero di versione pubblicata». I due Apps Script del progetto sono registri write-only (POST), non feed di versione. Tre strade nel report, con costo. 📌 E c'e' una collisione da conoscere prima di implementare: Settings.last_seen_version (SU-331) e' gia' consumato da MainMenu._check_update_notice() con semantica opposta — confronta la versione *installata* con l'ultima vista, per il banner «ti abbiamo aggiornato», e non tocca mai la rete. Scriverci dentro la versione *remota* farebbe comparire il banner sbagliato: serve un campo separato.
  • [doc] SU-636 — EOS_RESET_CLASSIFICHE.md (223 righe), con tre fatti misurati sul deployment dev, in sola lettura e con un PUID di 32 zeri per non toccare i dati di nessuno: (1) client_credentials risponde HTTP 200, quindi le credenziali funzionano fuori dal gioco; (2) la client policy nega comunque, 403 policy_requires_user su stats e leaderboard — e ⚠️ il client_id e' identico nei tre ambienti, quindi il limite vale uguale su stage e live; (3) DELETE /stats/v1/<dep>/stats/<puid> risponde 400 MethodRejection, non 403. 📌 La differenza fra i due codici e' tutta la risposta del ticket: un 403 direbbe «esiste, chiedi il permesso», un 400 sul metodo dice che la porta non c'e'. Nessun reset in blocco via API.
  • ⚠️ Il reset vero non e' stato eseguito: il pulsante «Reset Player Stats» sta dietro il login Epic di Ivan, e il cambio di BOARD_PREFIX vuole otto board create a mano nel portale. Serve Ivan al browser, con un PUID letto da una build di debug — in release _mask_puid() ne stampa solo quattro cifre.
  • [doc] SU-608 — INVENTARIO_PROVENIENZA_ASSET.md (363 righe), col numero invece dell'impressione: 159 voci con fonte nominata per singolo file (105 sprite, 47 audio, 2 font, 5 grafiche in codice) contro 316 PNG non determinati, 35 effetti senza record del piano attivo, e 8.647 celle di traduzione su 8.751 mai lette da una persona (98,8%).
  • ⚠️ «879 chiavi» era un dato vecchio: oggi sono 1.250 (1.232 distinte), 10.001 celle, zero vuote. Contate con un parser CSV vero — wc -l ne dava 2.329 per via delle celle multiriga.
  • 📌 Una riga e' stata riscritta perche' due fonti si contraddicevano: era stato scritto che di Gemini «non risulta materiale in produzione», ma il CHANGELOG lo chiama «il generatore che usiamo». I due fatti non si conciliano, quindi ora e' «non determinato» — ed e' la prima domanda da girare a Ivan, perche' decide se i moduli anti-filigrana di PixelRefiner (SU-642) cancellerebbero una marca a nostro favore.
  • ⚠️ Tre buchi da chiudere, in ordine di facilita': il piano OpenAI/Codex al momento delle generazioni (si chiude in cinque minuti guardando la fatturazione); l'origine di StreetU-Pixel.ttf, che se e' un derivato OFL porta obblighi precisi, nome riservato compreso — ed e' il punto aperto piu' serio; e il debito ElevenLabs (~35 effetti generati col piano gratuito), che non e' stato possibile riconfermare perche' la chiave API non ha user_read.
  • 📌 Mancano un LICENSE in radice e i campi application/copyright in export_presets.cfg (righe 307, 574, 577): sono i metadati del binario che distribuiamo, e costano niente.

SU-645: morire in metropolitana non blocca piu' il gioco, e il blocco era piu' vecchio del ticket2026-08-28

  • ⚠️ Il difetto delle statistiche non era dove sembrava. Dentro il tunnel il calo era gia' fermo (_congela_superficie() spegne il _process di GameState). Scendeva nelle due code della transizione — 0,22 s mentre il vagone entra prima che entra() apra il livello, 0,37 s mentre la galleria sfila dopo che _risali_ora() ha gia' scongelato — e per tutti i 0,95 s del cambio fermata, che non congela niente.
  • [decisione] Sospensione per TUTTA la durata del viaggio, non «due secondi» come proponeva Ivan. Si apre all'inizio e si chiude quando il velo ViaggioMetro esce di scena (tree_exited): cosi' segue il viaggio anche se un giorno durera' di piu', mentre due secondi tarati a occhio si romperebbero al primo telefono lento. Il tetto di 5 s e' solo rete di sicurezza con push_warning, non il meccanismo.
  • [decisione] L'orologio continua a scorrere: la guardia e' solo sul prelievo delle statistiche. Fermarlo per ~1 s reale a ogni corsa regalerebbe minuti di gioco a chi prende spesso la metro — e dentro il tunnel l'orologio e' gia' fermo da prima. Guardia unica, GameState.stats_drain_suspended() (riga 1259), letta da _decay_stats (riga 1327) e da _apply_wave_stat_drain (riga 992): una sola, non due copie che possono divergere.
  • ⚠️ IL BLOCCO: riprodotto in tutti e tre i momenti, e la causa e' World._on_game_over() che fa get_tree().paused = true (World.gd:7324). BonusLevel e' figlio della radice ed e' pausabile: da li' non gira piu', non arriva mai a _termina() ne' a _risali_ora(), quindi _scongela_superficie() non viene chiamata mai. Risultato: citta' in PROCESS_MODE_DISABLED, strato HUD visible = false, e la schermata di fine partita costruita sotto un HUD invisibile e senza input. Nessuno puo' piu' riaccendere niente. Misurato prima del rimedio: hud_acceso=false, process_mode=4, bonus_attivo=true dopo 22 secondi. Dopo: true / 0 / false in tutti e tre i momenti.
  • 📌 E il tester non era necessariamente nel bonus: gli stessi tween del velo girano sul motore ormai fermo, quindi il caso piu' probabile e' la morte durante il CAMBIO FERMATA — 0,95 s in cui non si congela niente, che e' alla lettera «l'animazione di spostamento della metro» della segnalazione.
  • [fix] Il rimedio sta in GameState.trigger_game_over(), PRIMA di emit_signal: _abort_metro_transitions() smonta il tunnel (BonusLevel.abort_for_game_over()), toglie il velo e azzera la sospensione — cosi' HUD e World trovano lo stesso stato di un game over per strada, e non e' servito nessun ramo nuovo nell'HUD. In piu' puo_entrare() ora rifiuta a run finita, altrimenti il tunnel si aprirebbe sopra la schermata di fine partita.
  • ⚠️ TERZO BLOCCO, trovato dalla sonda e che nessuno cercava: la citta' restava congelata anche sulla RISALITA NORMALE. _scongela_superficie() faceva var c: CanvasLayer = v["nodo"] su nodi eventualmente gia' liberati, e l'assegnazione tipizzata di un'istanza morta e' di per se' uno SCRIPT ERROR che interrompe la funzione a meta'. Ora si valida con is_instance_valid() prima di assegnare. 📌 E' la stessa famiglia di trappole di String(null): un errore che il compile-check non vede e che in gioco spegne mezzo mondo.
  • [verifica] I numeri (tools/autotest/probe_su645.sh): senza sospensione le statistiche scendono a 78,5489 / 78,1963 / 78,7975 in 2 s; con la sospensione restano 80,0000 / 80,0000 / 80,0000 identiche. Col calo da ondata forzato: sospese identiche, libere 68,4649 / 68,1600 / 68,7620. Tre discese e risalite vere: 40,0000 prima e dopo, tutte e tre.
  • MP: il bonus e' gia' SP-only (puo_entrare rifiuta in rete) e la sospensione vive su GameState, autoload locale al peer: e' del giocatore in metro, mai globale. Nessuna pausa globale aggiunta.
  • ⚠️ NON provato su device, ed e' la prova che conta: in headless metro_ride_da e PixelWipe.copri non viaggiano, quindi le finestre reali (0,22 / 0,37 / 0,95 s) durano zero sul banco — il meccanismo e' stato provato a secondi veri e il ramo tree_exited con un velo costruito a mano. Da provare a mano: scendere in metro con la fame quasi a zero e guardare le barre ferme durante l'animazione; morire di proposito durante un cambio fermata e verificare che la parete scura se ne vada e arrivi il giornale.
  • ⚠️ Due sonde preesistenti rosse, NON regressioni: probe_su467.sh e probe_su465.sh falliscono identiche su HEAD (verificato con una copia da git archive). Meritano un ticket a parte.

Fame e sonno, PixelRefiner e il nome del barbone: tre decisioni, e una filigrana che nessuno cercava2026-08-28

  • ⚠️ SU-617 — il ticket propone un canale che NON ESISTE PIÙ. Chiedeva di pesare «la vignetta a bordo schermo che già esiste (energy_intensity)», ma quella vignetta è stata rimossa il 2026-08-26 da una decisione di Ivan (SU-583: si vedeva pochissimo e il suo ColorRect fullscreen costava una passata intera a ogni frame sull'Honor). HUD.gd:11-16 lo dice per esteso.
  • ⚠️ E il difetto non è che manchi un canale: sono già sei, tutti attivi dal 50%. Il difetto è il TEMPO. Misurato sulle costanti (GAME_MINUTES_PER_SECOND = 2.4, DifficultyManager.gd:161-181): a 30 minuti di run la fame passa dal 50% alla morte in 10,4 secondi, mentre WARN_INTERVAL = 15.0 (HUD.gd:590). 📌 Quindi la scala a tre gradini spara un solo messaggio — il più mite — e poi è game over. Non serve un canale nuovo: serve che la cadenza degli avvisi sia proporzionale al tempo che resta.
  • 📌 Altro fatto che cambia il peso della cosa: hunger <= 0 è game over, non una vita persa (GameState.gd:1419-1420). Non è un avviso che si può permettere di arrivare tardi.
  • [doc] Raccomandato per SU-617: cadenza proporzionale al tempo residuo più la barra che si tinge dal 50% — l'unico canale permanente, e l'unico che sul telefono non dipende dallo zoom. ⚠️ Domanda che blocca: SU-339 aveva tolto il colore dalla barra *di proposito*. Se non si riapre quella decisione, resta metà rimedio — e su telefono è la metà che serve.
  • ⚠️ SU-642 — trovato, e nessuno lo stava cercando: PixelRefiner contiene tre moduli per RIMUOVERE LA FILIGRANA DI GEMINI, cioè del generatore che usiamo. 📌 Va deciso a occhi aperti insieme a SU-608: cancellare una marca di provenienza è l'esatto contrario di quello che SU-608 sta costruendo, cioè la prova documentata di come sono nati i nostri asset.
  • [doc] SU-642, il resto: licenza MIT, gira nel browser, zero modelli da scaricare, deterministico per costruzione (k-means senza sorteggio, test golden byte per byte) — quindi il requisito «ripetibile» è soddisfatto. Ma non ha CLI, ed è quello il vero ostacolo alla pipeline. Non è stato eseguito: servirebbe automazione browser o Node+pnpm sul Mac, e nessuna delle due era autorizzata. Il confronto prima/dopo resta da fare.
  • ⚠️ Due misure scomode uscite da SU-642: arch_delivery_female_v01_sheet.png ha 8.901 colori distinti su 10.069 pixel visibili (88,4%) e 11% di alfa parziale — non è pixel art, è un'immagine piccola. E la pipeline personaggi non usa BOX: usa LANCZOS+NEAREST (import_chatgpt_sprites.py:361-362), contro la regola che ci siamo dati.
  • [doc] SU-642, sulla proprietà (la domanda che il ticket dice pesare di più): no, un passaggio automatico non rende nostro niente — ed è lo stesso motivo per cui non lo rende il nostro LANCZOS+NEAREST di oggi. La tutela segue le scelte creative di una persona.
  • [doc] SU-409 — raccomandata la strada A: nickname dell'account e nome del personaggio restano due cose distinte. ⚠️ Ma un fatto blocca tutto: il gattile esiste solo a Gattopoli, mentre la proposta di Ivan chiede «una lettera per ogni quartiere, 50 gatti di *quel* quartiere». O il gattile entra in tutti i quartieri, o cambia il criterio.
  • 📌 Il «50» sarebbe il terzo significato dello stesso numero nel progetto, e il primo dei tre (MetaProgress.gd:29, gatti_segnati) è già il prerequisito dell'unico quartiere che il gattile ce l'ha.
  • ⚠️ Un documento è andato perso, e va detto: DESIGN_SU-617_fame_e_sonno.md era già stato scritto dal lotto interrotto ed è stato sovrascritto senza leggerlo. Non era mai entrato in git e non è recuperabile. Il rimpiazzo è completo e verificato sul codice di oggi, e la sua raccomandazione coincide con quella che SU-620 gli attribuisce, quindi la coerenza fra i due regge — ma se il primo conteneva misure che il secondo non ha, sono perse. 📌 La lezione è di processo: un documento prodotto da un agente morto va committato subito, non lasciato sul disco.

Dove sta l'avviso del maltempo: la premessa del ticket non reggeva alla misura2026-08-28

  • [doc] OPUS_BRIEFS/DESIGN_SU-620_avviso_maltempo.md, con figure annotate in TMP/su620/ (provino nuovo scripts/tools/shot_su620_angolo.gd + tools/autotest/shot_su620.sh): scatti veri dal viewport e rettangoli letti dai nodi, non stimati. Il ticket chiedeva «uno schizzo o uno screenshot annotato» e «il caso peggiore va disegnato, non immaginato».
  • ⚠️ La premessa del ticket è sbagliata su due punti, e il primo cambia tutto: l'avviso della calamità NON è in alto a destra, è in basso al centro. Ce l'ha portato SU-272, un KO di Ivan del 2026-08-04, proprio perché prima stava in alto «sotto livello e vite» e copriva la città (HUD.gd:4174-4177). Quello che si accavalla con le vite è la pila delle notifiche, che parte dal fondo della barra XP e prende tutta la larghezza. 📌 Spostare l'avviso lassù avrebbe riaperto un KO già pagato, che è esattamente il modo in cui nascono i giri di rilavorazione.
  • ⚠️ Secondo punto: sul telefono la colonna non è più stretta, è più LARGA. Il gioco è solo orizzontale (project.godot:91, orientation=4) e con canvas_items/expand un 844×390 diventa un canvas 1662×768: l'altezza resta, la larghezza cresce. Corridoio fra le vite e la colonna: 136 px sul tablet 4:3 contro 367 px sul telefonoil caso stretto è il tablet, non il telefono come diceva il ticket. Il problema del telefono è un altro: la taglia, perché 1 unità-canvas vale 0,51 px veri e il font 16 esce a 8 px fisici.
  • [doc] Il caso peggiore, disegnato invece che immaginato, con l'esito che due delle combinazioni non possono esistere: preavviso meteo + barra dell'ondata sono due *fasi dello stesso banner* (_update_wave_banner), e due calamità insieme sono escluse da _schedule_next_wave(). La coppia da progettare è una sola: countdown della retata + avviso meteo.
  • [doc] Raccomandato: il banner grosso non si muove, si sdoppia. In basso resta quello leggibile (SU-272 non si riapre), in alto a destra va un promemoria compatto di una riga. Sul telefono il promemoria non basta da solo — a 8 px non si legge — e lì il canale vero resta il banner in basso.
  • 📌 Due cose sono già gratis, ed è la parte che di solito si rompe: il pulsante pausa è agganciato a TOP_RIGHT_STACK_H e scende da solo se quella costante cresce; e _hud_blocker_rects() (HUD.gd:4688) legge i rettangoli veri dei nodi, quindi un elemento nuovo si fa scansare dalle notifiche aggiungendolo a quella lista, senza riscrivere niente.

La SUPER SCOREGGIA, il terremoto che torna a zero esatto, e il game over su tre pagine2026-08-28

  • [change] SU-644 — RUTTO SISMICO diventa SUPER SCOREGGIA in tutte e otto le lingue (ui_gioco.csv righe 14-15): SUPER SCOREGGIA / SUPER FART / SUPER PROUT / SÚPER PEDO / SUPER FURZ / СУПЕРПУК / 超级臭屁 / SUPER PUM. Il verso passa da BUUUURP! a PRRRRAAAP!. ⚠️ Le chiavi e l'id stomaco_ferro NON sono stati rinominati (l'id tocca i salvataggi, le chiavi muoverebbero anche shot_su277_schermate.gd): quindi grep -i rutto le trova ancora, ma nessun valore mostrato al giocatore contiene «RUTTO», verificato su CSV e .translation.
  • ⚠️ Il compile-check NON rigenera i .translation, che restavano fermi al 24/08 con dentro «RUTTO SISMICO»: è servito un --import forzato. È la stessa famiglia di trappole del «modificato ma non reimportato»: la modifica sembra non funzionare e non è colpa del codice.
  • [change] Il terremoto riusa l'impianto della scossa che c'era già (SU-245) invece di aggiungerne un secondo: due cose che scrivono camera.offset si sovrascrivono, e quando una finisce mentre l'altra vive resta la deriva. Decadimento, frequenza e modalità sono diventati per-scossa; la monetina resta identica a prima. Il sisma è su due assi a frequenze non multiple, così è un terremoto e non un rinculo.
  • [verifica] I numeri chiesti dal criterio, misurati su camera.offset e non a occhio: prima (0,0) → durante 10,18 px (tetto 17,40) → dopo (0,0) esatto, ampiezza residua 0.000000. Durata dichiarata 0,90 s, misurata 0,87 s; frequenza 26. In pausa non parte (ampiezza 0, offset fermo); al game over si spegne — e lo spegnimento sta prima di ogni ramo, quindi vale anche per lo spettatore MP.
  • [change] Il suono nuovo si aggancia per ID, non per tipo di effetto (SFX_BY_ID, che vince su SFX_BY_FX): 📌 SFX_BY_FX["gas"] serve a quattro super — cambiarlo avrebbe cambiato il suono anche ad AURA MEFITICA, AMNESIA COLLETTIVA e SCOREGGIA ATOMICA. Scartato anche sdoppiare il tipo fx, che avrebbe raddoppiato pure la nuvola visiva.
  • ⚠️ Il suono NON è stato generato, di proposito: il ticket vuole una scelta in A/B con Ivan, e c'è il nodo aperto della licenza ElevenLabs (gli effetti generati col piano gratuito vanno rifatti da abbonato). Prompt e specifica sono nel codice, assets/sounds/super_scoreggia.wav è già agganciato, e finché il file manca si sente il fart.wav di oggi. Il numero da battere è 0,48 s (attacco 15 ms, 2 colpi, dominante 366 Hz), misurato con tools/analizza_sfx.py.
  • [change] SU-643 — la scoreggia col tasto X tenuto, soglia 300 ms. Senza gatto parte subito come prima (nessun ritardo nuovo introdotto a chi non ha un gatto in mano); col gatto la pressione si pesa e il lancio va al rilascio. ⚠️ Scartato is_action_just_released, che è vero un frame solo: un evento iniettato fuori dalla fase d'input lo manca, e misurando la pressione breve non lanciava più il gatto. Al suo posto un controllo sull'orologio, che smaschera anche la pressione rimasta appesa durante il busking.
  • [change] SU-623 — dentro la nuvola i nemici non tirano più. Il meccanismo (THROW_BLOCK_DURATION, can_throw_now()) sta in NPC.gd perché il rallentamento riguarda chi ti insegue e il blocco dei tiri riguarda gente diversa; le quattro guardie che lo interrogano sono in ModularNPC.gd (righe 905, 1165, 1320, 1432), dove vivono tutti e quattro i lanciatori — tossico, spazzino, gattara, giullare. 📌 La ricarica continua a scorrere sotto la guardia e si ferma a zero: a effetto scaduto parte un tiro e non la raffica arretrata, che è il criterio, ottenuto per costruzione invece che con un contatore in più.
  • [change] SU-633 — il game over diventa tre pagine. Pagina 1 solo testata, titolone e foto; pagina 2 nuova coi due riquadri FATTI E NUMERI e EXTRA! EXTRA! a corpo grande; pagina 3 la classifica com'era, coi due pulsanti dove stavano. Il giro interno riusa lo stesso PageCurl dell'uscita (non toccato: era già generico), col velo che non scende sul giro interno.
  • 📌 Il criterio nascosto di SU-633 era il meccanismo che rimpicciolisce il testo, e alzare il font senza toccarlo avrebbe fatto tornare il ticket KO senza che nessuno capisse perché: _fit_step() ora ha pavimento e pezzi sacrificabili per pagina (FS_BODY_FLOOR_PAGE2 = 17). Sotto quel pavimento non si scende mai: si accorcia il contenuto, non lo si rimpicciolisce. Misurato: _fs_body = 20 su telefono, 31 su tablet.
  • [change] Tagliate le quattro colonne di cronaca in prosa e il testo finto di riempimento (_add_soldi/_add_punteggio/_add_lattine/_add_impresa/_add_fake_text e INK_FAKE, rimosse perché morte dopo il taglio): erano l'unica cosa che restava piccola, e a corpo grande non ci stavano. È una scelta di prodotto, non un effetto collaterale.
  • ⚠️ NON provato: il suono (non esiste ancora); la soglia dei 300 ms col dito, che il ticket chiede esplicitamente — il rischio è il lancio del gatto, che ora parte al rilascio: se in fuga sembra molliccio si scende a 0,25; il terremoto a occhio su telefono (12 px su schermo piccolo potrebbero essere troppi); nessuna prova su device né in MP.
  • ⚠️ Gap MP preesistente, segnalato e non toccato: sull'host la scoreggia di un client non arma né il rallentamento né il blocco dei tiri sugli NPC (player_misdemeanor è locale, net_fart fa solo player-vs-player). Era così anche prima e vale per entrambi gli effetti.

La nonna senza magenta, e i venditori che da ubriachi restano se' stessi2026-08-28

  • [fix] SU-635 — 15 pixel magenta nella nonna, e non erano il difetto che sembravano. Il file è assets/sprites/npc/bodies/arch_grandma_female_v01_sheet.png, l'unica delle cinque varianti (v01-v05) con l'artefatto: le altre quattro e la sheet «drunk» sono risultate pulite a uno scan pixel per pixel. 📌 Tutti e 15 i pixel erano INTERNI alla sagoma — fiancheggiati da pelle, capelli o vestito opachi su entrambi i lati, mai a contatto col bordo trasparente. Quindi era solo «gonna mangiata dal ritaglio» (colore vero sostituito da magenta) e mai «trasparenza mal ritagliata»: nessun buco da aprire. I due difetti hanno rimedi opposti, e distinguerli era il punto.
  • Celle coinvolte: idle_se (col0) e i frame 2-3 di walk_se/walk_ne, quasi tutte alla stessa posizione locale (~x20, y13) — un pixel fisso del busto che il taglio del corpo base non aveva mai colorato bene. Corretti con uno script Python deterministico (mediana dei vicini opachi non-magenta): nessuna rigenerazione dell'arte, nessun ridimensionamento. Verificato dopo: 0 pixel magenta opachi su tutto il foglio 160×160. Screenshot delle sei direzioni in movimento in TMP/su635/.
  • [fix] SU-634 — i venditori da ubriachi diventavano tutti «operaio ubriaco», e la causa era un id fisso contro un corpo pescato: ModularNPC.gd chiedeva get_drunk_frames_path_for("arch_vendor")costante — mentre il corpo *sobrio* del venditore viene estratto a caso da un pool di 72 corpi civili (pick_pool_variant_for, NPCDatabase.gd:967). Il corpo ubriaco non seguiva mai quello pescato.
  • 📌 Il fix non consuma RNG, ed è la parte che protegge il multiplayer: la nuova archetype_id_from_variant_path() (NPCDatabase.gd:984) risale all'archetipo vero dal path già pescato, con la stessa regex di _classify_variant — nessuna estrazione nuova, quindi il seed condiviso resta intatto. Usata solo per arch_vendor. Prova: TMP/su634_mercato/su634m_sobrio.png e su634m_ubriaco2.png, stesso banco e stessa turista, cambia solo la testa.
  • ⚠️ Tre trappole dei provini, trovate qui e utili altrove: (1) un preload() a livello di classe in uno script -s compila lo script prima che gli autoload esistano, e fallisce in silenzio — va usato load() a runtime; (2) GameState.is_drunk forzato senza drunk_time_left viene riazzerato dall'autoload entro un frame; (3) le prime letture di get_viewport().get_texture().get_image() in un processo Godot restano congelate sul primo frame per alcune chiamate, anche aspettando 15-25 frame — vanno scaricate con scatti buttati prima della sequenza vera.
  • ⚠️ NON provato: nessuna esecuzione MP (il fix non tocca RPC né relay, rischio ritenuto basso), e nessuna verifica in gioco coi controlli veri — solo provini in-engine.

Quattro decisioni su account e identita', con i numeri veri e un autoinganno corretto in corsa2026-08-28

  • [doc] Quattro documenti in OPUS_BRIEFS/ (SU-639, SU-640, SU-637, SU-641), misure riproducibili in TMP/lotto6/. Nessun file di gioco né Apps Script toccato, nessuna chiamata agli endpoint di produzione — il registro dei tester è vivo.
  • ⚠️ L'episodio più importante del lotto è un errore riconosciuto e corretto in corsa. Nella prima stesura di SU-641 erano state scritte quattro citazioni delle policy Google Play etichettate «verificato oggi sulle pagine ufficiali»: erano a memoria. Aperte le pagine davvero, due hanno retto parola per parola e due erano sbagliate — in particolare era scritto che i giochi non possono avere elementi aleatori nei programmi fedeltà, mentre la policy dice l'opposto: i *Gamified Loyalty Programs* sono un'eccezione ammessa a tre condizioni. La conclusione pratica non cambia (il premio non deve avere valore reale), il motivo sì, e la correzione è tracciata in fondo al documento con gli URL. 📌 Vale come promemoria: una citazione di policy non verificata è peggio di nessuna citazione, perché nessuno la ricontrolla.
  • ⚠️ SU-639 — metà del ticket è già in gioco dal 18/08: SU-457 ha reso «il canale d'ingresso di OGNI riga» un dato già presente (OnlineLeaderboard.gd:321-322 e :472), che compare a parole e costa zero chiamate per riga. Quindi il ticket non è ricerca, è disegno: il costo vero è che le righe sono Label di testo e un'icona vuole RichTextLabel/TextureRect, e nel pannello di fine partita il testo è un blocco unico. Misurato il payload col provider: 5 righe 270→299 B, 8 righe 418→466 B, 100 righe 4.729→5.332 B.
  • 📌 Trovata una stranezza in corso: RIGHE_MENU = 8 e RIGHE_FINE_PARTITA = 5 (OnlineLeaderboard.gd:133-134), ma il pannello di fine partita chiede 8 canali per mostrarne 5.
  • [doc] SU-640 — il numero a cui si rompe, che il ticket chiedeva e che non era «tanti»: il registro regge 5.000 scritture al giorno, che con FALLBACK_MAX_TRIES = 5 (fino a 6 scritture a testa) significa fra 800 e 5.000 giocatori nuovi al giorno. L'attesa peggiore di una rivendicazione è 106,2 s, senza alcun tetto lato UI — ed è il difetto più concreto emerso. Scansione di 1M righe: 13,6 ms; il collo di bottiglia non è la scansione ma gli 87,5 MB che getValues() materializza a ogni chiamata. Raccomandato: prima rendere distinguibili i tre «no», poi l'indice; base dati vera solo sopra il migliaio di rivendicazioni al giorno.
  • ⚠️ SU-637 si rompe sull'IDENTITÀ, non sul trasporto, ed è la sorpresa che cambia il piano: PROVIDER_PLATFORMS dà Apple solo su iOS e Google solo su Android, e LINK_PROVIDERS non contiene Epic → Android→iPhone oggi non esiste proprio. Inoltre la doc Epic dice che TransferDeviceIdAccount, con due PUID, scarta una progressione: il caso «venti ore da ospite e poi login» va fuso da noi, prima. Limiti EOS raccolti dalla doc: file max 200 MB, 400 MB e 1000 file per utente, 1000 richieste/minuto; Stats 60 ingest/minuto per utente, 500 stat per deployment. meta.cfg pieno pesa 2.323 B (34.930 B con 2.000 panchine): sta in un chunk da 4096.
  • 📌 Il prerequisito di SU-637: prima di qualunque sincronizzazione vanno rese monotone le lattine (guadagnate/spese separate). Senza, nessuna regola di conflitto è corretta — è il punto che il ticket chiamava «la parte che rompe i salvataggi se si sbaglia».
  • ⚠️ SU-641 — «una pagina web aggiorna le statistiche su Epic» non è oggi verificabile: la Web API delle stat di EOS non è nell'indice pubblico e il vecchio percorso risponde 404. Va chiesto a Epic; nel frattempo la funzione regge col conteggio sul nostro servizio. Fattibile solo se la moneta compra cosmetica, e l'auto-invito non si impedisce: si rende privo di valore. Policy citate testualmente (Payments, User Ratings/Reviews/Installs, Real-Money Gambling, Families) con gli URL, più la correzione di tre URL girati che oggi sono 404 o rinominati.
  • 📌 Una domanda che nessuno aveva posto: qual è il pubblico dichiarato su Play. Se comprende i bambini, un invito è una *social feature* e servono il promemoria in-app e l'*adult action*.
  • 📌 Tecnica riusabile: le pagine di dev.epicgames.com sono leggibili da riga di comando — sono Next.js e il testo sta dentro __NEXT_DATA__, non nell'HTML. Un curl ingenuo restituisce quasi nulla e fa concludere «illeggibile senza browser», che è falso.
  • Non misurato, e scritto nei documenti: il tempo vero di getValues() dentro Apps Script (serve un secondo deployment sulla tab dev), il giro dei canali a 8 righe (l'unica misura agli atti è a 2 PUID, 173 ms), e il costo del servizio Player Data Storage, che non compare nella doc pubblica.

HUD su tre file, mancini, taglie separate, e le frecce della metro che finalmente si vedono2026-08-28

  • [change] SU-632 — il pannello volante del flash è sparito, e l'interruttore è una voce normale delle opzioni: hit_neg in GRAFICA, dopo RIPRISTINA ZOOM, con in_game: true perché si deve poter cambiare a partita in corso. Da HUD.gd sono uscite 112 righe: 4 funzioni, 2 variabili e 8 punti di chiamata. Settings.hit_negative_enabled non cambia. Prova: TMP/lotto3/shots/SU632_grafica_telefono.png.
  • [change] SU-638 — modalità mancini (Settings.left_handed_controls), applicata a caldo. 📌 Si specchia la posizione, non la grafica — ma non basta specchiare i nodi: va specchiata anche la metà di schermo che risponde al joystick (senza, da mancini lo stick si disegnava a destra e rispondeva ai tocchi a sinistra) e l'inset del notch, che deve seguire il gruppo. Misurato: da destri il joystick sta a 37,8 px dal bordo sinistro e i tasti a 37,8 dal destro; da mancini esattamente scambiati.
  • [change] SU-613 — due taglie separate (joy_scale_mode / btn_scale_mode), nuove e non sostitutive: DIMENSIONE CONTROLLI resta e il default di entrambe è «come quella», così chi non tocca niente non vede differenze. Misurato: con i soli tasti a ENORME il joystick_rect è identico al default bit per bit, e viceversa. Le tre voci sono state provate premendole davvero, non solo leggendole.
  • [change] SU-627 — i contatori su tre file: monete / trombetta+super / gatto+bottiglia. 📌 La trappola di SU-530 sparisce per costruzione: la bottiglia non segue più la super, quindi non salta più a sinistra quando la super scompare — non è stato necessario risolverla, è il riordino stesso a scioglierla.
  • [change] SU-622 — le frecce della metro si ALLUNGANO, non si spostano a destra, ed è una scelta fatta sui numeri. Misurato prima: su telefono la fila copriva il 47% dello schermo (da x 505 a x 1100 su 1278) lasciando 505 px di corsia vuota a sinistra, e l'ultimo terzo finiva dietro al diamante A/B/X/Y (tasti da x 1008). Spostarla a destra l'avrebbe portata esattamente sotto ai pulsanti: il lato libero, su un telefono, è quello sinistro. Ora si allunga del solo *surplus* rispetto a un 4:3 — 18 chevron su telefono, 10 identici sul 4:3, cioè il desktop non cambia di un pixel.
  • ⚠️ Due trappole pagate dentro SU-622: la soglia di morte della fila è stata staccata dal primo chevron (CHEVRON_MORTE_DX) — se lo avesse seguito, la fila sarebbe rimasta accesa mezzo secondo dopo il passaggio del treno, cioè il difetto che SU-542 aveva chiuso, riaperto da un ticket di grafica; e la cascata ha un fattore che le tiene lo stesso tempo di percorrenza a fila più lunga.
  • ⚠️ Il primo giro di provini mentiva, ed è una trappola da ricordare: su Mac la taglia AUTO cade a 2,20 (il tetto desktop) invece di 1,35 (telefono), e mostrava una collisione fra controlli che sul telefono non esiste. I numeri buoni sono quelli con phone_screen_override.
  • [change] 14 chiavi nuove in 8 lingue (assets/translations/ui_menu.csv). ⚠️ de/ru/zh/pt vanno riviste da un umano. Disambiguata anche MENU_OPT_CONTROLLI_DIM in fr/de/ru, che diceva «taglia dei bottoni» e sarebbe stata identica alla voce nuova.
  • ⚠️ NON provato: un dito vero su un telefono. Mancini, le due taglie e i chevron sono misurati sul viewport, non toccati. E con TAGLIA TASTI a ENORME il diamante arriva a y 53 e invade la colonna alto-destra: nessun criterio lo vieta, ed è una scelta esplicita del giocatore, ma va vista.

Quattro decisioni di contenuto scritte, e due premesse dei ticket smentite dal codice2026-08-28

  • [doc] Quattro documenti di design in OPUS_BRIEFS/, uno per ticket, tutti con la stessa struttura — la domanda, cosa c'è già (verificato con file:riga), due o tre strade con costo e rischio, una raccomandazione, le domande aperte, i ticket che nascerebbero. Ivan sceglie sul documento: è la modalità che ha chiesto in chat il 2026-08-28 per tutti i (DESIGN).
  • ⚠️ SU-628 — il ticket parte da un presupposto falso: le monete raccolte danno GIÀ punti. Non direttamente, ma via GameState.total_money_earned * POINTS_PER_DOLLAR (=2) in ScoreSystem.compute_total() (ScoreSystem.gd:73 e :146). Il buco vero è solo sulle lattine, che oggi accreditano la valuta meta e zero punti run. Raccomandata una voce nuova per le sole lattine, una volta per evento e mai per oggetto raccolto — che è esattamente la trappola già pagata in XPSystem.gd con l'XP nelle monete. A 5-8 punti per evento il totale resta rumore su una run tipica (~2.500-4.000 contro un tetto di 500.000): a quella taglia la classifica esistente non va toccata, che era la preoccupazione del ticket.
  • ⚠️ SU-624 — l'ipotesi di Ivan («più scendi, peggio va») non ha oggi su cosa appoggiarsi: la metropolitana non ha livelli né profondità. È lo stesso corridoio riaperto più volte: BonusLevel.entra(mondo, giro) riceve un giro che risemina l'RNG dei treni, e non c'è nessuno scaling. Quindi «più scendi» significa o «più giro», oppure un vero sistema a piani, che è un ticket molto più grande — ed è la scelta che va fatta prima di scrivere codice.
  • 📌 Due collisioni di nomi evitate prima di nascere: arch_office_zombie esiste già ed è un civile comune («Impiegato esaurito»), e topo_bidone è già una carta perk. Un nemico nuovo con quei nomi avrebbe creato confusione nel manifest delle varianti.
  • 📌 Sul budget NPC: la banchina tiene già 40 AnimatedSprite2D (BonusRewards.gd:180, con la nota che la città di sopra ne regge 45 *con* fisica e dialoghi), e il gioco è già al tetto di framerate sull'Honor 10 (SU-226). Raccomandato di fare prima i topi, misurare, e solo dopo decidere sugli zombie.
  • [doc] SU-626 (cani): raccomandata la strada «super che chiama i cani», perché riusa PowerUpSystem/SuperPower.gd invece di introdurre un'entità persistente in World.gd, e non obbliga a inventare un danno al compagno che oggi non esiste in nessuna forma. La gag coi gatti si aggancia sopra gratis.
  • [doc] SU-625 (piccioni): raccomandato il piccione d'oro locale per giocatore (in MP niente gara, che sarebbe una fonte di frustrazione), più frequente ma meno generoso. RainbowArc.gd è riusabile come motore di disegno ma serve logica nuova per seguire un bersaglio mobile invece di due punti fissi; per le monete bastano World.drop_coin_reward() e CoinDrop.gd.

Un tocco a vuoto non ferma piu' il barbone (SU-592)2026-08-28

  • [change] Il pinch del pad fluttuante ora si ARMA all'appoggio e si CONVERTE solo al movimento (World.gd, nuova ZOOM_PINCH_CONVERT_DELTA = 20.0, stato _pinch_wait_*, nuove _prova_a_convertire_pinch() / _annulla_pinch_in_attesa() / _rinasci_pad_fluttuante()). Prima bastava appoggiare un secondo dito a più di 24 px dal pollice perché release_floating_pad() mollasse il movimento: un tap a vuoto fermava il barbone ogni volta.
  • 📌 La misura non è «di quanto è cambiata la distanza fra le dita», ed è la parte non ovvia. Quella strada è stata scartata con un numero: camminando, il pollice gira attorno al centro del pad e da solo sposta la distanza di ~64 px col secondo dito immobile — avrebbe convertito da sé. Si confronta invece la distanza di adesso con quella che ci sarebbe se il dito libero fosse rimasto dov'è atterrato, col pollice dov'è ORA: il vagare del pollice si semplifica e resta solo il contributo del dito libero, cioè il gesto vero.
  • 📌 Soglia 20 px, un terzo abbondante del raggio del joystick (58): un dito appoggiato per sbaglio non ci arriva tremando, una pizzicata vera lo supera subito. ZOOM_PINCH_MIN_DIST resta 24 e non c'entra: è la distanza minima *fra* le dita, un'altra cosa.
  • [change] Chiuso il pinch, il pad rinasce sotto il pollice ancora appoggiato, reiniettando un tocco con push_input(ev, true). ⚠️ Le due alternative sono state provate e scartate con la misura: una chiamata diretta a _begin_floating scavalcherebbe l'arbitrato HUD su cui è costruito SU-316, e Input.parse_input_event riapplica la trasformata di stretch — il pad rinasceva a (5520, 4160) invece che sotto il pollice a (345, 260).
  • ⚠️ Il punto 3 del ticket era falso ed è stato cancellato: «TutorialWorld ha la sua copia del pinch» non è vero. TutorialWorld.gd:800-812 ha un commento di SU-328 che dice «QUI IL PINCH NON C'È, ED È UNA SCELTA»: quella scena non eredita da World.gd e la sua camera ha Y fissa. File non toccato.
  • [verifica] Il provino sa riprodurre il difetto, che è ciò che lo rende una prova: probe_su592_tocco_a_vuoto.gd15 casi 0 KO sul codice nuovo e 6 KO su quello di HEAD (tap a vuoto che ferma il barbone, pad che non rinasce), mentre il pinch vero passa in entrambi. Regressione probe_su328.sh: 0 KO. ⚠️ Adattato il test 2 di probe_su328_pad_fluttuante.gd, che pretendeva la conversione all'appoggio — cioè il contratto che questo ticket rovescia di proposito.
  • ⚠️ Il caso «anche col joypad visibile» segnalato da Ivan il 28/08 non è stato riprodotto, e la diagnosi dice che è un'altra cosa: col ramo A che esce a World.gd:4948 (is_floating_pad_active falso) lo stick disegnato non passa di lì, né prima né dopo. 📌 Ma esiste un secondo percorso con lo stesso identico sintomo, in entrambe le modalità: se il dito centra il tasto B = mappa, MapOverlay è nel gruppo touch_modal (HUD.gd:1799) e il poll chiama _release_all_inputs() (VirtualControls.gd:338) → _joy_touch_idx = -1, barbone fermo, joypad da riprendere da capo. Il fix vive in VirtualControls.gd, che è di un altro lotto: passato a quel lotto, non implementato qui.
  • 📌 Nota utile emersa: i tasti azione restano disegnati anche col joypad nascosto (_apply_stick_visibility, VirtualControls.gd:1194, spegne solo base e pallino) — quindi «premo vicino ai pulsanti» è compatibile col caso del pad nascosto, e la citazione di Ivan non stabilisce che lo stick fosse disegnato.
  • ⚠️ NON provato su device: tutto in headless col driver dummy, dove le coordinate passano da una superficie 64×64. La rinascita del pad va riguardata sul telefono.

Il punteggio non si perde piu', e la classifica e' gia' pronta quando giri pagina2026-08-28

  • [change] SU-618 — l'invio del punteggio diventa insistente, con una coda su disco (user://coda_punteggi.cfg, ConfigFile nello stampo di HighScore). Vive dentro OnlineLeaderboard.gd, che dichiara in testa che «tutta la materia stat+leaderboard di EOS vive QUI». Tre momenti di ritentativo e nessun timer che gira: avvio del gioco (+5 s), refresh() di una board (cioè il rientro nel menu, senza toccare MainMenu), e l'inizio dell'invio della partita successiva.
  • 📌 La scelta non ovvia: si scrive in coda PRIMA di provare, non dopo aver fallito. Fra l'invio e la risposta di EOS passano fino a 20 s (TIMEOUT_SEC), e in venti secondi un telefono si chiude: accodando sull'esito ESITO_FALLITO il difetto sarebbe stato solo spostato di venti secondi più in là. Si cancella solo sulla conferma del server.
  • [decisione] ESITO_SESSIONE entra in coda, ESITO_RIFIUTATO e ESITO_NON_INVIATO no. Sessione non è un rifiuto: Settings dice che il giocatore è dentro, è l'identità EOS del momento a essere quella dell'ospite. La voce si porta dietro il PUID dell'account e riparte solo quando torna quella identità, mai a nome di un altro. Buttarla punirebbe chi aggiorna da una versione senza ancoraggio proprio nella partita in cui non poteva saperlo.
  • [change] SU-631 — l'invio e la lettura della classifica partono dal game over, mentre il giocatore guarda la prima pagina del giornale. Il punto d'innesco è RunManager._on_game_over() e non l'HUD: è l'unico posto che conosce il game over nell'istante in cui accade e ha già sistemato il punteggio — register_raid_survival() in endless aggiunge i secondi oltre il 30', e anticipare prima di quella riga spedirebbe un numero più basso di quello scritto sul pannello. L'HUD continua a chiamare submit_run_score() come sempre: trova il lavoro fatto e non lo rifà.
  • 📌 Una guardia sola per due ticket: l'id di run (RunManager.get_run_id(), coniato in start_run()) è insieme la chiave anti-duplicato della coda e la guardia contro il doppio invio. Sulla deduplica si riemette board_changed e non submit_finished, perché quest'ultimo farebbe scattare dall'HUD una terza rilettura forzata (SU-474) — cioè esattamente l'attesa che il ticket voleva togliere.
  • [verifica] I numeri di SU-631, misurati su mondo vero e game over vero (tools/autotest/probe_su631.sh, headless): la prima pagina del giornale compare 1021 ms dopo il game over e regala una finestra di 1486 ms prima che si apra la pagina della classifica — e questo col giornale chiuso appena è possibile chiuderlo, cioè nel caso peggiore per noi. Le righe compaiono in 0 ms dall'apertura della pagina, stesso frame. ⚠️ Il «prima» NON è misurato e non è stimato: è il giro di rete EOS vero con un account vero, e in headless non c'è né rete né login.
  • [verifica] La coda provata rientrando davvero (tools/autotest/probe_su618.sh, 8 controlli, 0 KO): il punteggio entra in coda senza aspettare l'esito, la stessa partita non entra due volte, una partita nuova riparte, l'implausibile è rifiutato e non accodato, da ospite non si accoda niente. Il criterio «la coda sopravvive alla chiusura del gioco» è provato rilanciando il provino e ritrovando le 2 voci del lancio precedente, non simulandolo.
  • ⚠️ Diagnosi del caso della tester (SU-618 punto 1): dal codice l'esito è ESITO_FALLITO, ma restano due candidati che il codice non distingueESITO_SESSIONE, e soprattutto ESITO_NON_INVIATO col consenso alla pubblicazione spento, che submit_text() traduce in nessuna riga: lì il gioco davvero non se n'è accorto, e la coda non lo copre. Sarebbe un ticket suo: una riga che lo dica.
  • 📌 user://coda_punteggi.cfg diventa una traccia diagnostica sui device dei tester: se un punteggio non arriva, resta scritto lì con tentativi e identità.
  • ⚠️ Segnalato un KO preesistente, non causato da qui: tools/autotest/probe_su446_nome.sh fallisce il caso 4 perché il runner scrive credenziali finte (deployment_id="probe-su446-finto") e la guardia d'ambiente di SU-574 le dichiara incoerenti. Verificato con controprova sul codice di HEAD: stesso KO. Va sistemato il runner, non il gioco.

Due difetti del bonus stage che non erano difetti, e le sonde che lo dimostrano2026-08-28

  • [verifica] SU-615 — il montepremi della banca era GIÀ giusto, e il ticket cercava nel posto sbagliato. Il ticket indicava BonusRewards.gd riga ~670 come sede del raddoppio; il raddoppio sta invece in GameState.bank_take_gold_debt() (GameState.gd:1514) ed è già protetto dalla guardia if bank_bonus_vinto: una riga sopra, alzata solo da bank_bonus_superato() che chiama solo BonusLevel._risali_ora() a bonus vinto. Era la correzione del 2026-08-22, mai richiusa sul ticket. ⚠️ Nessun file di gioco toccato: il rischio vero qui era "correggere" del codice che funzionava — un grep sulla sola riga *= 2 non mostra la guardia che sta sopra.
  • [verifica] I numeri chiesti dal ticket, misurati con una sonda nuova invece che letti a occhio (scripts/tools/probe_su615_banca.gd, runner tools/autotest/probe_su615.sh): giro 1 perso 100→100 e completato 100→200; giro 2 perso 200→200 e completato 200→400; giro 3 perso 400→400 e completato 400→800. Il terzo giro è stato aggiunto in chiusura perché il criterio lo chiede esplicitamente («lo stesso al secondo e al terzo giro») e la sonda si fermava al secondo: la spunta riga per riga serve a questo.
  • [verifica] SU-614 — l'ipotesi del ticket («un timer non azzerato che al rientro riparte da lì») è SMENTITA, con tre rientri veri. _intro_t, _lbl_conto, _intro_veloce sono variabili di istanza di un nodo che BonusLevel.entra() ricrea da zero a ogni discesa e libera con queue_free(); nessuno static var nel file. Sonda probe_su614_countdown.gd: tre discese consecutive nella stessa partita, con uscita vera (_termina(false) → outro → bottino → _risali_ora), danno sempre [3,2,1,VIA!], _intro_t a 0.0 ogni volta, nessuna cifra saltata.
  • 📌 Quello che Ivan vede è invece SU-492, ed è voluto: dal secondo giro l'intro si comprime a 0,45 s per cifra invece di 0,7 (INTRO_CIFRA_VELOCE_SEC, BonusLevel.gd:499, KO di Ivan del 2026-08-21 che chiedeva di non rispiegare ogni volta). Totale 5,51 s al primo giro contro 1,68 s dal secondo: le cifre ci sono tutte, ma passano in un terzo del tempo, ed è esattamente la sensazione di «parte già da 2». La decisione è di Ivan e non si riapre da soli: è la stessa che lui ha preso una settimana fa.
  • 📌 Segnalata una sonda stale di un altro ticket: scripts/tools/probe_su469_xp.gd sezione C si aspetta ancora il vecchio raddoppio incondizionato e oggi dà KO. Non toccata (fuori perimetro), ma va sistemata o l'errore verrà riletto come una regressione.

Qwen Coder in locale come esecutore, e il Mac che gli fa spazio2026-08-28

  • [change] claude-local nel ~/.zshrc (fuori dal repo): Claude Code puntato al modello locale. Avvia il server LM Studio se serve, carica qwen3-coder-30b-a3b-instruct Q4_K_M con identificatore local-coder a 64K di contesto su GPU, aspetta che risponda davvero, poi lancia la CLI su http://127.0.0.1:1234. Il comando claude resta quello di Anthropic: la configurazione globale non è stata toccata (verificato: zero variabili ANTHROPIC_* in una shell nuova, settings.json invariato). Backup del .zshrc prima di scrivere.
  • [decisione] Qwen prende il posto di tuttofare-haiku, non di dev-sonnet (concordato con Ivan in chat il 28/08). Claude resta orchestratore e riassembla; Qwen esegue brief chiusi su task meccanici con verifica automatica (compile-check, grep, git diff corto). Il risparmio di token esiste solo se l'output sporco non entra nel contesto di Claude: si delega come sottoprocesso e si legge il solo esito. Prova a tempo fino al 2026-09-04, poi si decide sulla macchina dedicata.
  • [verifica] Provato con due sessioni Claude Code fresche, e la prima tornata è FALLITA (richiesta di Ivan: «fai un test che sia tutto pronto dovessi aprire una nuova sessione»). Test con Haiku di proposito: se la regola la trova il modello più piccolo, la trova chiunque. Alla domanda «primi 3 passi prima di /sprint» la sessione ha risposto senza mai aprire WORKFLOW/02_SPRINT.md, saltando il passo 0 della RAM; alla domanda sulla delega dei ticket ha risposto NO ma con un motivo inventato («il tandem serve a valutare, non a delegare» — falso), senza mai aprire 01_TICKET.md. 📌 Lezione: una regola che deve valere sempre non può stare solo in un file di fase. CLAUDE.md e la memoria automatica arrivano a ogni sessione, i file di WORKFLOW/ solo se qualcuno li apre. Aggiunte due righe dense a CLAUDE.md (§«Regole minime sempre valide») e corretta la memoria del tandem, che con «ogni lavorazione passa anche da Qwen» induceva a leggerlo come un confronto in parallelo invece che come una delega. Test rilanciati identici: passano entrambi — passo 1 «liberare la RAM» citato per primo, e il NO sui ticket motivato col conto 270.000 contro 8.000.
  • [change] Il confine di Qwen scritto in ORCHESTRAZIONE.md (nuova sezione «Qwen locale», riga nel roster, e scala di delega che ora parte da lui: Qwen → haiku → sonnet → opus). Il criterio non è «Qwen ce la fa?» ma «quanto costa scoprire che non ce l'ha fatta»: si delega solo dove la verifica è meccanica e quasi gratis (compile-check, grep, git diff corto). Tabella di cosa sì e cosa no, più i tre vincoli: il suo output non entra nel contesto di Claude (si delega come sottoprocesso e si legge il solo esito), git status dopo ogni delega, e si annota il motivo di ogni fallimento — contesto o capacità, perché il primo lo risolve una macchina più grande e il secondo no.
  • [decisione] Il testo dei ticket NON si delega, l'istruttoria sì (WORKFLOW/01_TICKET.md, nuova sezione). Domanda di Ivan: «per scrivere i task in backlog si può usare Opus insieme a Qwen?». La premessa è giusta — su quel task il costo è quasi tutto in uscita, quindi sarebbe dove delegare renderebbe di più — ed è per questo che è il posto sbagliato: le descrizioni di uno sprint valgono ~8–10.000 token di output, un solo giro di KO ne costa ~270.000 (post-mortem 23/07: nove giri, ~2,4M). Un ticket scritto male cancella il risparmio di venti sprint, rapporto ~30:1 contro. A Qwen restano duplicati, quali file tocca ciascuna richiesta, e l'impaginazione per jira_scrivi.sh. 📌 Il risparmio maggiore su quel task non è Qwen ma la regola già esistente sulle alternative scartate: −78% su SU-467.
  • [change] tools/sprint_ram.sh: passo 0 di ogni sprint, ed è un'anteprima. Lanciato senza argomenti non chiude niente: elenca cosa chiuderebbe e quanto libera. Chiude solo con --chiudi, sempre col Quit normale — mai kill -9 — e un'app che resiste dieci secondi viene segnalata e lasciata in pace, perché quasi sempre ha qualcosa da salvare. Regola scritta in WORKFLOW/02_SPRINT.md §«Prima di toccare qualsiasi cosa», punto 0.
  • [verifica] Sul Mac di Ivan libera 4,6 GB: ChatGPT 3,2 GB (il più grosso di tutti), WhatsApp 498 MB, Safari 379 MB, Logi Tune 213 MB, Calendar, Mail, Activity Monitor. È una stima per difetto: i processi WebKit.WebContent di Safari (~730 MB) muoiono con l'app ma non sono contati nella riga. Misurato anche il motivo per cui serve: lo swap era già a 1,84 GB su 3 anche a riposo, senza sprint in corso.
  • 📌 La conferma sta in chat, non nel terminale. Il read interattivo dello script si bloccherebbe dentro una sessione Claude Code: quindi Claude incolla l'elenco, Ivan dà l'ok lì, e solo allora parte --chiudi -y. Le voci dubbie (oggi l'app Claude desktop, 1,4 GB) lo script le propone ma non le chiude: entrano solo con --anche-dubbie.
  • ⚠️ Trappola AppleScript evitata: tell application "Godot" to quit AVVIA Godot se non è aperto. Un «chiudi tutto» scritto in modo ingenuo aprirebbe Chrome e Godot invece di chiuderli — l'esatto contrario. Si controlla prima con if application "X" is running.
  • 📌 Chiudere l'app WhatsApp non rompe l'MCP. Verificato: whatsapp-bridge è un processo a sé (avviato da launchd, ppid 1, sotto whatsapp-mcp/whatsapp-bridge) e continua a funzionare ad app chiusa — quindi avvisa-tester resta usabile a Mac ripulito.
  • 📌 Il processo com.apple.Virtualization.VirtualMachine da ~976 MB è la VM Linux dell'app Claude desktop — intuizione di Ivan, verificata con lsof: monta ~/Library/Application Support/Claude/vm_bundles/claudevm.bundle (kernel vmlinuz, initrd-micro, rootfs.img da 10 GB, sessiondata.img) più smol-bin.arm64.img da dentro /Applications/Claude.app. Non ha /Claude.app/ negli argomenti, quindi il conteggio per nome dell'app non la vedeva: lo script ora la riconosce dal bundle che tiene aperto e la somma alla voce Claude desktop, che passa da 1,38 a 2,36 GB. Se ne va con l'app, non va chiusa a parte.
  • ⚠️ Prima di chiudere Claude desktop va guardato se ha una sessione viva: la VM teneva aperte cartelle di un altro progetto (Progetti Claude/Ricerca Lavoro), quindi non è un residuo inerte. È il motivo per cui resta fra le voci «proposte ma non chiuse».
  • 📌 Sull'endpoint locale: LM Studio espone /v1/messages nativo, tool calling compreso, e ripiega sul modello caricato per qualunque nome richiestoclaude-sonnet-4-5-… come modello-che-non-esiste rispondono 200. Non c'era nessuna opzione da attivare, contrariamente a quanto si poteva supporre. Serve invece CLAUDE_CODE_MAX_CONTEXT_TOKENS pari al contesto vero, o Claude Code assume 200k e non compatta prima di sforare i 64K.