← Novità

v0.28-dev

2026-08-05 → 2026-08-07 · 9 voce/i di changelog

Sprint, undicesimo giro2026-08-07

  • [fix] SU-305 (KO) — la posa del colpo passa allo sprite che Ivan si è rigenerato da solo. Il KO non contestava il meccanismo, contestava l'immagine: «usa barbone_colpito.png dentro sprites_raw come sprite per l'evento colpito, l'ho fatto rigenerare io a codex come volevo». ⚠️ Cosa era stato capito male il giro scorso: la posa consegnata da SU-303 e messa in gioco era un barbone in piedi col braccio alzato — leggeva come un saluto o una resa, non come uno sbalzo. Quella di Ivan è la caduta alla Sonic che il ticket chiedeva fin dall'inizio: corpo inclinato all'indietro, gambe divaricate, braccia larghe, faccia stupita. Guardate affiancate a 14× accanto ai frame di camminata, la differenza non è una sfumatura. Import con script, non a mano (tools/su305_importa_colpito.py, resta in repo perché il giro si ripeta identico): il fondo magenta si toglie prima del downscale e i colori si premoltiplicano per l'alpha, altrimenti la media d'area impasta il magenta nei bordi e resta un alone rosa; riduzione con BOX e mai nearest, che qui è un ~30×. 📌 La scala è stata scelta guardando, non misurando: due tentativi di misura automatica (larghezza del cappello a 32 px, poi sul raw ad alta risoluzione) hanno dato numeri non monotoni e presi le toppe grigie della manica — cioè rispondevano a un'altra domanda. Il criterio era percettivo e si è chiuso con un foglio zoomato: sagoma 26×32 nella cella 32×32, piedi sulla stessa riga di base dei frame di camminata, testa e cappello della stessa taglia dell'idle. Nessuno script toccato: la .tres puntava già a player_hit.png, e ci puntano anche le varianti gatto e ubriaco, quindi lo scambio le copre tutte e tre in un colpo solo. Il verso combacia con la convenzione già scritta in _apply_hit_pose: anche nello sprite nuovo il braccio alzato sta sul lato sinistro, cioè «botta arrivata da sinistra», quindi lo specchio resta giusto senza toccare il codice. Provino rifatto dalla città vera (10 scatti in files/homeless_city/TMP/su305/), tutti i numeri di nuovo verdi con l'immagine nuova: colpo da 3 vite della ronda segnali=3 → reazioni=1, specchio flip_h false da sinistra e true da destra, spostamento +21,9 px col colpo da sinistra e −8,3 px da destra (mai verso l'aggressore), gatto che sparisce durante e torna dopo, grafica leggera con la posa presente e il lampeggio spento, zero SCRIPT ERROR e zero SHADER ERROR. ⚠️ Un buco dichiarato, non riparato: la skin f1 non ha un'immagine nuova — Ivan ne ha consegnata una sola — e resta con la sua posa in piedi, che ora accanto a quella nuova stona. Serve una generazione dedicata, che per le regole del progetto è un ticket suo. Multiplayer: NON collaudato (EOS/ENet non gira su questo Mac), ma qui non è cambiata una riga di codice di rete: è un cambio di solo asset.
  • [feat] SU-306 — a ogni vita persa un cuore rotto alato se ne vola via. Era bloccato da SU-304, che Ivan ha appena approvato: quindi il cuore a 16×16 con le ali che battono entra in assets/ ed è quello, non ritoccato. Nessun sistema nuovo, come chiedeva il ticket: il cuore è un cliente in più del giro della monetina (Sprite2D dentro fx_canvas, screen-space, tween). Due sole aggiunte locali: hframes = 4 sullo sheet 64×16 — così lo sprite esce 16×16 esattamente come la monetina — e un tween di callback che scorre i quattro frame per il battito. ⚠️ La trappola aritmetica: il normalizzatore _fx_unit_size_mul() (SU-144) vale 16/larghezza, quindi sullo sheet da 64 px darebbe 0,25 e il cuore uscirebbe a un quarto della taglia giusta; qui non si usa, e il perché è scritto nel codice. ⚠️ Il punto delicato del lotto, e la ragione per cui è stato scritto con un ascoltatore separato: il colpo da 3 vite della ronda emette player_life_lost tre volte, e questo ticket vuole l'esatto contrario di SU-305 — tre cuori invece di una posa sola. L'anti-ripetizione di SU-305 (HIT_REACT_BURST_MS = 150) non è stata toccata: il cuore ascolta per conto suo (_on_life_lost_broken_heart) e la sua finestra numera soltanto i cuori dello stesso colpo per sfalsarli, senza sopprimerne nessuno. Tetto a 4. Numeri veri dal provino (shot_su306_cuore.gd, zoom e risoluzione di gioco): salita y 243→193 con alpha 1,00→0,40 in 380 ms, deriva laterale 37 px, tre cuori a distanza minima 65 px, a +120 ms se ne vede uno e a +360 ms tre. 📌 Una taratura fatta perché la misura l'ha smentita: la salita è stata abbassata da 92 a 40 dopo aver misurato l'apice a 114, cioè dentro il blocco HUD; ora il bordo alto del cuore sta a y=169 contro un HUD che finisce a 142 — 27 px di aria, e guardato: i cuori stanno nettamente sotto la fila delle vite e la barra LIV. Convivenza con SU-305 verificata guardando, non deducendo: nello stesso scatto il barbone è nella posa nuova del colpo, lampeggiante, con i tre cuori sfalsati sopra la testa. Grafica leggera: un cuore solo e ondeggio spento. Multiplayer: sì, il cuore lo vedono tutti — RPC nuova e dedicata (net_broken_heart / _cl_broken_heart_fx) sul modello di SU-305 e non un parametro aggiunto in coda a una RPC esistente, che romperebbe i peer con build vecchia; viaggia un solo intero per vita persa e lo sfalsamento lo rifà il ricevente, quindi niente contatori né array. NON collaudato (EOS non gira su questo Mac). 📌 Un errore trovato e chiuso in corsa: il tween delle ali sopravviveva al queue_free e sparava un ERROR per ogni cuore che moriva; ora _end_broken_heart spegne prima i tween e poi libera lo sprite. Compile-check rifatto dall'orchestratore: 197 script, 62 scene, FALLITI 0, zero SHADER ERROR. ⚠️ Resta NON MISURATO un criterio solo: la nuvoletta di fame/sonno accesa insieme al cuore — geometricamente non si incrociano (il cuore nasce a −38 e sale, la nuvoletta sta a −16), ma non c'è uno scatto che le mostri insieme.

RIENTRI ATTESI: SU-305, SU-306 — le due chiavi messe In revisione in questo giro. Al prossimo sprint, prima dei lotti: quante sono tornate in «Da fare» e con quale KO. *(Giro precedente: delle tre chiavi attese — SU-304, SU-305, SU-313 — ne è rientrata una, SU-305: tasso di rientro 1 su 3. SU-304 e SU-313 sono passate a Fatto.)* ⚠️ Come va letto questo giro, che è piccolo: due ticket soli, quindi il tasso dirà poco per costruzione. Il rientro di SU-305 non era un difetto di codice ma un'immagine giudicata a occhio, ed è la terza volta di fila che i rientri sono giudizi estetici e non bug — il che sposta il sospetto su come vengono scelte e mostrate le immagini prima di consegnarle, non su come viene scritto il codice. ⚠️ I buchi dichiarati, in ordine di gravità. Primo: il multiplayer non è stato collaudato e SU-306 aggiunge una RPC nuova — è il candidato numero uno a rientrare, come ogni giro da tre giri a questa parte. Secondo: la skin f1 ha ancora la posa vecchia del colpo, e accanto a quella nuova stona: serve una generazione dedicata, che è un ticket suo. Terzo: niente è stato visto su un device vero, solo in provini in-engine sul Mac. Quarto: SU-268 non è stato lavorato ed è fermo su Ivan (serve il suo login itch.io e l'URL da mettere nell'invito).

Sprint, decimo giro2026-08-07

  • [feat] SU-305 — quando le prende, il barbone reagisce: posa, contraccolpo e lampeggio. Prima perdevi una vita e non succedeva niente a schermo. L'aggancio è uno solo, EventBus.player_life_lost, invece di una modifica per sorgente: quel segnale è il collo di bottiglia di tutte e sette le sorgenti (bottiglia del tossico, gatto della gattara, birilli dello spazzino, sacchetto della spazzatura, barbone rivale, ronda notturna, arresto del poliziotto), quindi nessuna resta scoperta — RaidOfficer compreso, che eredita da PoliceOfficer. Il segnale però non porta la posizione di chi ha colpito, e allargarlo avrebbe rotto l'HUD che già lo ascolta: le sorgenti chiamano note_incoming_hit(pos) subito prima di lose_life(), un indizio con scadenza (400 ms), e se manca la reazione parte lo stesso rinculando all'indietro. Ingredienti: posa dedicata dai frame approvati in SU-303, contraccolpo di 150 px/s che si somma alla velocity invece di sostituirla — così il giocatore non perde il controllo e la spinta passa dalla stessa move_and_slide() del movimento normale — e lampeggio a 14 Hz, spento con GRAFICA LEGGERA mentre la posa resta sempre (è informazione di gioco, non decorazione). Durata 0,5 s. ⚠️ Il punto facile da sbagliare, e i numeri che dicono che non è stato sbagliato: GameState scala le vite una per volta, quindi il colpo da 3 vite della ronda emette il segnale tre volte — con una finestra anti-ripetizione di 150 ms, misurato segnali=3, reazioni=1. Sbalzo misurato +20,2 px col colpo da sinistra e −32,4 px da destra: il barbone non viene mai spinto verso chi l'ha colpito. ⚠️ La trappola scoperta strada facendo: le SpriteFrames del giocatore che Player scambia a caldo sono quattro, non una (base, gatto in braccio, ubriaco, skin f1) — mettere la posa solo su quella base avrebbe fatto fallire in silenzio il colpo preso col gatto in braccio. La posa è in tutte e quattro, e gli sheet con le loro region non sono stati toccati: i due PNG nuovi sono file autonomi. ⚠️ Un difetto solo estetico, dichiarato e NON riparato: se il colpo è quello che ti toglie l'ultima vita, il game over mette l'albero in pausa e il barbone resta congelato in posa dietro il pannello di fine partita. Non è un crash, non blocca il RICOMINCIA, ed è coerente col resto della scena che in quel momento è comunque ferma; ripararlo vorrebbe dire far girare la reazione anche a gioco in pausa, che è un rischio più grosso del difetto. Multiplayer: NON collaudato (EOS/ENet non gira su questo Mac) — il broadcast costa due soli interi, sul modello già usato per l'azzuffata col gatto, ma è verificato solo a lettura di codice. Per provarla senza farsi picchiare c'è il pannello F1, sezione «COLPO SUBITO».
  • [fix] SU-313 — la partita nuova non eredita più la calamità di quella prima, e nemmeno la sua difficoltà. ⚠️ Il sintomo esatto segnalato non si riproduce: la sonda dice che allo spawn della run nuova la fase è idle e la freccia del riparo è spenta in tutti e tre i percorsi (RICOMINCIA, game over, ESCI AL MENU + INIZIA PARTITA). Quello che è stato trovato e riparato è un difetto vicino ma diverso: il reset viveva solo sulle USCITE, in tre liste diverse che dimenticavano ognuna qualcosa — RICOMINCIA azzerava meteo, difficoltà e punteggio; ESCI AL MENU solo il meteo; INIZIA PARTITA dal menu principale niente del tutto. E l'ordine era rovesciato: ws.reset() prima di dm.reset(), mentre _schedule_next_wave() legge get_wave_interval_mult() — quindi il tempo alla prima calamità della run nuova veniva sorteggiato sul tier alto di fine partita precedente, e col preavviso di 20 s la freccia poteva riaccendersi pochi secondi dopo aver premuto RICOMINCIA. Numeri veri: passando dal menu principale la run nuova nasceva a tier 3 con la prossima ondata a 75 s, fuori dalla finestra sana 90-180; dopo il rimedio tier 0 e 144 s. Su RICOMINCIA 98 s, su game over 137 s. Il rimedio è un punto soloGameState.start_run() chiama il nuovo _reset_world_systems_for_new_run(), prima la difficoltà e poi il meteo — invece dell'ennesima toppa su ogni uscita: è la run che NASCE ad azzerare il proprio mondo. Le chiamate sparse sulle uscite restano dove sono, ora ridondanti e innocue, così non si rompe nessun percorso che oggi funziona. Chiude anche, per costruzione, la RIVINCITA in multiplayer, che passa dallo stesso World._ready(). Tutorial intatto (non passa da start_run()). Resta in repo la sonda probe_su313_restart.gd con il suo lanciatore tools/autotest/probe_su313.sh (modi restart|gameover|menu|pausa|tutorial): vive su /root per sopravvivere a reload_current_scene(), e stampa fase, secondi, tier e visibilità della freccia invece di farli giudicare a occhio.
  • [change] SU-304 — il cuore rotto alato passa a 16×16 e i suoi quattro frame smettono di ballare. Scelta di Ivan fra le due prove del giro precedente: si tiene il rigenerato a 16×16 (stessa griglia di pixel della monetina e del barbone) e si corregge il jitter. Misurato prima: il bounding box del solo cuore — isolato per colore, così le ali che battono non falsano la misura — si spostava di 1 px in verticale fra i frame 1-2 e 3-4. Rimedio con PIL, non rigenerando: traslazione di interi pixel dell'intera cella, così l'attacco delle ali al cuore resta quello originale. Misurato dopo: centro del cuore identico nei quattro frame, jitter a 0 px. Scartata la strada della ricomposizione (incollare il cuore di un frame negli altri tre): sulla carta azzerava la varianza, ma guardando il PNG lasciava un buco visibile fra ali e cuore. Consegna in TMP/su304/, come vuole il ticket: in assets/ non entra niente finché non approva Ivan.

RIENTRI ATTESI: SU-304, SU-305, SU-313 — le tre chiavi messe In revisione in questo giro. Al prossimo sprint, prima dei lotti: quante sono tornate in «Da fare» e con quale KO. *(Giro precedente: delle undici chiavi attese — SU-83, SU-287, SU-288, SU-289, SU-293, SU-296, SU-298, SU-303, SU-304, SU-308, SU-309 — ne sono rientrate due, SU-303 e SU-304: tasso di rientro 2 su 11, il migliore da quando si misura, contro il 2 su 7 e il 6 su 7 dei due giri prima. Ma va letto per quello che è: entrambi i rientri sono generazioni di immagini, cioè giudizi estetici, non difetti di codice — e nessuno dei due era «sbagliato», erano due cambi di impostazione decisi guardando. SU-303 è poi passato a Fatto in giornata.)* ⚠️ Il buco dichiarato di questo giro, in ordine di gravità. Primo: il multiplayer non è stato collaudato, e SU-305 ci mette dentro un broadcast nuovo più un parametro aggiunto in coda a sei RPC già esistenti — il che significa che un peer con una build vecchia non combacia più (stesso vincolo già visto in SU-256). È il candidato numero uno a rientrare. Secondo: SU-313 non riproduce il sintomo che era stato segnalato — la sonda dice fase idle e freccia spenta allo spawn in tutti e tre i percorsi; quello che è stato riparato è un difetto vicino e misurato (il tempo alla prima calamità dimezzato, e il tier che restava alto passando dal menu), ma se il sintomo originale era un'altra cosa, quel ticket torna indietro. Terzo: la reazione al colpo non è mai stata vista su un device vero, solo in provini in-engine sul Mac.

Sprint, nono giro2026-08-07

  • [fix] SU-289 — il tutorial comincia da MUOVITI, l'elemosina ha quattro passanti, il cartello delle 9 vite non sborda più. Le quattro richieste del KO, una per una. 1) «metti muoviti subito»: l'ordine è ora move, beg, busk, cat, lives, … — misurato a runtime prima e dopo, 20 stazioni in entrambi i casi, nessuna persa, solo riordinate. 2) «metti 4 npc»: il passo dell'elemosina ne spawna quattro invece di due. 3) «il titolo LE 9 VITE non è centrato e va sul bordo» — ⚠️ la causa era una misura fatta col font sbagliato: il centraggio si calcolava con ThemeDB.fallback_font mentre la Label disegna col font del tema di progetto, quindi la larghezza stimata non c'entrava niente con quella vera. Prima: margini 25 px a sinistra e 4 a destra, cioè il testo appoggiato al bordo. Dopo: 25 e 30, e lo scarto resta costante anche in tedesco e russo, che sono le lingue che sbordano per prime. 4) «spiega anche sul rifugio che ti ripristina vite»: TUTORIAL_ISTR_RIFUGIO riscritta in 8 lingue su 8, con il numero preso da GameState.SHELTER_LIFE_RESTORE invece che scritto a mano.

    ⚠️ Un difetto trovato per strada e chiuso, che il riordino ha smesso di nascondere: il banner del meteo iniziale («Il cielo si rasserena») lo emette WeatherSystem al primo cambio di stato, cioè prima che TutorialManager.begin() si agganci a show_big_notification — e quell'aggancio copre solo gli avvisi futuri. Il banner restava quindi dov'era per il suo secondo e mezzo, sopra il cartello più vicino allo spawn. Finché il primo cartello era ELEMOSINA, lontano, non si vedeva quasi mai (0 volte su 1); col nuovo ordine il primo è MUOVITI, che sta addosso al barbone, ed è diventato 3 volte su 3. La cura è una riga: al momento dell'aggancio si riposiziona anche l'avviso già a schermo. Misurato con una sonda nuova (probe_su289_banner.gd, che stampa i numeri invece di farli giudicare a occhio): offset_top del banner 180,5 contro una quota consentita di 180,5 — cioè esattamente sulla riga, non sopra — e guardato: banner sotto il cartello MUOVITI, tutti e due leggibili.

    ⚠️ Resta NON MISURATO quello che era già non misurato il giro scorso: il tutorial giocato dall'inizio alla fine su desktop e con i controlli touch. Serve input reale, e l'automazione GUI è vietata su questa macchina: va provato a mano.

  • [fix] SU-308 — il barbone non nasce più dentro un palazzo: adesso il punto di partenza è VERIFICATO calpestabile. _pick_player_spawn() non era mai cambiata dal primo commit del progetto e il suo commento mentiva: diceva «un'intersezione strada-strada», ma ROAD_WIDTH + cx * CELL_SIZE + ROAD_WIDTH / 2.0 restituisce il centro del lotto — le due letture coincidono solo perché BLOCK_SIZE e ROAD_WIDTH valgono tutti e due 96. Risultato: 64 città su 64 partivano dentro un isolato, e finora non si notava solo perché la depenetrazione di CharacterBody2D sputava fuori il barbone. SU-283 (muri a sagoma invece che a lotto intero) ha tolto quella rete: con una pila di rettangoli le spinte opposte si annullano e chi nasce in mezzo resta lì — brina/seme 1 si spostava 1,3 px invece di 98. Adesso il punto si sceglie, non si calcola: griglia da 16 px, collisioni di layer 1 lette dall'albero e dilatate di raggio 8 + margine 16 (footprint di parchi e piazze gonfiati di 24), e si prende la cella libera più vicina al centro mappa. ⚠️ Valgono lo stesso trattamento i quattro spawn del multiplayer, che erano player_spawn ± 32 px e cadevano nello stesso lotto: ora sono player_spawn_slots, verificati sulla stessa griglia, con gli offset fissi solo come ripiego. Collaudo rifatto dall'orchestratore, non sulla parola dell'agente, 8 quartieri × 8 semi = 64 città: 0/64 dentro un lotto · 0/64 dentro una collisione · 0/64 impastati · 0/64 bloccati · 0 sacche irraggiungibili · 0/256 slot MP KO, caso peggiore 98,0 px (era 1,3). Costa 1,3-1,5 ms per città, una volta sola in generate(). La sonda resta committata (shot_su308_spawn.gd + la sua scena: con -s gli autoload non sono registrati alla compilazione e WorldGenerator, che cita GameState, non compila affatto).
  • [fix] SU-287 — la pensilina proietta TRE ombre indipendenti, e a mezzogiorno il riparo si legge. KO di Ivan: «l'ombra va gestita in triplice ombra: una che gestisce il lato sinistro, un'ombra che gestisce il lato dietro e un'ombra che gestisce il tetto», e soprattutto «deve occupare già dentro la pensilina fino dopo quando il sole è a nord e a mezzogiorno, quando il sole va a sud deve solo andare dietro». Prima era una sagoma composta sola, ancorata al piede dei montanti: cadeva tutta davanti e sotto la tettoia il pavimento restava illuminato. ⚠️ Il punto architetturale, che è la ragione per cui il giro scorso non poteva funzionare: _spawn_ombra_generica è il modello dei muri — ribalta la silhouette sul piede, la allunga di altezza × 0,45 e la fa sparire a mezzogiorno. Un tetto è un piano: la sua ombra è l'impronta traslata, esiste anche a mezzogiorno, e la lunghezza dipende dalla profondità, non dall'altezza. Nessun parametro poteva far stare una sagoma unica dentro e oltre insieme. Quindi: parte dietro e lato sinistro restano pareti verticali e passano dalla _spawn_ombra_generica invariata (ciascuna ancorata alla propria riga di piede), mentre la tettoia ha un giro suo (_spawn_ombra_tettoia) con lo stesso shader, la stessa ombra_alpha e lo stesso skew delle ombre di SU-198. Guardati i cinque provini a orari diversi: sole a nord → dentro la pensilina e ben oltre; mezzogiorno → esattamente l'impronta; sera → interno pulito e ombra che sbuca dietro. Moneta e barbone messi apposta sotto la tettoia in tutti e cinque: sempre sopra l'ombra. Riparo e zona di gioco invariati. 📌 Scartate e dichiarate due strade: allungare la sagoma composta (non sa stare dentro e oltre) e scalare scale.y a 4× per rubare lunghezza al modello dei muri (si rompe al primo ritocco di ombra_lunghezza).
  • [fix] SU-298 — gli avvisi meteo si nascondono in pausa e tornano alla ripresa; il game over resta definitivo. Secondo KO: «gli avvisi temporale rimangono sopra i menu anche quando metto in pausa, devono scomparire durante la pausa o il game over (nascosti) e riapparire se ritorno in game». Il giro scorso c'era un bool solo, e serviva a spegnere per sempre. Ora i motivi sono due flag indipendenti in OR_presentation_muted_by_pause (reversibile) e _presentation_muted_by_gameover (definitivo) — così un «riprendi» non può mai riaccendere ciò che il game over ha spento, nemmeno quando il game over arriva da dentro la pausa. ⚠️ Un pezzo che il lotto aveva dichiarato NON FATTO e che è stato chiuso dall'orchestratore: la freccia «vai al riparo» restava congelata a schermo sopra il pannello di pausa. Il motivo è sottile: Player._update_shelter_arrow interroga già WeatherSystem e si spegnerebbe da sola, ma gira dentro _physics_process, che con get_tree().paused si ferma — quindi non arriva mai a spegnersi. La cura è NOTIFICATION_PAUSED, che raggiunge ogni nodo anche a processo fermo; alla ripresa non si riaccende niente a mano, ci pensa il primo frame, che è anche l'unico a sapere se il riparo serve ancora. Vale solo in single player, perché in MP le pause globali non esistono. Censimento in-engine dei nodi veri, cinque scatti nella stessa run: in gioco banner=true overlay=true freccia=true → in pausa banner=false overlay=false freccia=false col pannello leggibile → ripreso banner=true overlay=true freccia=true → game over da dentro la pausa, tutto pulito. Il flag resta locale e non viaggia in rete: la macchina delle ondate è intatta, quindi in MP chi muore vede pulito e chi gioca continua a vedere tutto.
  • [fix] SU-288 — la coda della freccia dell'evento è più ciccia. KO: «ok ma la base della freccia più ciccia, ora è troppo sottile». ARROW_SHAFT_HH da 3.0 a 5.0, un numero solo, tarato guardando e non a intuito: a 3.0 la coda quasi spariva alla scala vera, a 6.0 la freccia leggeva come una bandierina. Provini alle cinque direzioni su fondo metà asfalto e metà verde, di giorno e di notte, più uno in gioco per il rapporto col barbone. Il resto del disegno non è stato toccato: resta la rasterizzazione a celle allineate allo schermo di SU-288, dove il nodo non ruota ma ruota la matematica.
  • [fix] SU-309 — il prompt «Chiedi» sopra il barbone ora cambia lingua a partita in corso. Player.gd era l'unico file con un'etichetta di testo persistente a non essere agganciato a I18n.lingua_cambiata: la traduzione era giusta, mancava solo il riaggancio, e _refresh_interact_label() veniva chiamata solo in _ready() e al cambio di tipo di input. Da quando SU-307 ha portato le OPZIONI dentro la pausa il difetto si vede davvero. Una riga: I18n.lingua_cambiata.connect(_refresh_interact_label). Guardato il passaggio it → ru sulla stessa istanza di barbone, senza riavviare la partita, con lo scatto di riscaldamento buttato via (il primo esce sempre in italiano perché I18n non ha ancora caricato). Tolto anche il segnaposto [F] Chiedi scritto a mano in Player.tscn, che citava per giunta un tasto sbagliato. 📌 Rassegna dello stesso difetto altrove: nessun altro file chiama I18n.t() dentro _ready() tenendosi l'etichetta; l'unico simile è BetaGateScreen._apply_state(), ma è la schermata pre-menu dove la lingua non si cambia — lasciato stare.
  • [fix] SU-83 — specchiata SOLO la riga EST: le quattro diagonali tornano byte per byte a com'erano. Quarto giro, e il KO era secco: «avevo chiesto di specchiare solo EST e OVEST puri, NORD EST NORD OVEST SUD EST E SUD OVEST andavano già bene e li hai spaccati». ⚠️ Cosa avevamo capito male il giro scorso: il KO precedente («est e ovest sono al contrario») era stato letto come «lo sheet intero è specchiato», e avevamo flippato tutte e 25 le celle. Ma lo sheet è una griglia 5×5 dove le righe sono direzioni indipendenti (0=E, 1=SE, 2=S, 3=NE, 4=N, e ovest/NO/SO non esistono: sono flip_h delle est): sbagliava una riga sola, e specchiando tutto abbiamo rovesciato le quattro che erano giuste. La correzione: ripartire da 973cc91 e specchiare solo le 5 celle della riga EST. Verificato in modo indipendente, non sulla parola dell'agente: 5 celle su 25 differiscono da 973cc91, tutte e cinque nella riga E; SE, S, NE e N sono byte-identiche a quello stato. 📌 Perché la misura del giro scorso non aveva visto niente: il bbox cella per cella misura *dove sta* la sagoma, non *dove guarda* — un criterio guardato tradotto in un criterio misurato, la stessa forma di errore di SU-210. Stavolta la prova è un foglio delle otto direzioni come le disegna il gioco (le tre versioni a confronto, diagonali cerchiate), guardato. Ancore dei prop (gatto in braccio, lanterna), misurate a soglia alpha significativa contro il barbone base: 1 cella oltre 1 px, la stessa che era già fuori in 973cc91 — cioè lo stato approvato — contro le 6 dello sheet bocciato. Correzione con PIL, mai rigenerando; .import non toccato; 9681 pixel opachi, nessuno perso. Nuovo provino shot_su83_ko4_skin_f1.gd, che cammina in otto direzioni (il precedente ne copriva sei, ed è per questo che le diagonali erano passate).
  • [fix] SU-293 — la trombetta era diventata una tromba d'orchestra in cinque lingue su otto. Il ticket era tornato in «Da fare» senza un KO scritto, e guardando il motivo c'era: il criterio «termine giusto per ogni lingua, non un misto» era stato dichiarato FATTO ed era falso. La carta in ui_meta.csv diceva correttamente la trombetta da festa (PARTY HORN, TROMPETTE DE FÊTE, PARTYTRÖTE, ДУДКА…), ma le tre stringhe che il giocatore legge in partitaPLAYER_TROMBETTA_SCARICA, PLAYER_BUSKING_HINT, PLAYER_BUSKING_FINE in ui_gioco.csv, un file mai aperto dal lotto — dicevano Trumpet · Trompette · trompeta · Trompete · Труба: lo strumento da orchestra, cioè esattamente ciò che il ticket vietava. ⚠️ La forma dell'errore è quella già vista su SU-198: la spunta era stata fatta sui file toccati invece che sul criterio, e un criterio che parla di «tutte le 8 lingue» non si verifica guardando solo dove si è lavorato. Ora en/fr/es/de/ru usano il termine della carta (party horn, trompette de fête, trompeta de fiesta, Partytröte, дудка); zh e pt erano già coerenti e non sono stati toccati. Misurato: 110 righe, 0 celle vuote su tutte e 8 le colonne, audit_emoji_pittogrammi.py codice 0, zero occorrenze residue della tromba d'orchestra nei CSV. L'id interno "kazoo" resta com'era, deliberatamente: è la stessa stringa scritta in user://meta.cfg e rinominarla cancellerebbe i progressi dei tester.
  • [docs] SU-296 — brainstorm chiuso: il premio della notte fuori apre LA FUMAROLA, non NOTTEFONDA. Decisione di Ivan: «facciamo la terzultima stazione allora, 3 notti superate in qualsiasi partita (anche non nella stessa)». La sesta stazione cambia chiave — da bidoni_rovistati >= 60 a notti_fuori >= 3 — mentre LA COLLINA (7ª, giorno3_raggiunto) e NOTTEFONDA (8ª, retate_sopravvissute) restano come sono, confermate. Aperti tre ticket di implementazione: SU-310 (contatore a vita, impresa NOTTAMBULO alla prima notte, ricevuta comica all'alba, FUMAROLA alla terza, più l'indizio del Quaderno che oggi racconta ancora la condizione vecchia), SU-311 (la riga di tutorial che dice che stare fuori premia senza dire cosa), SU-312 (il rifugio in multiplayer). 📌 Due fatti verificati leggendo il codice, che valevano il giro: MetaProgress.unlocked è persistito e is_unlocked() legge da lì invece di ricalcolare dai contatori, quindi chi ha già aperto la FUMAROLA coi 60 bidoni non la perde; e il cooldown del rifugio oggi è 600 secondi = 10 minuti, non 5 — il «ogni 5 minuti» di Ivan era una modifica, non una descrizione. Nessuna riga di codice di gioco toccata, come chiedeva il ticket.

