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 solo — GameState.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 partita — PLAYER_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_KAZOO → CARTA_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_changed → World._on_has_cat_changed → request_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).