RIENTRI ATTESI: SU-83, SU-287, SU-288, SU-289, SU-293, SU-296, SU-298, SU-303, SU-304, SU-308, SU-309 — le undici chiavi messe In revisione in questo giro. Al prossimo sprint, prima dei lotti: quante sono tornate in «Da fare» e con quale KO.

*(Giro precedente: delle ventitré chiavi attese ne sono rientrate nove — SU-83, SU-287, SU-288, SU-289, SU-293, SU-296, SU-298, SU-303, SU-304: tasso di rientro 9 su 23, il 39%, contro il 2 su 15 del giro prima e il 6 su 7 del peggiore mai registrato.)* ⚠️ Le nove non pesano uguale, e separarle è il punto: difetti nostri veri sono tre — SU-83 (le diagonali specchiate per aver letto «una riga sbagliata» come «lo sheet intero»), un pezzo di SU-289 (il titolo che sbordava, perché la larghezza si misurava col font sbagliato) e SU-293 (una traduzione dichiarata FATTA che era falsa). Quattro sono requisiti allargati da Ivan dopo aver visto il lavoro (SU-287 e SU-298 chiedono più di quanto diceva la descrizione; tre delle quattro richieste del KO di SU-289 sono aggiunte). Due sono giudizi estetici su generazioni (SU-303, SU-304), dove il KO è il meccanismo che funziona, non un errore. E SU-296 era un brainstorm: rientrare con una decisione è esattamente ciò che doveva fare.

📌 La lezione operativa di questo giro, che vale più del numero: SU-293 è tornato in «Da fare» senza nessun KO scritto, e la tentazione era leggerlo come un trascinamento per sbaglio. Guardando invece il difetto c'era, ed era grosso: la carta diceva «trombetta da festa» ma le tre stringhe che il giocatore legge in partita dicevano ancora la tromba d'orchestra in cinque lingue. La spunta del giro scorso era stata fatta sui file toccati invece che sul criterio — e un criterio che dice «tutte e 8 le lingue» non si verifica guardando solo dove si è lavorato. È la stessa forma di errore di SU-198. Un ticket che torna indietro senza commento non è un errore di Ivan: è una domanda a cui rispondere guardando.

Sprint, ottavo giro2026-08-07

  • [fix] SU-83 — la barbona guardava dalla parte sbagliata: sheet specchiato cella per cella. Terzo giro su questo ticket, e il KO era secco: «est e ovest sono al contrario, vanno specchiati». La causa: player_skin_f1_sheet.png era disegnato con la convenzione opposta a quella del barbone base — nelle celle della riga EST il viso stava a sinistra invece che a destra. Siccome le direzioni ovest il gioco non le disegna affatto ma le ottiene specchiando le est (flip_h, e infatti nel .tres non esistono animazioni _w/_sw/_nw), sbagliare il verso di partenza rovescia tutte e otto le direzioni in un colpo. ⚠️ Perché non ce ne eravamo accorti nei due giri prima: avevamo misurato il bbox cella per cella contro il barbone base — geometria perfetta, ≤1 px su 25 celle — senza mai chiederci *dove guardava*. Un criterio misurato al posto di un criterio guardato. Correzione con PIL, mai rigenerando: flip orizzontale delle 25 celle una per una (mai dell'immagine intera, che rovescerebbe anche l'ordine dei frame di camminata) e ricentraggio sul bbox del barbone base di 8 celle, perché lo specchio secco avrebbe portato lo scostamento da 1 a 2 px sulle ancore di gatto in braccio e lanterna. Esito misurato: scostamento massimo 1 px su tutte e 25 le celle, 0 celle fuori soglia, 9509 pixel opachi prima e dopo (nessun pixel perso). Zero righe di codice di gioco toccate: silhouette e «?» del menu si ricavano a runtime dallo sprite, quindi si sono corretti da soli.
  • [fix] SU-300 (completamento) — chiuse le altre tre differite e il doppio tocco. Il censimento del lotto World aveva lasciato scoperti quattro punti in file che allora erano di altri lotti. HUD._is_in_tutorial() era il più grave: contiene *la stessa identica istruzione* che faceva segfaultare l'app — get_tree().get_nodes_in_group(...), tipizzata, quindi su albero staccato Godot salta il controllo e dereferenzia — ed è raggiunta da una call_deferred. Guardia anche su _resize_gameover_box() (dentro il flusso di fine partita) e _mp_show_ranking(), più LevelUpScreen._open_ventaglio(), che è accodata per il level-up successivo e sotto tocca get_tree().paused e get_viewport(). Doppio tocco: _scene_change_pending su _do_restart_or_rematch e _on_exit_to_menu_pressed, così due tocchi rapidi non accodano due cambi scena — che è la strada più corta per rimettere in piedi il crash appena chiuso. ⚠️ Una trappola evitata scrivendolo: il flag non si arma sul ramo del rematch multiplayer, perché quello non ricarica la scena e l'HUD resta vivo — armarlo lì avrebbe bloccato per sempre i restart successivi della stessa sessione. Censimento chiuso: delle 16 differite del progetto, nessuna tocca più l'albero senza guardia.
  • [feat] SU-295 (completamento) — le teste di gatto si riaccendono, e si vede. L'aggancio esistente era un poll silenzioso che ricoloriva le icone senza un fiato. Nuovo HUD._on_player_lives_restored, speculare a quello della vita persa ma al contrario: le icone recuperate si riaccendono sfalsate di 90 ms l'una dall'altra, in verde tenue, così si legge «te ne sono tornate tre» invece di un lampo unico. 📌 Nessun suono nuovo, deliberatamente: la dormita fa già partire sfx_shelter — il «sospiro di sollievo» — e sovrapporne un secondo nello stesso istante sarebbe rumore, non informazione.
  • [fix] SU-290 (completamento) — la cornice del tutorial non punta più nel vuoto. _lives_root.visible = GameState.run_active or _is_in_tutorial(): SU-232 nascondeva la fila delle vite nel tutorial, e SU-290 ci ha messo sopra un passo che la spiega e la indica. Guardato prima e dopo (TMP/su290_dopo/): la cornice gialla era vuota, ora contiene le nove teste. La barra XP resta nascosta come prima — quella nessuno la spiega — e fuori dal tutorial non cambia nulla.
  • [feat] SU-307 — le OPZIONI si aprono dalla pausa, e la schermata è una sola per menu e partita. Presa la strada raccomandata da Ivan: componente condiviso, non duplicazione. Nuovo OptionsPanel con il flag per-voce in_game nella tabella VOCI — la struttura dati che Ivan aveva chiesto esplicitamente, così una voce nuova nasce dichiarando dove si vede invece di finire in un if sparso. Estratte da MainMenu.gd la pagina OPZIONI e quella LINGUA (senza la seconda il criterio «cambia lingua a run in corso» non era nemmeno provabile, e duplicarla era vietato); RESET DATI, COMANDI, GRAFICA e CREDITI restano dove sono, aperte dal menu tramite il segnale porta_richiesta(id). MenuScrollList, che era una classe interna, è uscita verbatim in un file suo. MainMenu.gd scende da 6880 a 5775 righe. In partita si toccano 8 voci, scelte conservative: musica, dimensione controlli, d-pad digitale, suggerimenti, lingua, mantieni joypad (solo touch), dimensione testo, indietro. Fuori dalla pausa: mostra tutorial, reset dati, comandi, grafica, crediti e il nome MP. Guardato (18 PNG): il menu principale identico dopo l'estrazione, il pannello sopra la città di giorno e di notte su telefono/tablet/iPhone/iPad, e le prove a caldo — lingua → inglese con HUD e titolo pausa tradotti, testo → large (content_scale_factor 1.000 → 1.300), controlli → small, e alla ripresa il barbone cammina 22,5 px e si ferma al rilascio. Comandi veri iniettati con Input.parse_input_event: GIÙ = un passo, levetta tenuta ferma = un solo passo (l'isteresi di SU-286), ESC chiude le opzioni e lascia la pausa aperta, come chiedeva il ticket. 📌 Trovato per strada e sistemato: le quattro chiavi MENU_OPT_*KEEP_PAD* non erano mai finite nei CSV da SU-234, quindi in inglese la voce restava in italiano — ora è in 8 lingue. ⚠️ Un difetto suo, trovato dal provino e corretto: il pulsante pausa restava nascosto dopo la ripresa, perché la visibilità veniva ricalcolata mentre la modale era ancora a schermo. ⚠️ Aperti: SCHERMO INTERO a caldo non è offerto in pausa perché non è stato possibile provarlo; MP verificato solo a lettura (get_tree().paused non viene mai toccato); tocco vero su device da fare.
  • [change] SU-294 — il rifugio apre a mezzanotte, non più alle 19:00. Decisione di Ivan («proviamo mezzanotte e vediamo che succede»): SHELTER_OPEN_HOUR 19 → 0, e resta una costante sola. ⚠️ Non era un cambio di numero: con apertura a 0 la vecchia formula a OR di is_shelter_open_now() sarebbe stata sempre vera, e minutes_until_shelter_open() usciva negativa. Riscritte generiche (AND se apertura ≤ chiusura, OR se apertura > chiusura). Tabella countdown verificata in-engine su otto orari: 08:00 → 960 min, 18:00 → 360, 23:00 → 60, 23:59 → 1, 07:00 → 1020, e aperto a 00:00 / 00:01 / 06:59. Casi limite: run nata alle 2 → state=OPEN con _closed_for_schedule=false; nata alle 15 → CLOSED con _closed_for_schedule=true. In MP il gate orario resta spento, come prima. Il cartello diceva ancora «APRE ALLE 19:00»: corretto in tutte e 8 le lingue (MONDO_RIFUGIO_ORARIO) più il ripiego italiano e i commenti rimasti indietro. Bilanciamento, dichiarato: −42% di finestra, e sparisce proprio la fascia 19:00-24:00, cioè le prime due ore di notte restano senza rifugio; il recupero vite premia solo chi il rifugio lo *raggiunge*, ed è proprio quello che è diventato più raro — non si pareggiano da soli, va guardato al playtest.
  • [feat] SU-295 — dormire al rifugio restituisce tre vite. SHELTER_LIFE_RESTORE = 3, mini(cat_lives + 3, CAT_LIVES_MAX), agganciato alla dormita vera con un parametro nuovo player_slept su ShelterController.play_close(): l'unica chiamata esterna (un solo call site in tutto il progetto, sempre una pressione vera) prende il default, mentre la chiusura automatica di fine fascia passa esplicitamente false e non regala niente. 📌 L'anti-fontana non ha richiesto un contatore nuovo: dormire porta l'orologio alle 07:00, che è fuori dalla finestra, quindi il rifugio si richiude nello stesso istante e non riapre fino alla mezzanotte dopo — una dormita esaurisce da sola la notte. Tetto teorico in una run da 30 minuti: 3 dormite × 3 = 9, cioè esattamente CAT_LIVES_MAX, e solo sopravvivendo fino a lì e trovando il rifugio ogni volta. 7 asserti in-engine, tutti passati: 5+3=8; cap 8+3→9; già pieno 9+3→9 senza segnale; fuori run non tocca nulla; vite a zero non tocca nulla; la chiusura di fine fascia non restituisce. MP: nessun RPC, ShelterController gira solo sul peer che preme, quindi il recupero è per-giocatore per costruzione.
  • [change] SU-289 e SU-290 — il tutorial insegna l'elemosina per prima, e spiega le 9 vite. Nuovo ordine in TutorialWorld.STATIONS: elemosina → trombetta → gatto → 9 vite, poi tutto il resto invariato. Il primo passo spawna due NPC a cui chiedere (stesso schema già usato da «busk») e si chiude sul segnale vero EventBus.player_begged, non a tempo, come chiedeva il ticket. Passo nuovo «LE 9 VITE» con le tre cose richieste in poche righe: nove vite come un gatto, dove si vedono, e che la ronda notturna ne porta via tre in un colpo solo. Due chiavi nuove in ui_mondo.csv, 8 lingue piene (validato con csv.DictReader: zero celle vuote in tutto il file). Conteggio passi, l'assert richiesto: 19 → 20 — 19 riposizionati e 1 nuovo, verificato sia sul file originale sia a runtime. Guardato: TMP/su289/SU289_SU290_0_beg.png (elemosina come primo passo, con gli NPC in scena) e _3_lives.png. ⚠️ Un conflitto vero fra due ticket, dichiarato e non nascosto: HUD.gd nasconde sempre la fila delle vite nel tutorial (visible = GameState.run_active, scelta deliberata di SU-232), quindi la cornice di evidenziazione di SU-290 punta nel vuoto — misurato a runtime, HUD.CatLives visible=false, e visibile nel provino. SU-290 resta aperto finché non si decide se la fila si mostra anche nel tutorial.
  • [change] SU-293 — il kazoo si chiama TROMBETTA in tutte e otto le lingue. Chiavi CARTA_KAZOO_NOME/CARTA_EFF_KAZOOCARTA_TROMBETTA_NOME/CARTA_EFF_TROMBETTA in ui_meta.csv, e il termine scelto lingua per lingua è quello della trombetta da festa, non dello strumento da orchestra: PARTY HORN · TROMPETTE DE FÊTE · TROMPETA DE FIESTA · PARTYTRÖTE · ДУДКА · 小喇叭 · TROMBETA DE FESTA. ⚠️ Una traduzione corretta a mano dopo il lotto: il francese era uscito «MIRLITON», che è letteralmente la parola francese per kazoo — la rinomina in francese non sarebbe avvenuta affatto. 📌 L'icona non si rigenera: aperta e guardata (assets/ui/perk_hi/kazoo.png), è già una trombetta dorata riconoscibile, non un kazoo — quindi nessun ticket di generazione. Il suono è l'SFX condiviso busking.wav, non un asset dedicato: non toccato. ⚠️ L'id interno "kazoo" resta, ed è una scelta deliberata: è la stessa stringa scritta in user://meta.cfg (cards_seen, counters["cardmax_kazoo"], il super CONCERTONE), quindi rinominarla cancellerebbe i progressi di chi gioca già. Il ticket lo permette esplicitamente. Occorrenze di «kazoo» nel progetto: 25 → 18, e le 18 sono tutte invisibili al giocatore (10 nell'id dei salvataggi, 5 in provini usa-e-getta, 3 in commenti storici).
  • [add] SU-303 e SU-304 — generate le immagini per il colpo subito e per la vita che vola via. In TMP/, in attesa che Ivan approvi. SU-303: due frame «barbone colpito» da 32×32, uno per skin (m1 e f1), con riferimento di stile ritagliato dallo sheet della skin giusta e righello testa/piedi come secondo -i — senza quello viene tozzo, è successo in SU-201. Downscale con BOX più un filo di unsharp: col solo BOX i frame uscivano più morbidi degli originali. ⚠️ Un errore vero, trovato e corretto: la prima f1 aveva la barba, verificato con un confronto pixel contro l'idle_s esistente; rigenerata al secondo giro col riferimento frontale e il vincolo esplicito, e lo scarto resta come traccia. SU-304: cuore spezzato alato, striscia 128×32 = 4 frame con ciclo di battito completo, riuscito al primo giro. ⚠️ Due riserve dichiarate a Ivan, entrambe da decidere lui: la posa del colpito è uscita quasi orizzontale (più «steso» che «sbalzato alla Sonic») e a 32×32 si legge bene su f1 ma poco su m1; e il cuore è 32×32, quindi a size_mul 0.5 i suoi pixel sono metà di quelli del barbone e della monetina. Nessun file fuori da TMP/, verificato con git status.
  • [fix] SU-283 — il muro invisibile non era un palazzo mancante: il muro murava il lotto invece del palazzo. ⚠️ Il sospetto scritto nel ticket era falso e va detto: nessun PNG manca all'appello e nessun lotto finisce sul placeholder, verificato su 8 quartieri × 4 semi. La causa è strutturale: _make_static_wall(rect) murava il lotto intero, mentre il packing di SU-120 piazza i palazzi alla loro larghezza nativa (mai ingranditi) e distribuisce l'avanzo come aria ai bordi. Su un lotto 96×96 con un solo palazzo 61×74 — GATTOPOLI, SOLLEONE, BRINA, COLLINA, NOTTEFONDA, distretto residential — restavano ~17 px per lato e ~22 px sopra di muro cieco: copertura 53%, cioè mezzo lotto di ostacolo che non si vede. Ora il muro segue la sagoma disegnata (_muri_sagoma()), fila per fila, fermandosi allo skyline a nord. Ogni fila resta un rettangolo pieno, quindi non nasce il vicolo cieco che era stato scartato come ripiego. Misure A/B nella stessa sessione: celle di muro cieco toccabili dal barbone 29463 → 2392 (−92%), fasce ≥32 px 909 → 156, giri interrotti 5 → 0. E il disegno è pixel-identico prima/dopo (diff PNG: identici): si toccano solo le collisioni, quindi zero rischio visivo, zero tiri di dado spostati, MP invariato. Guardato: TMP/su283_coll_prima.png_dopo.png, col contorno di collisione disegnato a mano perché il debug azzurro di Godot è illeggibile.
  • [fix] SU-297 — la chiesa: il blocco duro non si riproduce, ma i due difetti veri c'erano. Flood fill col raggio del giocatore su 32 città: zero sacche chiuse. Però i giri interrotti erano 5 e adesso sono 0, e la causa sono due cose distinte: (1) il muro del landmark era disallineato — l'origine partiva dal bordo sinistro del disegno ma la larghezza era maxf(tw, lotto), quindi una chiesa da 94 px su un lotto da 96 lasciava 1 px di muro in mezzo alla strada a est e 1 px di facciata calpestabile a ovest; ora è centrato sullo sprite. (2) In 5 città su 32 un cestino 10×12 piazzato a 10 px dall'angolo tappava la corona attorno a chiesa e banca: nuovo registro _landmark_clear_rects letto solo dai cestini, tenuto apposta separato da _bin_avoid_rects — quello entra in _cappello_libero e avrebbe spostato i tiri di dado, cioè cambiato la città.
  • [feat] SU-287 — la pensilina ha la sua ombra, e nasce dai tre pezzi giusti. _ombra_pensilina_tex() compone a runtime la sagoma coi soli pezzi che intercettano la luce, come chiedeva il ticket: tetto intero, fascia di fondo (righe 29-44 di bus_stop_base.png) e montante sinistro (x ≤ 24), ritagliata al piede dei montanti. Passata poi a _spawn_ombra_generica, quindi eredita da sola direzione del sole, opacità, z-order e spegnimento delle ombre di SU-198: non è un sistema nuovo. Guardato: TMP/su287_pensilina_mattina.png e _sera.png — l'ombra nasce dai montanti, va a sud-est di mattina e si allunga a nord-est di sera come i palazzi accanto; sotto la tettoia il pavimento resta chiaro e il barbone non viene coperto. ⚠️ Un fatto trovato strada facendo: Settings.low_graphics non spegne le ombre in nessun punto del codice, e da MainMenu.MOSTRA_GRAFICA_LEGGERA = false non può nemmeno diventare vero. Il criterio del ticket è soddisfatto perché la pensilina sta nella stessa lista delle altre ombre, non perché esista davvero un interruttore.
  • [fix] SU-300 — il crash iOS di fine partita: causa trovata, riprodotta, chiusa. Ed era il gatto in braccio. La diagnosi dai quattro crash TestFlight puntava a due indiziati; il colpevole è il primo, ma la catena completa non era ovvia: GameState.reset_run_state() mette has_cat = false → il setter emette has_cat_changedWorld._on_has_cat_changedrequest_cat_refill()call_deferred("_ensure_cat_count"). Subito dopo l'HUD cambia scena, e Godot 4 stacca la scena da /root all'istante cancellandola solo dopo: al CallQueue::flush() il World è ancora vivo ma fuori dall'albero, quindi get_tree() è nullo e la chiamata tipizzata get_nodes_in_group() salta il controllo di validità (validated_call) e segfaulta a 0x138. ⚠️ La miccia è il gatto in braccio a fine run: senza gatto il setter esce subito e la differita non viene nemmeno accodata — ecco perché il crash colpiva solo alcuni tester e non tutti. ⚠️ E c'è il motivo per cui non l'avevamo mai visto in editor: la riproduzione in-engine sul Mac stampa SCRIPT ERROR: Cannot call method 'get_nodes_in_group' on a null value, mentre il SIGSEGV lo dà solo l'export di rilascio, che i controlli di debug non ce li ha. RunHUD._bind_player, il secondo indiziato, è innocente: RunHUD.tscn non è istanziata da nessuna scena. Rimedio: una guardia _tree_alive() (is_inside_tree() and get_tree() != null) in testa a 8 callback differiti di World.gd, RunHUD.gd e MoneyCoin.gd. Prova: scripts/tools/check_su300_deferred.gd, 12 asserti, 0 errori.
  • [fix] SU-285 — le monete non nascono più dentro i palazzi e i camioncini. Una funzione sola, World.snap_coin_to_walkable(), chiamata da ogni sorgente prima del broadcast _cl_coins_scattered, così host e client ricevono già la posizione buona e il pacchetto resta identico (~160 byte): intersect_shape sul layer degli ostacoli con un cerchio da 8.5 (il corpo del barbone è 8, più margine) e una spirale deterministica — passo 6, raggio massimo 66 px, anelli ruotati di mezzo passo — con sentinella Vector2.INF per «non generarla affatto», come chiedeva il ticket. Si paga alla generazione, non a ogni frame. ⚠️ Il raggio non è stato scelto a occhio: con 7.0 la spirale si incolla al bordo e il 9,5% delle monete finiva nella fascia di 1 px dove il barbone non passa (429 su 4500 misurate); con 8.5, zero. Misura finale su 15 città (centro/slums/rich × 5 semi) e 4500 monete: prima 13,91% dei punti grezzi cadeva dentro una collisione, dopo 0 col centro dentro, 0 dove il corpo non ci sta, 0 scartate. Guardato: TMP/su285_van_prima.png_dopo.png, il camioncino dei gelati coperto di monete diventa monete sul marciapiede attorno (7/8 → 0/8; sui palazzi 2/8 → 0/8).
  • [fix] SU-298 — al game over gli avvisi meteo spariscono, e il multiplayer non se ne accorge. Il baco era reale e riprodotto (TMP/lotto_hud/SU298_prima.png: «ONDATA DI CALORE IN ARRIVO 18» stampato sopra il pulsante ESCI AL MENU PRINCIPALE). Invece di spegnere un effetto per volta, l'interruttore è uno solo e locale: WeatherSystem.local_presentation_muted. Da acceso, tutte le query pubbliche (is_wave_active, wave_kind, get_wave_phase, …) rispondono «niente ondata», e i tre consumatori — banner HUD, freccia sul Player, calura/gelo/pioggia di World — si spengono da soli, per tutti gli eventi e non caso per caso come chiedeva il ticket. La *macchina* delle ondate resta intatta: è ciò che tiene in piedi il multiplayer, perché l'host collassato resta autorità meteo dei vivi e non parte nessun RPC. Scartate le due alternative ovvie (azzerare _wave_phase, emettere weather_wave_ended): spegnerebbero l'evento a tutti. Acceso in _do_gameover_presentation(), punto unico per ogni causa di fine, e rispento all'inizio della run nuova. Guardato: SU298_dopo.png, menu completamente libero.
  • [change] SU-288 — la freccia dell'evento è diventata pixel, e non è servito nessun PNG. Prima cosa da chiarire secondo il ticket: la freccia era già disegnata a codice (Polygon2D), quindi niente ticket di generazione e niente flusso a due passi. Rifatta come rasterizzazione a celle allineate allo schermo — il nodo non ruota, ruota la matematica — con la stessa tecnica della nuvoletta di SU-206: bordo nero netto, corpo ambra, filo di luce, nessuna sfumatura. ARROW_CELL = 1.0 perché in questo gioco 1 unità mondo = 1 pixel di sprite; a 2.0 il bordo mangiava le ali e alle diagonali non si leggeva più come freccia — provato e scartato col provino alla mano, non a intuito. Guardato: SU288_confronto_giorno.png (le vecchie sopra, blob a bordi lisci; le nuove sotto, ai 5 angoli, con i gradini leggibili sia su asfalto sia su verde), _notte.png e SU288_in_gioco.png per la dimensione rispetto al barbone.
  • [feat] SU-284 — la nuvoletta avvisa già al 50%, e si distingue dall'allarme a colpo d'occhio. Due livelli invece di un booleano, con isteresi su entrambe le soglie, in polling sulle stesse costanti che già emettono stat_threshold_crossed: nessuna soglia nuova inventata, come chiedeva il ticket. Il solo segnale non bastava — scatta una volta per discesa e non copre il caso «stat già bassa quando il player entra in scena». Guardato: soft = nuvoletta traslucida, allarme = bianca piena (SU284_soft_fame.png vs SU284_allarme_fame.png), e con fame e sonno sotto soglia le due icone convivono nella stessa nuvoletta invece di alternarsi (SU284_misto.png) — che è la «regola chiara» richiesta contro lo sfarfallio. ⚠️ Scelta opinabile dichiarata: l'icona della fame passa da hotdog.png a bread.png, perché il ticket dice «pane» e così coincide con l'icona della barra Fame; si reverte con una riga in Player.gd.
  • [change] SU-299 — pausa in alto a destra, sotto le scritte, agganciata alle costanti della safe-area. Il pulsante si posiziona da costanti condivise con _apply_safe_area, così se domani si aggiunge una scritta in quell'angolo scende da solo invece di finirci sotto. Guardato ai quattro formati (SU299_telefono_android/tablet_android/iphone/ipad.png + gli zoom): sempre sotto «Sereno / punteggio», mai sovrapposto a fila delle vite, orologio, barra XP o notifiche, e rientrato dal bordo sull'iPhone. I formati sono orizzontali perché project.godot ha window/handheld/orientation=4.
  • [fix] SU-286 — la levetta non salta più la carta di mezzo. LevelUpScreen.gd non aveva nessuna gestione di joypad o assi: lo stesso colpo di levetta veniva contato una volta come InputEventJoypadMotion e una seconda come azione direzionale, quindi la selezione faceva due passi e la carta centrale era di fatto inselezionabile. Aggiunto in testa a _unhandled_input lo stesso blocco di isteresi già usato da MainMenu.gd (~665) — InputManager.menu_stick_step() e set_input_as_handled(), senza scrivere una seconda gestione, come chiedeva il ticket. Stesso trattamento alla cerimonia super (SuperCeremony.gd:766, solo asse X: lì non c'è navigazione verticale). Il pannello di fine run in HUD.gd è stato controllato e usava già menu_stick_step correttamente: non toccato. Prova vera, non dedotta: scripts/tools/check_su286_stick.gd apre la schermata con carte reali e inietta InputEventJoypadMotion con Input.parse_input_event() (la pipeline vera, non una chiamata diretta) — 5/5 asserti, e col fix rimosso a mano il test fallisce riproducendo il doppio salto (0 → 2). D-pad, tastiera e touch invariati.
  • [fix] SU-291 — in lobby MP la freccia non resta più appiccicata a QUARTIERE. L'ipotesi del ticket («forse in SP funziona») era sbagliata e va scritta: non è la stessa schermata. In SP si sceglie «metro»; in MP la voce QUARTIERE vive dentro mp_lobby ed è l'unica gestita da una funzione a parte (_refresh_mp_lobby_quartiere) invece che da _refresh_mp_lobby_items. _move_mp_lobby_selection() richiamava solo la seconda: spostandosi via da QUARTIERE, freccia e colore le restavano scritti addosso perché nessuno li ricalcolava più. Una riga — la chiamata alla prima in coda alla seconda (MainMenu.gd:6385) — copre in un colpo tastiera, d-pad, levetta e tocco. Guardato: TMP/su291_1_quartiere_selezionato.png e su291_2_dopo_spostamento.png, una freccia sola e sempre sulla voce corrente; senza il fix la seconda schermata ne mostra due. Provino in scripts/tools/shot_su291_mp_quartiere.gd, che aggira l'hang di host_game() passando da _host_game_enet() come fa l'harness --mp-host.
  • [change] SU-292 — rivale e ronda notturna rallentati: adesso scappare funziona. L'unico numero che conta è il rapporto con la velocità del giocatore, che è una sola (Player.gd:7, speed = 90.0: non esistono camminata e corsa distinte). RIVAL_CHASE_SPEED 80 → 63 px/s (dall'89% al 70% del giocatore: il margine di fuga passa da +12,5% a +43%, cioè 27 px guadagnati ogni secondo invece di 10). NIGHT_GANG_CHASE_SPEED 78 → 67 px/s (dall'87% al 74%, margine da +15% a +34%), tenuta un filo sopra il rivale perché il suo colpo resta il più pesante della notte — 3 vite in una volta, SU-256. Tossico, gattara, spazzino e polizia non toccati: verificato sul diff, le uniche due costanti di velocità cambiate nel file sono quelle.
  • [feat] SU-301 — un colpo al rivale e sparisce, poi ricompare lontano e senza rancore. RIVAL_HITS 2 → 1: chi si difende ottiene un risultato al primo colpo. A fine zuffa il rivale non despawna come gli altri nemici ma si ricolloca (_rival_reappear_elsewhere), con RIVAL_RESPAWN_MIN_DIST = 800 px misurati sia dal giocatore sia dal punto in cui è sparito — prima era a tiro di gatto — e riparte pulito, hp e cooldown resettati. La sparizione è leggibile: riusa le battute di fuga RIVAL_DEFEAT_KEYS, già tradotte in tutte e 8 le lingue in ui_npc.csv, quindi zero righe CSV nuove. La refurtiva ora cade a terra invece di riaccreditarsi da sola (net_scatter_coins, la stessa funzione del drop del colpo di gatto): premia chi reagisce. Tutte le modifiche sono dentro un archetype_id == RIVAL_ARCH_ID: gli altri cinque archetipi nemici restano byte-identici. MP senza un solo RPC nuovo: World._net_host_npc_tick sincronizza le trasformate a 10 Hz e NPC._net_puppet_process ha già lo snap per i salti oltre 220 px, cioè lo stesso meccanismo del respawn dei civili colpiti dal gatto — 800 > 220, quindi il teleport si propaga da solo. ⚠️ Limite dichiarato: in MP la refurtiva dei peer remoti resta a credito istantaneo, perché solo il loro peer conosce l'importo rubato (GameState.money è locale); farla cadere anche lì richiede un parametro posizione in World.rival_refund_peer, e il diff è annotato nel codice.
  • [add] scripts/tools/shot_su83_ko3_skin_f1.gd — il provino che sarebbe servito la prima volta. Clone ridotto di shot_su203_barbone_nuovo.gd che forza la skin f1, cammina *per davvero* (Input.action_press, come TestBot) in E/W/SE/SW/N/S e per ogni direzione stampa _last_dir, flip_h e il nome dell'animazione, oltre a scattare dal viewport. È il controllo che distingue «la geometria torna» da «guarda dalla parte giusta». Verificato in partita: dir=E → flip_h=false, anim=walk_e · dir=W → flip_h=true, anim=walk_e, e nei PNG la barbona guarda a destra andando a est. ⚠️ Trappola pagata scrivendolo, ora scritta nel file: in uno script -s gli autoload non sono ancora sotto /root quando parte _initialize() — prendere /root/Skins subito lascia il MainLoop vivo con una finestra vuota, e attendi.sh va in timeout senza dire perché.

RIENTRI ATTESI: SU-83, SU-283, SU-284, SU-285, SU-286, SU-287, SU-288, SU-289, SU-290, SU-291, SU-292, SU-293, SU-294, SU-295, SU-296, SU-297, SU-298, SU-299, SU-300, SU-301, SU-303, SU-304, SU-307 — le ventitré chiavi messe In revisione in questo giro. Al prossimo sprint, prima dei lotti: quante sono tornate in «Da fare» e con quale KO.

*(Giro precedente: delle quindici chiavi attese — SU-153, SU-188, SU-236, SU-255, SU-256, SU-257, SU-258, SU-260, SU-261, SU-262, SU-263, SU-264, SU-265, SU-267, SU-268 — tredici sono Fatto, SU-267 è ancora In revisione, e l'unica in «Da fare» è SU-268, che non è un KO: è il ticket fermo in attesa del login itch.io di Ivan. Tasso di rientro per difetto: 0 su 15, il migliore mai registrato, contro 2/7, 6/7, 2/11 e 1/6 dei giri precedenti. ⚠️ Con l'avvertenza già scritta la volta scorsa e che vale ancora: cinque di quelle quindici erano consegne in attesa di un giudizio di Ivan, non codice provato in partita, quindi lo 0 non è tutto merito del codice.)*

⚠️ Ventitré chiavi in un giro solo sono il record assoluto — un terzo in più del precedente, che era già stato segnalato come «il doppio del massimo storico». Il numero del prossimo giro va letto separando tre cose, perché mescolarle non direbbe niente: (a) due sono consegne d'arte dove il «Fatto» è l'approvazione di Ivan, non un collaudo — SU-303 e SU-304, ed entrambe hanno già una riserva dichiarata da noi, quindi un rientro lì è una scelta estetica, non un difetto; (b) una è una proposta di design in attesa della sua parola (SU-296), e per giunta contiene una contro-proposta: ho raccomandato di NON legare NOTTEFONDA alla notte passata fuori, quindi se Ivan la pensa diversamente il ticket torna indietro senza che nulla sia rotto; (c) le altre venti sono codice, e quelle sì misurano la qualità del giro.

⚠️ I buchi di questo giro, dichiarati: il multiplayer non è stato collaudato (non è collaudabile su questo Mac, hang noto di host_game()) e sei ticket lo toccano — SU-285, SU-292, SU-295, SU-298, SU-301, SU-307 — tutti verificati solo a lettura del codice sul lato rete. Il crash iOS di SU-300 non è stato riprovato sui simulatori: la catena è riprodotta e chiusa in-engine sul Mac, ma le venti fini partita su SU_iphone/SU_ipad restano da girare, ed è l'unica prova che lo chiude sul campo. Nessun ticket è stato provato su device reale, e tre chiedono esplicitamente il tocco vero (SU-299, SU-307) o il playtest di bilanciamento (SU-292, SU-294, SU-295).

Quattro tester nuovi dentro gli store, e l'invito TestFlight si fa da riga di comando2026-08-06

  • [add] BETATESTING/testflight_tester.py: i beta tester Apple entrano nel gruppo TestFlight senza aprire il browser. La procedura era documentata in messaggio_reclutamento_beta.md ma non l'aveva mai scritta nessuno, e si rifaceva a mano ogni volta. Tre comandi: --elenco (chi c'è nel gruppo, con lo stato), --build (che build vede il gruppo) e --aggiungi "Nome Cognome" mail (invita davvero). JWT ES256 firmato con la P8 in ~/.appstoreconnect/, firma convertita da DER a r||s grezzo — senza quella conversione Apple risponde 401 — e chiamate HTTPS fatte con curl, perché python3 su questo Mac non ha i certificati CA. Prima di invitare controlla che al gruppo sia assegnata una build valida (senza, l'invito porta a una pagina vuota) e cerca la mail fra i beta tester esistenti per non creare doppioni. ⚠️ Due trappole pagate scrivendolo, entrambe incorporate nel codice: (1) le quadre di filter[email] fanno credere a curl che sia un intervallo da espandere e muore con exit 3 «URL malformato» senza uscire di casa — servono -g e la codifica %5B…%5D; (2) subprocess.run(check=True) in caso di errore stampa il comando JWT compreso, quindi il codice di uscita si controlla a mano. ✅ Verificato sul gruppo vero: build 0.27.2 VALID, tester passati da 23 a 24, il nuovo arrivato risulta INVITED.
  • [change] Quattro tester nuovi ammessi, tutti e quattro avvisati: Alberto Lagna, Lorenzo Lagna e Yuri Balliana su Play, Francesca Marinelli su TestFlight. Sulla Play Console la mailing list «Test Interno Street University Android» passa da 28 a 31 indirizzi (verificato ricaricando la pagina, non solo dopo il salvataggio); su TestFlight il gruppo passa da 23 a 24. Messaggi di conferma pescati da messaggio_reclutamento_beta.md senza riscriverli, cambiato solo il saluto — tranne per Lorenzo, figlio di Alberto, che non era mai stato contattato: il messaggio 2 sarebbe stato il primo contatto in assoluto e si apriva con un «sei dentro il programma beta» rivolto a uno che non sapeva nemmeno dell'esistenza del gioco, quindi ha avuto un'apertura che dice chi scrive e perché.
  • [change] Excel dei tester: due correzioni trovate leggendo le chat, non richieste. Davide Panato aveva lo Stato vuoto pur essendo già dentro su TestFlight (l'invito era partito, la riga no) → Invitato. Mauro Franceschinis aveva ancora gurzit@msn.com, la mail che lui stesso ha dichiarato irrecuperabile («non ho manco più la password»), mentre l'invito vero era già stato rifatto su mauro.franceschinis@gmail.com → allineate Email e Utente App Store. 📌 Il gruppo TestFlight conferma il secondo caso: gurzit@msn.com non c'è, mauro.franceschinis@gmail.com sì.
  • [note] ⚠️ Il bridge WhatsApp non salva i messaggi che manda lui, e cercarli nel database è un falso negativo. sendWhatsAppMessage (whatsapp-bridge/main.go:298) chiama client.SendMessage e torna success senza mai chiamare StoreMessage: nel database finiscono solo i messaggi che arrivano come evento, cioè quelli in entrata e quelli che Ivan manda dai suoi dispositivi. Un controllo «il messaggio è partito? cerchiamolo in messages.db» risponde sempre di no, anche quando è partito. 📌 La prova che partono lo stesso: nemmeno i messaggi di reclutamento mandati dal bridge stamattina sono nel database, eppure nell'arco della giornata una trentina di persone hanno risposto con la loro mail. L'unica conferma disponibile resta l'ack del server WhatsApp — che è ciò che success: true significa davvero.

La privacy policy è online, ed è scritta leggendo il codice invece che i documenti2026-08-05

  • [add] Informativa sulla privacy come pagina unica nel repo pubblico dei device beta (street-university-beta-devices/index.html), italiano e inglese. Era l'ultimo pezzo mancante di DISTRIBUZIONE_STORE.md §7.3: Play la pretende anche in closed testing, e senza un URL pubblico non si apre nemmeno il canale interno. Ospitata dove serviva già un repo pubblico, quindi zero infrastruttura nuova. È online: https://blackwindita.github.io/street-university-beta-devices/, Pages su main/root, HTTPS forzato — è l'URL da incollare in Play Console e in App Store Connect. La pagina è autoconsistente — nessun font, script o immagine da fuori — per la ragione ovvia che un'informativa sulla privacy che chiama un CDN mentre la leggi è una contraddizione in termini; chiara/scura, responsive, HTML validato (nessun tag aperto, nessuna ancora rotta) e guardata a video, non solo scritta. 📌 Il contenuto è stato ricavato dal codice, e in due punti il codice smentiva la documentazione: (1) use_custom_user_dir=true, quindi i salvataggi stanno in ~/Library/Application Support/Street University/ e non nel Godot/app_userdata/ che si sarebbe scritto per abitudine — verificato trovando lì beta_gate.cfg, highscores.cfg e gli altri sette; (2) _gate_applies() ammette solo Windows e macOS (SU-274), quindi la frase «il gioco controlla devices.json» vale solo per le build desktop di prova, e su Android/iOS sarebbe stata una dichiarazione falsa in un documento legale. Dichiarati per intero anche i tre dati che i moduli degli store tendono a far dimenticare: l'IP che arriva a GitHub con la GET di devices.json, l'IP visibile agli altri giocatori nel P2P di EOS, e il fuso orario dell'host pubblicato come attributo di lobby (SU-149) — è un'indicazione geografica grossolana, e taciuta sarebbe stata l'unica bugia per omissione della pagina. Il resto ricalca §8: codice dispositivo salato, troncato e mai trasmesso; login EOS anonimo, nessun account Epic; zero pubblicità, analytics, acquisti e chat; dati dei beta tester con base giuridica (consenso), conservazione, cancellazione e diritti GDPR; trasferimenti extra-UE verso Epic e GitHub; reclamo al Garante. Email di contatto del titolare: info@streetuniversitygame.com, scelta da Ivan — è l'indirizzo a cui arriveranno le richieste GDPR, e diventa pubblico anche sulla scheda di Play (§2.4), quindi va tenuto vivo e letto. ✅ Verificato online, non solo pubblicato: HTTP 200 senza login, http:// che rimanda a https://, email presente e nessun residuo del segnaposto nel documento servito. 📌 Controllo che valeva la pena fare: accendere Pages sul repo dei device non ha toccato il cancello della betaraw.githubusercontent.com/.../devices.json risponde ancora 200, perché il gioco legge il file per una strada diversa da quella di Pages. ⚠️ Aggiornare la pagina d'ora in poi = un commit e un push sul repo dei device, niente altro: Pages ricostruisce da sé in un paio di minuti.
  • [add] scripts/tools/shot_store_play.gd + i dieci screenshot per tablet della scheda di Play (5 soggetti × 2 formati). In STORE_ASSETS/google_play/screenshots/tablet_10/ (2560×1440) e tablet_7/ (1920×1080): menu, LA BARACCA, città di giorno con l'HUD, notte con le pozze di luce, ventaglio delle carte — tutti 16:9 come Play pretende per i tablet (il gioco è landscape, handheld/orientation=4), tutti in inglese come la scheda, tutti verificati file per file contro i limiti dei due riquadri (7": lati 320-3840; 10": 1080-7680; ≤8 MB). ⚠️ NON vengono dall'emulatore Android, ed è una scoperta che vale più degli screenshot: sull'emulatore il gioco parte e vive — pid stabile, autoload caricati, GDExtension EOS a posto — ma non disegna niente. Nero in tutte le combinazioni provate: -gpu host e -gpu swiftshader_indirect, con e senza finestra, a risoluzione nativa e forzata con wm size. 📌 Le due prove che chiudono la diagnosi invece di lasciarla a metà: adb screencap sulla home di Android dà un'immagine vera (quindi la cattura funziona), e adb emu screenrecord — che bypassa SurfaceFlinger — è nero anche lui. Non è la cattura a essere cieca, è il gioco: nei log QueuePresentKHR failed with error: 5, e --rendering-driver opengl3 non aiuta perché il layer Java sceglie Vulkan da ProjectSettings prima di leggere la riga di comando (usesVulkan(): true — renderingDevice: vulkan (ProjectSettings)). Corretta di conseguenza la memoria di progetto, che attribuiva il nero al solo headless. ⚠️ Due differenze da uno scatto su tablet vero, dichiarate e non nascoste: niente joypad virtuale (compare solo sulle build mobile) e, in menu e Baracca, voci da desktop come QUIT GAME / ENTER/E SELECT / ESC BACK; i tre scatti di gioco sono invece neutri e da soli bastano al minimo di 2 richiesto. 📌 Due difetti trovati guardando i PNG, non dai log: la notte usciva identica al ventaglio delle carte (il level-up mette in pausa l'albero, quindi la tinta giorno/notte, che converge per lerp in _process, si congelava: l'orologio segnava 22:00 su un mondo in pieno giorno) — risolto scattando la notte prima delle carte; e le carte a 1,6 s erano ancora in volo. ⚠️ imposta_lingua() SCRIVE su settings.cfg: il commento del provino diceva il contrario, ed era falso — verificato rileggendo il file, che era rimasto su lingua="en". Rimesso it e corretto il commento. 🔧 Restano gli screenshot da telefono. File: files/homeless_city/scripts/tools/shot_store_play.gd, STORE_ASSETS/google_play/screenshots/*, DISTRIBUZIONE_STORE.md.
  • [add] tools/store/genera_asset_play.py: icona 512×512 e immagine in evidenza 1024×500 per la scheda di Play, derivate dall'arte già approvata invece che generate. Escono in STORE_ASSETS/google_play/ — icona 512×512 RGBA 446 KB (limite 1024 KB), in evidenza 1024×500 RGB senza alpha 875 KB (limite 15 MB), specifiche verificate sulla guida ufficiale e misure ricontrollate sui file veri. 📌 Derivare non era la strada comoda ma l'unica giusta: l'icona nasce dal master iOS/icon.png, la stessa arte delle icone Android in assets/icons_android/, perché un'icona nuova sullo store diversa da quella che l'utente si ritrova sul telefono fa sembrare che siano due giochi. ⚠️ Il difetto vero l'ha trovato l'occhio, non il righello: il master ha la cornice dorata arrotondata cotta nel PNG e fuori da quella gli angoli sono neri, e siccome Play applica una maschera arrotondata sua, con un raggio più stretto quelle unghie nere sarebbero rimaste in bella vista sulla scheda; ritagliare avrebbe mangiato la cornice, quindi si consegna l'icona a tutto quadro riempiendo gli angoli. 📌 Due riempimenti più furbi sono stati scritti, guardati e buttati, e tutti e tre passavano i controlli numerici allo stesso modo: la diffusione con sfocature ripetute lasciava un alone sbavato, la crescita per vicinanza lasciava striature radiali a raggiera. Vince l'oro pieno campionato come mediana dell'anello esterno della cornice — l'unico che regge sotto qualunque maschera. ⚠️ Anche il taglio della key art è asimmetrico di proposito (200 px da sinistra e 82 da destra, non 141 e 141): centrato spezzava a metà parola il cartello LEARN EARN LEVEL UP sul muro di sinistra («RN / N / L UP»), peggio che non averlo. Così sui bordi non resta nessun testo leggibile, che è la condizione che Google chiede perché su alcune superfici Play ritaglia ancora — e una parola tagliata due volte diventa un refuso. Chi lo rivuole intero: --offset-sx 0. 🔧 Restano da fare i due screenshot da telefono (minimo per pubblicare, quattro consigliati a ≥1080 px), dai simulatori e non da device reali. File: tools/store/genera_asset_play.py, STORE_ASSETS/google_play/*, DISTRIBUZIONE_STORE.md.
  • [doc] DISTRIBUZIONE_STORE.md §7.4 non è più «servono descrizione breve e completa» ma le contiene, scritte. Categoria proposta Arcade (con Action come alternativa tattica e il ragionamento sul perché non è Adventure: la run finisce sempre con la Retata, non c'è arco narrativo, e chi arriva dalla lista Adventure lo scrive nelle recensioni), cinque tag presi dal vocabolario chiuso di GoogleRoguelike, Survival, Pixellated, Offline e il quinto lasciato vuoto di proposito — e le due descrizioni in inglese pronte da incollare (breve 77/80, completa 2454/4000, contate, non stimate). 📌 Correzione di Ivan che ha cambiato due cose insieme: l'online è un versus, non un co-op, quindi il tag giusto sarà Competitive multiplayer e non Cooperative — ma resta in attesa con lo slot vuoto, perché il multiplayer EOS non è mai stato collaudato davvero e taggare una modalità che non regge porta recensioni proprio su quella. Per lo stesso motivo dalla descrizione è sparita l'intera sezione co-op. ⚠️ Tolta anche la riga «nothing to pay, ever» su richiesta di Ivan («non si sa mai»): era l'unica affermazione sul futuro invece che sul presente, e un acquisto in-app un domani obbligherebbe a riscrivere la scheda e renderebbe pubblico l'indirizzo di casa (§2.4). Restano solo cose verificabili — otto lingue, zero pubblicità e analytics, e «solo play works completely offline», vero alla lettera su Android dove il BetaGate non parte. 📌 Annotata in chiaro anche la scelta editoriale della prima riga, che dichiara il tono («a cartoon roguelike… played entirely for laughs») prima del soggetto: un gioco con un senzatetto protagonista, letto senza cornice, può passare per irrisione di una difficoltà vera, e quelle dieci parole chiariscono che l'antagonista è la città-cartone. Scritto perché non venga tolto per sbaglio a una revisione futura. 🔧 Resta da scrivere la versione italiana, non bloccante per aprire il canale di test.
  • [fix] DISTRIBUZIONE_STORE.md §7.5 lasciava credere che Play inviti i tester da solo. Diceva «ogni tester riceve un link di adesione», che si legge come un'email automatica: non esiste, l'invito lo manda lo sviluppatore (verificato il 2026-08-05 sulla guida ufficiale, answer/9845334). È un errore che si maschera bene, perché la release resta su «Disponibile per i tester interni» con 0 installazioni e sembra un blocco di Google mentre è solo posta mai spedita. Aggiunti al loro posto: dove si copia il link (Test interno → Tester → Come fanno i tester ad accedere al test, forma play.google.com/apps/internaltest/<numero>), i tre passi che deve fare chi lo riceve, e i due motivi per cui al primo colpo può dire che l'elemento non esiste — account sbagliato sul telefono, oppure la propagazione della prima pubblicazione, che può chiedere qualche ora mentre gli aggiornamenti successivi arrivano in minuti. 📌 Ricontrollato nell'occasione anche il confine fra i due canali, perché è quello che decide se si perdono due settimane: il test interno non conta ai fini dei 12 tester × 14 giorni (answer/14151465), che pretendono un test chiuso. Il canale «Test chiusi - Alpha» che Play mostra come «Bozza» non va completato perché il test interno funzioni — sono indipendenti — ma finché resta bozza il cronometro dei 14 giorni non parte, il che conferma il consiglio già in §2.5 di aprirlo presto anche solo per farlo scorrere. File: DISTRIBUZIONE_STORE.md.
  • [fix] DISTRIBUZIONE_STORE.md §7.6 diceva il falso e ora dice il contrario di prima. Sosteneva che «BetaGate.gd continua a funzionare anche sulle build da Play» e che i tester andavano avvisati di far mettere il loro codice in devices.json: su Android il gate non parte mai (BetaGate.gd:94-100, decisione di SU-274). Un tester avvisato secondo quel paragrafo avrebbe cercato un problema inesistente. Riscritta col rovescio della medaglia, che è la parte che interessa davvero prima di aprire Play: chi installa da Play e poi esce dalla lista dei tester tiene la build che ha già e continua a giocarci finché non la disinstalla — su mobile un kill-switch immediato oggi non esiste. Allineate anche §7.2 (la riga «privacy policy: oggi non esiste») e §7.3, riscritta con dov'è il file, l'URL che avrà, i due passi che mancano e cosa dichiara. File: DISTRIBUZIONE_STORE.md, street-university-beta-devices/index.html (repo separato).

Il ponte EOS Android passa a pagine da 16 KB: l'AAB per Google Play non è più bloccato2026-08-05

  • [fix] SU-280 — ricompilata libeosg.android.template_release.arm64.so con segmenti LOAD a 0x4000: l'ultima delle quattro librerie native fuori norma è a posto, e l'upload su Play smette di essere respinto in partenza. Era l'avviso lasciato in coda alla voce di ieri («il blocco a monte resta»): delle quattro .so dentro l'AAB tre erano già a 16 KB (motore, SDK Epic, libc++) e l'unica a 4 KB era il ponte GDExtension del plugin epic-online-services-godot, perché la CI di 3ddelano compila con NDK r23c (da r28 il 16 KB è il default del linker). 📌 La trappola vera non era il flag ma godot-cpp: upstream esiste già una segnalazione (issue #67, marzo 2026) di chi ha ricompilato col solo -Wl,-z,max-page-size=16384 tenendo il godot-cpp 4.2 pinnato dal plugin — il gioco crashava all'avvio entro un secondo in eosg_library_init, e il maintainer conferma: serve un godot-cpp recente. Ricetta usata qui: sorgenti del plugin al tag 2.3.0 + godot-cpp godot-4.5-stable (un ref «4.6» non esiste ancora su godotengine/godot-cpp; l'API 4.5 gira sul motore 4.6.2 per compatibilità in avanti) + NDK r28.1.13356709 + flag 16 KB espliciti per cintura + due ritocchi da una riga per il salto d'API 4.2→4.5 (BIND_VIRTUAL_METHOD rivuole l'hash, List non ha più operator[]). ⚠️ Attenzione al clone col git 2.13 di questo Mac: --depth 1 --recursive lascia il submodule godot-cpp sull'HEAD di master invece del commit pinnato — scoperto perché il primo checkout risultava «del 31 luglio 2026» per un tag di giugno; il ref va imposto a mano. 📌 Verifica sull'AAB vero, non sui file sciolti: export completo del preset Android Play (AAB) e lettura delle intestazioni ELF con llvm-readelf sulle quattro .so estratte dal bundle — tutte a 0x4000 (prima: libeosg a 0x1000). E il precedente della #67 imponeva la prova d'avvio: APK release sull'emulatore SU_phone (API 36) — Godot 4.6.2 parte, la catena di autoload passa (compreso [NPCDatabase], che viene DOPO i 10 autoload H* del plugin: se libeosg non caricasse sarebbero esplosi in parse error, lo stesso motivo per cui non esiste la build web), processo stabile fino allo spegnimento dell'emulatore contro il crash <1 s della issue. Zero errori GDExtension nel logcat. ⚠️ NON MISURATO e dichiarato: login Epic + lobby multiplayer — il conflitto EOS/ENet di questo Mac è noto dal 31/07, e l'online vero si vede solo su due device reali; il ticket stesso lo metteva fra i rischi. ⚠️ È una patch da mantenere: l'addon è fuori git, quindi il binario ricompilato vive tracciato in tools/android/eosg_16kb/ con lo script applica_eosg_16kb.sh (da rilanciare dopo OGNI reinstallazione o aggiornamento del plugin — richiami aggiunti in EOS_SETUP.md §2 e RICOSTRUZIONE_DA_ZERO.md §3) e la ricetta completa in RICETTA.md; le .so originali restano accanto come *.bak_4kb. Rifatta con la stessa ricetta anche la variante template_debug.dev.arm64 (vive solo nell'addon: agli export di prova). Se upstream sistema la CI, tutta la cartella si butta — e la correzione gliel'abbiamo proposta noi: alla issue #70 il maintainer ha risposto in giornata chiedendo una PR, aperta come PR #71 dal fork BlackwindITA (CI su NDK r28 + godot-cpp 4.5-stable, submodule allineato, i due fix API, flag di cintura, compatibility_minimum 4.1→4.5 dichiarato come punto di decisione suo) — build di verifica rifatto dal branch della PR: sha256 identico al binario collaudato sull'emulatore, quindi la prova d'avvio vale bit-per-bit anche per la PR. Al primo review Delano ha chiesto di limitare il salto a godot-cpp 4.5 ai soli build Android (le altre piattaforme restano su 4.2 per coprire Godot 4.2+): fatto nel commit 471c042 — ref per-matrix nella CI, submodule e compatibility_minimum ripristinati, e la chiamata a BIND_VIRTUAL_METHOD resa condizionale su un define di versione che il SConstruct ricava da extension_api.json (l'argomento hash è nato in godot-cpp 4.4). Ri-verificato dopo lo split: android/4.5 ancora sha256-identico al collaudato, macos/4.2 compila e linka (prova del ramo vecchio). Delano ha fuso la PR la sera stessa (squash su main, entrambi i commit) — ma il primo build post-merge è uscito rosso sui 4 job Android: godot-cpp 4.5, quando ANDROID_HOME è definita (sempre, sui runner GitHub), cerca l'NDK in ndk/<ndk_version> col default 28.1.13356709, mentre setup-ndk r28 installa la 28.0.x — in locale non s'era visto perché la versione era passata esplicita. Correzione da una riga (r28b = 28.1.13356709) proposta come PR #72. ⚠️ Scoperta di servizio sulla CI di quel repo: le PR da fork muoiono TUTTE in 10 secondi su «Checkout private EOS SDK mirror repo» (Input required and not supplied: token — i secrets non arrivano ai fork), quindi il rosso della #72 è strutturale e non dice nulla sul fix: la prova vera è il run su main dopo il merge. Segnalato anche questo a Delano con la possibile via d'uscita (skip condizionale dello step). DISTRIBUZIONE_STORE.md §1 riscritta da «blocco da risolvere» a «risolto». File: tools/android/eosg_16kb/*, DISTRIBUZIONE_STORE.md, EOS_SETUP.md, RICOSTRUZIONE_DA_ZERO.md.

iOS sale su TestFlight: minimo iOS 15.0 e firma manuale2026-08-05

  • [fix] ITMS-90208: Godot scrive MinimumOSVersion fisso a 14.0 nei framework che genera, e Apple boccia la contraddizione. Convertendo le dylib GDExtension in .framework, l'esportatore iOS di Godot 4.6 ci mette dentro un Info.plist con MinimumOSVersion = 14.0 hardcoded, che *non* viene dal preset: resta 14.0 anche con application/min_ios_version="15.0". Ma la dylib di EOS è compilata per iOS 15, quindi il bundle dichiara di girare dove il suo stesso binario non regge, ed è letteralmente ciò che dice l'errore — *«the bundle … does not support the minimum OS Version specified in the Info.plist»*, dove l'Info.plist è quello del framework, non quello dell'app. 📌 La prova che indica il colpevole: EOSSDK.framework non è mai stato segnalato, e arriva da Epic già come framework col plist coerente (15.0 = 15.0); l'unico bocciato è sempre stato libeosg, l'unico generato da Godot. La toppa sta in tools/release/testflight.sh, che dopo l'export riallinea ogni plist generato al minos del binario che ha accanto. ⚠️ Due rifiuti prima di arrivarci: il primo tentativo aveva letto l'errore come «app 14.0 < framework 15.0» — vero ma non era quello che bloccava.
  • [fix] application/min_ios_version passa da 14.0 a 15.0 nel preset iOS. Necessario ma non sufficiente (vedi sopra): i Mach-O di EOS — sia EOSSDK sia libeosg, in entrambe le varianti debug e release — hanno minos 15.0, verificato con vtool -show-build, quindi con l'app a 14.0 il gioco prometteva di avviarsi dove EOS non si sarebbe nemmeno caricato. ⚠️ Alzare a 15.0 non toglie nulla di reale: nessun iPhone con iOS 14 avrebbe potuto giocare da quando c'è il multiplayer EOS. Effetto collaterale gradito: sparisce anche l'avviso ITMS-90068, che dalla primavera 2027 sarebbe diventato un errore bloccante per tutti.
  • [fix] L'export iOS va fatto in release, non in debug. Il progetto Xcode sul Desktop era stato generato da un export debug: il .pbxproj incorporava libeosg.ios.template_debug e libgodot-cpp.ios.template_debug, cioè la build per i beta tester si portava dietro l'estensione EOS di debug. Rifatto con --export-release, ora incorpora libeosg.ios.template_release. 📌 Apple non aveva segnalato questo: sarebbe passato inosservato fino a un crash o a un calo di prestazioni in mano ai tester.
  • [note] La build per TestFlight si fa da riga di comando con firma manuale, non dall'Organizer. Con «Automatically manage signing» Xcode muore su *«Team "Ivan Bianco" does not have permission to create "iOS App Store" provisioning profiles»*, perché prova a generarsi un profilo suo invece di usare Street University Distribution già creato sul portale (IsXcodeManaged = false). Certificato e profilo erano corretti fin dall'inizio. La ricetta — xcodebuild archive con CODE_SIGN_STYLE=Manual, poi -exportArchive con ExportOptions-AppStore.plist (method app-store-connect) — sta in ~/Desktop/streetUniversityIOS/, fuori da git insieme al progetto Xcode, con lo script upload_testflight.sh. ⚠️ A monte contava anche application/app_store_team_id, che puntava al team personale (2g6uc63agh) invece che a quello a pagamento (RD6E4K56FB): corretto nel preset.
  • [add] tools/release/testflight.sh + skill /testflight: dal progetto Xcode esportato alla build su TestFlight, in un colpo solo. Pre-volo (progetto esportato davvero, export release e non debug, deployment target compatibile coi framework, certificato e profilo installati), archive con firma manuale, IPA app-store-connect, post-volo sull'IPA vero e validazione; carica solo con --invia, come --pacchetto in /esporta. 📌 Ogni controllo del pre-volo corrisponde a un modo in cui oggi è andata storta, non a un'ipotesi: sono stati provati contro un progetto-esca che riproduce entrambi i difetti, e li segnalano fermandosi prima di compilare. ⚠️ Il confronto minos/MinimumOSVersion è il controllo che manca ad altool — è l'unico punto in cui ITMS-90208 si vede prima di spedire. ⚠️ Un falso negativo trovato provando lo script, non ragionandoci sopra: i controlli scritti come comando | grep -q fallivano *proprio quando il match c'era* — grep -q esce al primo match, il comando a monte prende SIGPIPE, e con set -o pipefail la condizione risulta falsa. Aveva bloccato l'invio di una build sana; ora si cattura l'output e poi si filtra. Percorso del progetto sovrascrivibile con SU_TF_PROGETTO, profilo e team con SU_TF_PROFILO/SU_TF_TEAM. Allineati /esporta §5 e WORKFLOW/04_RELEASE.md, che dicevano ancora «iOS resta da finire in Xcode» — oggi dall'Organizer non si può proprio.
  • [change] Due colonne nuove nell'Excel dei tester, «Utente App Store» e «Utente Play Store», subito dopo «Email». Servono a tenere l'account dello store separato dall'email di contatto: non sempre coincidono. Compilata per ora solo la prima, con l'email di chi ha su iPhone o iPad (9 persone). Allineati /avvisa-tester e /lista-beta, dove le colonne ora si citano per nome e mai per lettera — le lettere sono già slittate due volte (2026-07-31 con «Nickname», oggi con queste due) e ogni volta la documentazione è rimasta indietro, mentre gli script, che risolvono per intestazione, non si sono mai rotti. 📌 Corretto anche un errore preesistente: /avvisa-tester §4 indicava i link cliccabili «nelle colonne P e R», che erano già sbagliate prima di oggi (P era «Note», R «Telegram»).
  • [note] ⚠️ openpyxl.insert_cols() non sposta i collegamenti ipertestuali, e il danno non si vede. Inserendo le due colonne, le celle e i loro valori sono migrati giusti, ma i 20 hyperlink di «Link WhatsApp»/«Link Telegram» sono rimasti ancorati agli indirizzi vecchi: i loro URL sono ricomparsi come testo dentro «Versione testata», e le colonne dei link sono rimaste con la sola etichetta. Non sposta nemmeno celle unite, convalide, filtro automatico e larghezze di colonna. La strada buona è spostare le celle a mano (valore + _style + hyperlink con ref aggiornato, da destra a sinistra) e rifare a mano merge/convalide/filtro/blocco riquadri. 📌 Se ne è accorto solo il confronto prima/dopo cella per cella: senza quello il file sarebbe stato salvato rotto e nessuno l'avrebbe notato fino al prossimo /avvisa-tester.
  • [doc] DISTRIBUZIONE_STORE.md §6 diceva il contrario di quello che funziona. Consigliava «Automatically manage signing, che è la strada meno dolorosa» e dava l'invio iOS per manuale da Xcode: la prima è esattamente la strada che non funziona, la seconda è superata da /testflight. Corretti entrambi i punti — una guida che indirizza sull'unica strada rotta è peggio di una guida assente. ✅ Un timore invece rientrato: il BetaGate non tocca iOS (il preset non ha la feature beta e _gate_applies() ammette solo Windows/macOS, BetaGate.gd:99), quindi il recensore della Beta App Review non verrà bloccato dal buttafuori.
  • [note] Il numero di build va alzato a ogni caricamento. CFBundleVersion eredita config/version (0.27), ma TestFlight rifiuta due caricamenti con lo stesso numero, e uno bocciato in elaborazione lo consuma comunque: il secondo tentativo è salito come 0.27.1 con versione commerciale invariata a 0.27, passando CURRENT_PROJECT_VERSION a xcodebuild senza toccare il preset.

Android esce dal giro dei tester: la distribuzione passa dal Play Store2026-08-05

  • [change] Decisione di Ivan — «la skill /esporta non deve più esportare per android, ci penserò con il Play Store». Android smette di essere un canale di distribuzione ai beta tester: niente più APK pubblicato nella cartella ANDROID/ di Google Drive, niente più export Android dentro il giro di release. 📌 La modifica che conta non è nella documentazione ma nello script, perché è l'unico punto in cui il divieto si può far rispettare: in export_all.sh il default passa da (mac win android) a (mac win), e soprattutto il ramo android con --pacchetto non chiama più publish_apk.sh — verifica l'APK, lo lascia sul disco e dice dov'è. Le sole parole non sarebbero bastate: un --pacchetto battuto per abitudine avrebbe rispedito l'APK ai tester senza che nessuno lo volesse. ⚠️ Il target android NON è stato rimosso, ed è deliberato: la build per Play va comunque prodotta, quindi resta completo (keystore, template Gradle, pre-volo) e si chiede esplicito; all continua a comprenderlo. Chi avesse un motivo per riaprire la pubblicazione lo fa con SU_ANDROID_PUBBLICA=1, cioè scegliendolo invece di inciamparci. Provato nei tre casi: variabile assente, vuota e 0 non pubblicano, solo 1 pubblica. 📌 Due effetti collaterali buoni, misurati e non supposti: il pre-volo di default ora tocca solo macOS e Windows Desktop e torna in un secondo invece che in minuti, perché non passa più da Gradle; e un problema di keystore o di build template Android non può più fermare l'export di mac e win, che era il prezzo dichiarato quando Android era entrato nel default. Allineati /esporta (target di default, §4 sulle destinazioni, §5b riscritta come «solo se Ivan lo chiede per Play», avvisi di prima pubblicazione), /release (i canali dei tester sono due), /avvisa-tester (⛔ ai tester con solo Android non si annuncia più una build: vanno elencati a parte e decide Ivan, invece di mandargli il link di una cartella Drive che non si aggiorna più), WORKFLOW/04_RELEASE.md (passo 8b ritirato, non cancellato: resta scritto perché e cosa resta valido) e WORKFLOW/README.md. ⚠️ Segnalato, non toccato: WORKFLOW_CHEAT_SHEET.pdf non nominava Android, quindi non contraddice nulla — ma è disallineato per conto suo (parla di export a mano da Godot e di un cloud per canale, ignora /esporta), e nel repo non c'è lo script che lo rigenera. ⚠️ Il blocco a monte resta e va ricordato ogni volta che si parla di Play: libeosg.android.template_release.arm64.so del plugin EOS non è allineato a 16 KB, quindi l'AAB oggi viene rifiutato in upload finché quel binario non è ricompilato con NDK r28+. File: tools/release/export_all.sh, .claude/commands/esporta.md, .claude/commands/release.md, .claude/commands/avvisa-tester.md, WORKFLOW/04_RELEASE.md, WORKFLOW/README.md.