- Street University è multilingua: italiano, inglese, francese, spagnolo e tedesco, con la voce LINGUA nelle Opzioni che cambia tutto il gioco senza riavviare
- Nei bidoni si trova l'energy drink: la caffeina come ripiego del sonno
- I bagni chimici non sono più uno solo: sei cabine colorate disposte in fila
- L'avviso ai tester non parte più a freddo: prima della frase sulle novità c'è un'apertura che annuncia la versione, scherzosa e con emoji.
- SU-193 (KO) — il bicchiere di caffè rifatto con fascia bianca e logo verde, un solo tentativo sul tetto di due.
- SU-193 — generati lattina di energy drink e caffè d'asporto
Anche i poliziotti e i passanti parlano cinque lingue2026-07-28
- [fix] KO di Ivan su SU-196: «le frasi dei poliziotti non sono ancora localizzate, compaiono sempre in italiano». Aveva ragione, e l'«ad esempio» era la parte importante:
ModularNPC.gd e PoliceOfficer.gd erano rimasti fuori dal perimetro dei cinque lotti del giro precedente — cioè proprio i file dove vivono le battute che il giocatore legge di continuo. Ora sono convertiti con la stessa forma già usata in NPC.gd: array di chiavi accanto agli array di testi italiani, che restano come ripiego, e _show_speech() riceve la chiave e traduce da sé. 139 chiavi nuove in due domini (ui_npc 110, ui_extra 29): passanti, tossico, gattara, giullare, spazzino, barbone rivale, ronda notturna, polizia (tosse, avvisi, fermo, arresto), Squadra Sgombero, più le notifiche di nemico abbattuto, gli eventi di città, il meteo e i proiettili. Le battute passano come chiave anche in rete, quindi in multiplayer ognuno legge la propria lingua invece di quella dell'host. - [fix] Le nuvolette dei passanti dicevano letteralmente «NPC_DONO_2» — difetto nato nel giro precedente e mai notato.
NPC.gd era già stato convertito a chiavi, ma quelle chiavi non erano finite in nessun CSV: t() ripiegava sulla chiave stessa e la stampava sopra la testa dell'NPC. Le 18 righe mancanti sono ora nei CSV; NPC_ALLARME_POLIZIA, che in sorgente era ancora l'inglese «HELP! Police!», è diventato «AIUTO! Polizia!». - [chore] Un controllo che dice, senza fidarsi dell'occhio, quali punti del gioco parlano ancora italiano fisso (
tools/verifica_i18n.py). Nasce da questo KO: cercare a mano i file dimenticati è esattamente il modo in cui il KO si è prodotto. Lo strumento scandaglia i punti che mostrano testo (_show_speech, big_notify, .text =) e segnala quelli il cui argomento è una frase invece di una chiave, ignorando markup BBCode, formati numerici e nomi propri; controlla inoltre che gli array di battute abbiano la loro controparte di chiavi. Esce con codice 1 se trova qualcosa, così è usabile come cancello. Sul progetto ora dà 0. A questo si affianca la verifica incrociata chiavi-usate/chiavi-definite: 504 chiavi usate nel codice e 95 dentro gli array, tutte presenti nei CSV — è il controllo che avrebbe intercettato «NPC_DONO_2» al primo giro. - [fix] Altre due etichette rimaste in inglese (SU-195, coda): gli eventi di città
police_sweep e cold_snap. E nella scena del RunHUD il livello mostrava ancora LV 1, che nel codice era già stato corretto in LIV.
Il gioco parla cinque lingue, e si cambia al volo dalle Opzioni2026-07-28
- [feat] Street University è multilingua: italiano, inglese, francese, spagnolo e tedesco, con la voce LINGUA nelle Opzioni che cambia tutto il gioco senza riavviare (SU-196). Prima non c'era nulla: nessun
tr(), nessun file di traduzione, nessuna sezione di localizzazione — le stringhe erano scritte a mano in italiano dentro gli script, quindi il ticket è partito dalle fondamenta e non dall'aggiunta delle lingue. Il motore è il nuovo autoload I18n (scripts/autoload/I18n.gd): carica i .translation generati dall'import dei CSV in assets/translations/ ed espone t(chiave, ripiego) / tf(chiave, ripiego, argomenti) più il segnale lingua_cambiata. Il ripiego italiano è la scelta che ha reso possibile fare il lavoro a pezzi: se una chiave manca, t() restituisce il testo di prima invece di lasciare la schermata muta, così le cinque lavorazioni in parallelo non si sono mai rotte a vicenda. I testi stanno in cinque CSV per dominio (ui_menu 208 chiavi, ui_hud 165, ui_mondo 130, ui_gioco 110, ui_meta 72) — un file per gruppo di schermate, che è ciò che ha permesso a cinque lavorazioni di scrivere insieme senza toccarsi: 685 chiavi in totale, 3.425 testi tradotti. Le battute restano battute anche fuori dall'italiano (RUTTO SISMICO → SEISMIC BURP / BEBEN-RÜLPSER, GATTLING → CATLING GUN / CHAT-TLING, NOVE VITE → SIETE VIDAS). La voce LINGUA è una sotto-schermata dedicata con le cinque bandierine (24×16, disegnate con PIL da tools/gen_flags.py, rigenerabili) e la lingua attiva evidenziata; la scelta si salva in Settings (user://settings.cfg) ed è retrocompatibile — chi ha già un salvataggio parte in italiano. Il cambio è a caldo: le schermate agganciate a lingua_cambiata riscrivono i propri testi, pannello aperto compreso. In multiplayer la lingua è locale: due giocatori possono giocare insieme con lingue diverse. - [fix] Quello che viaggiava in rete non era il dato ma la frase già tradotta — in multiplayer tutti leggevano la lingua dell'host (SU-196).
NetworkManager._srv_register spediva il motivo del rifiuto già scritto per esteso («LOBBY PIENA», «VERSIONE DIVERSA»): ora manda i codici DENY_FULL/DENY_RUN/DENY_VER|<versione> e li traduce il client, con il codice ignoto mostrato tale e quale invece di un avviso muto. Stessa cosa per le battute degli NPC: World._srv_beg/_srv_busk rilanciavano in broadcast la frase prodotta dall'host, ora passa la chiave e ciascuno la legge nella propria lingua — e il pacchetto è pure più corto, che sul tetto EOS di 1170 byte non guasta. - [fix] Tre punti decidevano il comportamento confrontando testi italiani: tradotti, avrebbero smesso di funzionare in silenzio (SU-196).
HUD.gd e SuperPower.gd stabilivano se suonare il ka-ching o il trombone della sconfitta con reason.contains("Grande Retata"): in inglese, francese, spagnolo o tedesco il confronto sarebbe fallito e a ogni sconfitta per retata sarebbe partito il suono sbagliato — un difetto che nessun compile-check può vedere. Ora la decisione passa da GameState.fine_per_grande_retata(), cioè dalla causa e non dal testo. Terzo caso, già vivo prima di questo sprint: Quaderno.gd confrontava un indizio con la stringa «LAVORI IN CORSO» mentre Quartieri.indizio() restituiva già il segnaposto tradotto. Collaudato dal vivo con gioco in inglese: trombone silenziato, ka-ching caricato. - [fix] Le scritte rimaste in inglese sono passate all'italiano (SU-195). Nell'HUD
🏆 Best: %d → 🏆 Record: %d e LV %d → LIV %d (barra XP e RunHUD); la nuvoletta dei passanti spaventati "HELP! Police!" → "AIUTO! POLIZIA!". Il problema più fastidioso però non era l'inglese ma il gergo tecnico che arrivava al giocatore: EOSBridge gli mostrava roba come create_lobby_async fallita (vedi log HLobbies) o connessione online (client) → errore 31 dentro «ONLINE NON DISPONIBILE». Ora _fail() tiene separati i due testi — il tecnico va solo in push_error, al giocatore arriva una frase comica («il buttafuori dell'online non ci fa entrare», «il filo verso l'host non si aggancia»). - [feat] Nei bidoni si trova l'energy drink: la caffeina come ripiego del sonno (SU-194). Nuovo esito al 15% in
Interactable._use_bin() che ridà energia con GameState.rest(15 × yield_mult) — meno della panchina (30), perché costa meno tempo e rende meno — e scala come gli altri ritrovamenti con bin_yield e il moltiplicatore bidoni_resa del quartiere. Le soglie esistenti sono state ristrette in proporzione senza azzerare nulla: cibo 30→26%, soldi 20→17%, calzino 15→13%, buca 35→29%. FX sopra il barbone con la lattina e «+N energia», sul modello di spawn_bin_food_fx, suono riusato. Come da tua indicazione entra solo la lattina, niente bicchiere di caffè («poco gestibile»), quindi niente pesca 50/50 fra due sprite. Verificato con 400 usi reali del bidone: esiti 124/51/45/61/119, coerenti con le soglie. - [feat] I bagni chimici non sono più uno solo: sei cabine colorate disposte in fila (SU-197, nasce dall'approvazione di SU-170).
WorldGenerator._spawn_bagno_pubblico() piazza tre cabine davanti affiancate — blu, rossa, verde — e tre dietro girate a nord, con le colonne monocrome (dietro la blu c'è la blu) e uno stacco di 2 px nativi, cioè la fila dietro che si intravede appena: sono le due correzioni chieste sui KO precedenti, misurate sulla linea gialla della foto di riferimento. Ordine di disegno con z_index esplicito perché la fila davanti copra quella dietro, muro solido e ingombro allargati all'intera fila, interazione replicata su tutte e tre le facce sud. La voce F1 «Bagno pubblico» piazza ora la fila intera. Gli sprite approvati non sono stati né rigenerati né ricolorati: solo copiati e piazzati. - [chore] La scrematura degli sprite che si sistemano gratis era ottimista: ora è vera, e dice 35 invece di 51 (SU-188).
tools/classifica_specchio.py giudicava se lo specchio della fascia gambe regge guardando solo la silhouette, così arch_raid_v2 — scudo da una parte, manganello dall'altra — risultava pulito benché lo specchio glieli scambierebbe. La nuova metrica cromatica raggruppa i pixel saturi per tonalità e confronta quanto pesano a destra e a sinistra: l'antisommossa scende a 0.0 ed esce dai puliti. Il controllo visivo ha poi scovato un secondo caso che la sola soglia non prendeva (lo spazzino: lo specchio gli staccava le setole dalla mano), da cui una soglia più prudente. Conto aggiornato sui 96 archetipi: 35 puliti, 30 sporchi, 31 dubbi contro i 51/28/17 di prima — numeri peggiori ma affidabili. Lo specchio è già applicato ai 35 in sprites_raw/PEOPLE/temp/walk/, senza toccare il gioco. Zero generazioni Codex, quota settimanale intatta come da tua indicazione.
Il messaggio ai beta tester si apre con un annuncio diverso a ogni release2026-07-28
- [feat] L'avviso ai tester non parte più a freddo: prima della frase sulle novità c'è un'apertura che annuncia la versione, scherzosa e con emoji. Richiesta di Ivan. Il messaggio passa da
Ciao Nome! <frase> a Ciao Nome! <apertura> <frase> — per esempio «Ciao Marco! 🦝 Un procione ha rubato la v0.26 di Street University e me l'ha lasciata qui! Ci sono il giullare e il bagno pubblico.». La variazione è garantita, non affidata al caso: genera_link_messaggi.py ha un pool di 18 aperture (tono cartoonesco: procioni, cassonetti, scatolette) e tiene in tools/release/annunci_usati.json la memoria di quelle già spese, così una frase non torna finché non sono finite tutte le altre; a pool esaurito riparte tenendo fuori solo l'ultima, che è il caso in cui la ripetizione si noterebbe davvero. La versione la legge da sé da config/version di project.godot e la sostituisce a {v} (--versione per forzarla). Il dettaglio che evita un pasticcio vero: rilanciare lo script per la stessa versione riusa la stessa apertura invece di ripescarne una nuova — se il giro d'invio si inceppa a metà e si riprende, i tester rimasti ricevono lo stesso messaggio dei primi. --rigenera-annuncio forza la pescata nuova quando l'apertura non piace, --annuncio "🎪 Circo aperto: c'è la {v}!" la scrive a mano e in quel caso il pool non viene toccato. In --tsv l'apertura scelta esce su stderr (Apertura (nuova): …), perché lo stdout è dati e va lasciato pulito. La skill /avvisa-tester ora mostra a Ivan il messaggio completo — apertura + frase — prima di inviare: la regola «mai senza il suo ok» vale sul testo intero, non solo sul riassunto. Collaudato su copia dell'Excel: rilancio a parità di versione (stessa apertura), --rigenera-annuncio (apertura diversa), versione nuova (apertura mai usata), --annuncio a mano, e stato di prova ripulito dopo il test così la prima release vera parte col pool intatto. tools/release/genera_link_messaggi.py, .claude/commands/avvisa-tester.md, WORKFLOW/04_RELEASE.md (passo 9).
Sprint 16: le note di rilascio diventano scure invece che chiare, il caffè prende la sua fascia bianca e la fila di cabine smette di sfalsare i colori2026-07-28
- [fix] SU-192 (KO) — il giro precedente aveva risolto il nero facendo il bianco, che sul cartone è illeggibile uguale. Ivan: «ma è ancora bianco! o metti un bordo nero al testo bianco oppure lo metti direttamente nero, vedi tu». Delle due strade è stata presa la seconda: testo scuro su cartone chiaro, perché su un fondo chiaro il bordo appesantisce il font pixel mentre il contrasto diretto no. Bullet e corpo passano a
#1E1710 (quasi-nero caldo), le intestazioni di versione da giallo a #6B2410 (ruggine scura) — il giallo su beige era illeggibile quanto il bianco, quindi anche il requisito originale «intestazioni in giallo» è caduto col KO. Il colore resta scritto nel bbcode e non nell'override di tema: quella parte della diagnosi del giro scorso regge, add_theme_color_override("default_color", …) su quella schermata non arriva a destinazione. Verificato a schermo lanciando il gioco vero e navigando fino alla schermata, non per deduzione. - [docs] SU-170 (KO) — la fila di cabine adotta l'opzione B, e i colori sfalsati vengono ritirati. Ivan ha scelto lo stacco stretto (2 px nativi invece di 4) e ha chiesto che i pochi pixel visibili della fila dietro siano dello stesso colore di quella davanti. È il ribaltamento della regola posta due giri fa — «stessi colori ma shiftati» — ed è giusto così: con 2 px di stacco quel colore diverso che spunta non legge come varietà, legge come un difetto grafico. La correzione non è costata né una generazione né una ricolorazione, solo l'accoppiamento per colonna dei sei PNG v8 già approvati (
nord_blu dietro sud_blu, e così via). Costo Codex: zero. Nessun ritocco a pixel è servito: il contorno del tetto, accoppiato allo stesso colore, torna coerente da sé. - [feat] SU-193 (KO) — il bicchiere di caffè rifatto con fascia bianca e logo verde, un solo tentativo sul tetto di due. Il KO diceva «generala con una fascia bianca in mezzo al bicchiere con un logo pixellato verde», e quella fascia risolve per strada anche il difetto vecchio, cioè il poco contrasto fra coperchio, fascetta e corpo. Il prompt non è stato riscritto da zero: è partito da quello del secondo tentativo precedente, tenendo silhouette a tronco di cono e coperchio staccato che erano già validi, e aggiungendo solo il requisito nuovo — con la v1, la lattina approvata e
wine.png allegati come riferimenti di forma e scala. La lattina «ZAP» non è stata toccata: era già approvata di fatto, rigenerarla sarebbe stato quota bruciata per niente. su193_coffee_v2.png resta 18×30 con alpha binaria, fuori dal progetto Godot finché Ivan non approva. - [test] SU-188 — nessuna generazione (quota Codex intatta), ma la domanda «quanti sono i pochi?» ha finalmente un numero. Ivan non poteva scegliere fra rigenerare i soli archetipi problematici, accettare che l'accessorio salti di lato o rinunciare allo specchio, perché nessuno sapeva quanti fossero. Nuovo
tools/classifica_specchio.py: misura la simmetria della fascia appena sopra la linea di taglio delle gambe, cioè se lo specchio gratis regge. Tarato sulla verità nota del giro precedente e la rispetta (player 0,44 SPORCO, police 0,97 e tourist 1,00 PULITI). Verdetto su 96 archetipi: 51 puliti, 28 sporchi, 17 dubbi. Il limite è stato cercato, non subìto: un campione di 5 puliti guardati a occhio ne ha smascherato uno sbagliato — arch_raid_v2, che ha scudo da una parte e manganello dall'altra, silhouette simmetrica e colori no, e la metrica guarda la silhouette. Quindi il 51 è ottimista e va letto come scrematura, non verdetto; la lista degli sporchi, controllata per intero, è invece solida (valigette, tracolle, scope, skateboard, zaini). Il ticket torna in «Da fare» perché la scelta resta di Ivan, ma ora è una scelta quantificata. - [docs] SU-83 e SU-194 non lavorabili, e in un caso il blocco è più profondo di quel che dice la board. SU-194 aspetta l'approvazione degli sprite di SU-193, com'è nel flusso a coppie. SU-83 aspetta SU-153, ma c'è dell'altro: SU-154 risulta «Fatto» mentre nel codice è il rollback ad essere stato applicato —
Player.set_skin() e la skin nel registro peer non esistono più in scripts/. Cioè il gancio su cui SU-83 doveva innestarsi non c'è, e la board da sola non lo direbbe.
Sprint 15: il gioco parte già a schermo intero, le note di rilascio si leggono, e due ticket di sprite aspettano l'occhio di Ivan2026-07-28
- [change] SU-191 — su desktop la finestra non balla più all'avvio.
project.godot passa da window/size/mode=2 (massimizzata) a 3 (fullscreen): la prima finestra che si vede è già a schermo intero, e il salto di ridimensionamento sparisce alla radice invece di essere mascherato. Settings._ready() fa ora il percorso inverso — torna in finestra 1024×768 SOLO se lo schermo intero è disattivato, se la piattaforma è mobile, o se fra gli argomenti c'è --bot/--windowed. Quella via d'uscita non è un accessorio: senza, ogni collaudo futuro partirebbe a schermo intero e nessuno potrebbe più fare screenshot di verifica a risoluzione nota. Il collaudo l'ha subito bocciata e ha fatto emergere due difetti veri: i flag del progetto viaggiano dopo il doppio trattino e si leggono con get_cmdline_user_args() (è la convenzione che TestBot.gd usa già per --bot), quindi con get_cmdline_args() da sola --windowed non veniva proprio visto; e apply_fullscreen_enabled(true) riaccodava un FULLSCREEN dopo il ritorno in finestra, e fra call_deferred vince l'ultima. Entrambi corretti: al boot l'escape hatch esce prima di tutto, il toggle dal menu a partita avviata resta identico. Verificato lanciando il gioco vero, non per deduzione: mode=0, finestra 1024×768. - [fix] SU-192 — le note di rilascio erano scritte in nero sul cartone. Le intestazioni di versione si vedevano (gialle) perché il loro colore è scritto esplicitamente nel bbcode; i bullet no, perché dipendevano solo dall'override di tema
default_color messo a runtime sul RichTextLabel, che su quella schermata non arriva a destinazione. Ora anche i bullet — e il messaggio di ripiego quando il file manca — hanno il colore esplicito nel markup. Verificato a schermo, non solo col compile-check. Lo stesso schema (override default_color a runtime) esiste anche in Quaderno.gd e TutorialManager.gd: se lì saltasse fuori lo stesso nero, la causa è questa. - [fix] SU-189 — lo zaino del delivery ubriaco perdeva un dettaglio rosa, ma il KO resta aperto.
ricuci_dettagli_magenta.py scartava come sfondo qualunque componente con un pixel vicino al magenta: serviva a togliere la chiazza fra le gambe, ma intercettava anche un pezzo di zaino sulla spalla, di colore troppo simile al fondo. Nuovo parametro --y-min-scarto (default 0, retrocompatibile): lo scarto vale solo nella fascia bassa, mai su casco e spalle. Va detto chiaro che sono 2 pixel: lo sheet era già allineato alla cura approvata sul male v1, e quel che resta non è un ritaglio ma la perdita del downsize da 250 a 32 px — identica sul male v1 che Ivan ha già approvato. Verificato anche che non fosse la trappola dell'asset vecchio: NPCDatabase punta davvero allo sheet corretto. - [feat] SU-193 — generati lattina di energy drink e caffè d'asporto (
sprites_raw/ITEMS/temp/, fuori dal progetto Godot finché Ivan non approva). La lattina «ZAP» 20×28 è riuscita al primo colpo e sta in scala con vino e gelato. Il bicchiere di caffè ha richiesto il secondo e ultimo tentativo del tetto: legge come bicchiere d'asporto, ma con coperchio e fascetta poco contrastati. Nessuna correzione a mano sull'arte: su questo ticket l'approvazione è di Ivan per definizione. - [change] SU-170 — la fila di cabine ricomposta, e un limite che va detto. La fila dietro è stata avvicinata fino a rispettare la linea gialla del KO (stacco da 29 a 4 px nativi), ricolorando nulla e rigenerando nulla: solo composizione con PIL, gli sprite v8 approvati sono intatti. Ma il vincolo, preso alla lettera, nasconde quasi del tutto le tre cabine dietro — sopra quella soglia restano visibili 23 px in scala anteprima, comunque le si scali. Per questo l'immagine di consegna ha due pannelli: la fila come l'ha chiesta Ivan, e i sei sprite non occlusi per poterli approvare comunque. Lo stacco in gioco lo decide il piazzamento, non il PNG: serve un ticket a parte.
- [test] Collaudo: compile-check verde su
Settings.gd e MainMenu.gd, screenshot di verifica delle note di rilascio, smoke SP col bot senza errori di script e con orphans:0, prova reale dell'avvio forzato in finestra.
Sprint 14: il fix degli sprite del giro scorso aveva rotto quelli giusti, le carte si scelgono con un tocco, e il barbone rivale si vede arrivare2026-07-28
- [fix] SU-47 — la correzione precedente aveva raddrizzato sprite che erano già dritti, e storto quelli buoni. Ivan ha rimandato indietro dodici soggetti dicendo «est e ovest invertiti», e sono esattamente quelli che il giro scorso aveva flippato credendo di ripararli. La diagnosi è stata dimostrata due volte, non ipotizzata: confrontando le sheet con
091591e^, su 11 dei 14 soggetti la riga EST guardava già a destra ed è ora che guarda a sinistra; l'unico flip davvero corretto era tourist_drunk, che infatti è l'unico dei quattordici che Ivan non ha ri-segnalato. La controprova, indipendente dalla storia di git, misura il baricentro dei pixel di incarnato sulla fascia della testa (nel profilo EST deve stare a destra del centro): 6 soggetti mai toccati tutti positivi, gli 11 segnalati tutti negativi — accordo 11 su 11 con la lista di Ivan, zero falsi positivi. Grandma v4 e v5 risultano positivi, cioè su di loro il flip era giusto: ed è il motivo per cui Ivan non li aveva citati. Correzione con ri-flip della riga 3 sugli 11 raw, più la riga 5 (NE) su police male v1 e v2 — il loro difetto era un altro, quello che Ivan descrive come «nord-ovest e nord-est invertiti»; v3 e la variante drunk, misurate corrette, sono state lasciate stare. flip_riga_raw.py è involutivo (zero pixel di differenza dopo un doppio flip), quindi i raw sono tornati esattamente al contenuto precedente. Nessuna rigenerazione: il ciclo di camminata resta la parte aperta del ticket. - [fix] SU-189 — al delivery mancavano pezzi di casco e zaino, e la variante femminile non era mai stata curata davvero. Il ticket diceva «lo stesso problema di magenta della female»: verificando, SU-187 aveva rigenerato solo la sheet della female, e le due sheet del male erano ancora quelle di maggio, mai passate da quel trattamento. Alla
male_v01 è bastato lo stesso ritaglio_perimetrale: +638 px opachi recuperati su casco e zaino, zero magenta residuo. Sulla variante drunk il perimetrale puro lasciava una chiazza magenta fra le gambe, quindi è nato tools/ricuci_dettagli_magenta.py, che ricuce per componenti connesse i soli pixel mangiati, scartando quelle che toccano lo sfondo: +418 px, zero magenta. Residuo noto e misurato, dichiarato invece che nascosto: ~32 px fucsia nella fascia dei piedi su 25 frame, circa 1 px per frame — l'alternativa era lasciare il casco rotto. Le due sheet stanno però fuori dalla pipeline standard: un import_people_sprites.py --only delivery le riporterebbe al difetto (vale già per la female), e la cura vera sarebbe far scegliere alla pipeline il ritaglio giusto per archetipo. - [change] SU-167 — un tap sulla carta già evidenziata la conferma; il doppio serve solo se si cambia idea. Ivan: «se scelgo la carta già suggerita quando arrivano non devo fare doppio click». Il giro precedente aveva introdotto l'«armamento» della carta, che imponeva due tocchi anche a quella pre-evidenziata all'apertura — cioè proprio il caso in cui la carta suggerita è già quella giusta. Tolto lo stato armato, la regola diventa una sola: tap sulla carta selezionata conferma, tap altrove sposta l'evidenziazione. La parte delicata è quella che non si vede: senza l'armamento, la de-duplica per frame diventa l'unica cosa che regge il fix originale, perché un tap fisico genera due eventi nello stesso frame (il mouse emulato di Godot, poi il touch vero) — il primo sposterebbe l'evidenziazione e il secondo, trovando la carta ormai selezionata, confermerebbe all'istante, cioè il bug di partenza travestito. Il tocco residuo all'apertura resta coperto dalla finestra
_entering che già esisteva. Vale anche per le carte super, e l'hint a schermo che prometteva ancora «1° selezioni, 2° battezzi» è stato corretto. - [change] SU-186 — su telefono spariscono le opzioni grafiche, e si parte sempre in V-Sync. Un solo punto decide chi siamo,
_is_mobile(); su mobile LIMITE FPS e SCHERMO INTERO non vengono nascosti ma proprio non costruiti, e la voce GRAFICA… sparisce dalle Opzioni, perché una schermata vuota sarebbe un vicolo cieco. Il conto delle voci era l'insidia: il menu principale è una lista di dati, ma le OPZIONI stanno a posizioni fisse, quindi togliere una riga non basta — senza equivalenti runtime delle costanti, ← INDIETRO sarebbe rimasto appeso alla vecchia posizione, lasciando un buco nel pannello e un indice selezionabile su una voce inesistente. Lato persistenza apply_fps_limit() forza Engine.max_fps=0 su mobile a prescindere dal valore salvato: un settings.cfg arrivato da un altro dispositivo non deve poter imporre un cap dove si vuole il V-Sync. apply_fullscreen_enabled() esce prima di toccare DisplayServer su mobile, lasciando intatto il ramo desktop col boot differito — quello che in un giro precedente aveva mandato la scena a schermo nero. Su desktop non cambia nulla. La voce ESCI DAL GIOCO, già introdotta, è stata riverificata e portata anch'essa su _is_mobile(): il collaudo l'ha trovata decidere per conto suo con OS.has_feature("pc") diretto — identico sul device, ma erano due punti di verità invece di uno. - [change] SU-154 — il sistema di skin a livelli è stato tolto di mezzo, su richiesta di Ivan («ripristina come era prima poi ci riproviamo con calma»). Non un revert cieco: prima verificato che nessun commit successivo avesse toccato quei file, quindi niente lavoro estraneo da salvare, poi ripristino byte per byte a
da174c9^, con diff vuoto verso quella revisione. Il gatto in braccio torna allo sheet dedicato, ubriaco e fantasma ai loro, e la skin sparisce dal registro peer. Rimossi SpriteOverlay.gd, extract_prop_overlay.py e prop_cat.png. Il prezzo del rollback, dichiarato: tornano i due bug latenti degli accessori NPC che quel commit aveva corretto per strada — la cella indovinata invece che letta dalla region dell'AtlasTexture e il mancato aggancio ad animation_changed. Oggi non fanno nulla, perché quella cartella è vuota e il codice non gira mai, ma torneranno il giorno in cui gli accessori si accendono. - [feat] SU-190 — il barbone rivale ora si vede arrivare, col triangolo rosso degli altri nemici. La tentazione era aggiungerlo a
ENEMY_ARCHETYPES, ed è la cosa da non fare: lo renderebbe abbattibile e gli toglierebbe la reazione da civile (SU-109 lo esclude apposta), mentre Ivan ha chiesto solo che si veda. Serviva quindi separare due domande che finora erano una sola — «va segnalato?» e «è abbattibile?» — con un predicato nuovo usato soltanto al gate del marker. In multiplayer si vede da sé sui client, perché il marker nasce dentro apply_archetype, che i puppet rieseguono col seed dell'host: zero righe di rete in più. - [test] SU-188 — nessuna generazione (quota Codex al 65% e strada non ancora scelta da Ivan), ma due misure che cambiano il quadro. Nuovo
tools/verifica_ancoraggio.py, che misura linea di terra e scivolamento orizzontale invece di «quanto due colonne differiscono»: era quest'ultima metrica a dichiarare 15/15 OK guardando la cosa sbagliata, perché una figura che scivola dentro la cella produce una differenza enorme senza muovere una gamba. Il verdetto sul pilota bocciato è netto: peggiora lo scivolamento in 15 casi su 15 rispetto agli originali (player SUD da 0,0 a 11,5 px) e la linea di terra in 11 su 15; solo la riga EST migliora. Provata anche, a costo zero, l'idea dello specchio della sola fascia gambe su SUD e NORD: regge pulito su police e tourist, dove cintura e tracolle stanno sopra il taglio, e fallisce sul barbone, la cui bisaccia scende dentro la fascia e viene spaccata dallo specchio. Il roster sembra dividersi in due classi, e quale strada prendere lo decide Ivan. - [docs] SU-170 — sei cabine in fila, e la scelta sull'angolo lasciata a Ivan invece che presa al posto suo. Le tre varianti di colore nascono ricolorando con PIL la v7 già approvata (mappa colore→colore sui quattro gradini di ombreggiatura, non un hue-shift che avrebbe appiattito lo shading), col tetto portato al bianco della foto: costo Codex zero e arte approvata intatta. L'ambiguità vera del KO — se «vista a 3/4 dall'alto come in foto» descriva la *disposizione* o l'*angolo di camera* — non è stata risolta a tavolino: una sola generazione di confronto mostra l'alternativa col tetto molto più presente, che però accorcia il corpo e sfuma la porta, cioè perde il dettaglio chiesto al giro precedente.
- [docs] SU-83 resta fermo, e ora i blocchi sono due: SU-153 (le 8 skin) aspetta ancora l'approvazione di Ivan, e il rollback di SU-154 gli toglie anche il gancio su cui doveva innestarsi.
Il gioco disegnava quattro volte i pixel che gli servono, e adesso gli NPC si possono guardare uno per volta2026-07-27
- [feat] Il ciclo di camminata del barbone rifatto perché le gambe si alternino davvero — e la ricetta che lo ottiene è controintuitiva: descrivere la posa da zero, mai chiederla come modifica. Doppio KO di Ivan sul giro precedente: voleva il raw ad alta risoluzione in
sprites_raw (gli avevo consegnato solo il 160×160 ridotto) e le gambe che «non si alternano bene». Sul primo punto la risposta era già in casa: NB2 aveva generato a 2048×2048, cioè più grande dei 1254×1254 di ChatGPT — bastava consegnare il raw invece del ridotto. Ripulito (fondo portato a magenta piatto, da 1534 colori distinti a 1) e messo in sprites_raw/PEOPLE/temp/barbone_nanobanana_v1_2048.png. Sul secondo punto è servito capire perché sbagliava. Il difetto non è di Nano Banana: è nel riferimento. Il modello ricalca barbone.png entro il 4,4%, e barbone.png quel ciclo ce l'ha già sbagliato — quindi copiando copia anche l'errore. Serviva impedirgli di copiarlo: come riferimento gli è stata passata solo la colonna idle, che porta l'identità del personaggio ma non i frame difettosi. Prima scoperta, che ha demolito una conclusione del giro precedente: chiesto un foglio 5×4 invece del 5×5, l'impaginazione è collassata — un tentativo ha prodotto due personaggi per cella sulle righe 4 e 5, l'altro 6 colonne e 4 righe. Quindi la griglia perfetta del giro precedente veniva dall'immagine di riferimento, non dal fatto che il modello capisca la specifica: tolto il modello di impaginazione, la struttura si sfalda. Ripiegato su una direzione alla volta (striscia di 4 frame), che è un compito banale: 5 strisce su 5, tutte con esattamente 4 sprite. Seconda scoperta, ed è la ricetta: per ottenere il contatto opposto, chiederlo come modifica del contatto A («ridisegna questo sprite con le gambe scambiate») fallisce sistematicamente — restituisce l'identico, misurato +18 contro +18. Descriverlo invece da zero e in termini di ombreggiatura («la gamba SCURA, quella lontana, davanti; la gamba CHIARA, vicina, dietro») funziona: −27 contro +18, gambe scambiate. È la stessa logica per cui il modello copia benissimo e ragiona male: se gli mostri la cosa da cambiare, la ricopia. Serviva anche la metrica giusta, e le prime due erano sbagliate: il centroide della massa gambe non risponde alla domanda (di profilo scambiare le gambe non sposta la massa) e la differenza di silhouette nemmeno. Quella che funziona misura la luminanza della gamba avanti contro quella dietro: se le gambe si scambiano, il segno si inverte. È il «cosa fanno le gambe» invece del «quanto differiscono» che era costato un giro a SU-188. Un errore mio corretto in corsa: la stessa istruzione («scura avanti») applicata a tutte le direzioni ha fallito su SE e NE, dove il contatto A aveva già la scura avanti — rigenerate con l'istruzione invertita. Per S ed N niente generazione: specchio, come proponeva SU-188, perché di fronte e di spalle il ribaltamento scambia davvero le gambe. Ma lo specchio pieno ribalta anche la tracolla, mettendola sulla spalla sbagliata: risolto specchiando solo il 38% inferiore attorno all'asse del busto — verificato, differenza busto 0,0 (borsa intatta) e gambe 45-49 (scambiate). Esito: 4 direzioni su 5 alternano (S, N, E, NE). SE resta il punto debole e va dichiarato: la metrica dice no, l'occhio dice che la falcata è comunque diversa, ma in quella vista il contrasto vicino/lontano nell'arte del personaggio quasi non esiste, quindi né la misura né l'istruzione basata sull'ombreggiatura ci fanno presa. Deriva d'identità dei frame rigenerati a parte: nella norma — busto 25-33 contro i 27-32 di variazione che c'è già fra due frame della stessa riga, cioè non si nota più di quanto varino i frame fra loro. Debolezza residua dichiarata: nella riga S il passo è poco visibile (apertura piedi 119 nel contatto contro 126 dell'idle — più stretto), che è normale in vista frontale ma resta da giudicare a occhio. Consegnati sprites_raw/PEOPLE/temp/barbone_nanobanana_v2_ciclo_1280.png (1280×1280, celle 256, fondo piatto) più _alpha, e il pronto-al-gioco TMP/nanobanana/barbone_nanobanana_v2_ciclo_160.png. La quinta colonna è la ripetizione del passaggio, come proponeva SU-188. Costo del giro 1,35 $, sessione 2,22 $. Nulla committato (i raw stanno in LFS) e convenzione 7 sempre intatta: il giudizio finale è di Ivan col VisoreNPC. - [test] Il foglio 5×5 completo col prompt di ChatGPT: Nano Banana lo replica entro lo 0,1%, ed è talmente fedele che il ciclo di camminata resta la domanda aperta che era. Terzo banco di prova, col prompt di Ivan passato verbatim (griglia 5 colonne × 5 righe, righe S/N/E/SE/NE, colonne idle + walk_1..4, sfondo magenta piatto) e
sprites_raw/barbone.png allegato come esempio, come chiesto. Due generazioni a 2K, NB2 e Pro (~0,24 $). Salto enorme rispetto al giro precedente: dove prima usciva una posa sola, qui escono fogli 5×5 completi con le cinque direzioni tutte corrette — frontale, di spalle, di profilo, 3/4 avanti e 3/4 dietro — personaggio coerente in tutte e venticinque le celle. La fedeltà è la vera notizia, ed è misurata: le bande delle colonne cadono all'8,9-18,6% della larghezza in tutti e tre i file, cioè il riferimento e le due generazioni coincidono entro lo 0,1%; NB2 e Pro combaciano fra loro al pixel (183 contro 184). Cella per cella, la differenza media dal riferimento è ~11 su 255, cioè il 4,4%. NB2 è più vicino del Pro (11,2 contro 11,6) e costa meno: secondo giro consecutivo in cui il Pro non si ripaga. Difetto comune, e non è solo di Nano Banana: il prompt chiedeva «sfondo piatto senza ombre o vignette» e nessuno dei tre l'ha rispettato — il magenta va da (240,0,231) a (255,22,251) nelle generazioni, ma anche il barbone.png di ChatGPT vignetta da (234,0,223) a (252,36,244). Nei 160×160 prodotti il fondo è stato reso piatto #FF00FF in post. Sul ciclo di camminata la risposta onesta è: non è stata risolta, e le metriche si sono contraddette. L'asimmetria del centroide delle gambe dà lo stesso segno fra walk_1 e walk_3 su tutte e cinque le righe di tutti e tre i fogli — che suggerirebbe gambe non alternate, il difetto di SU-188. Ma l'overlay delle due sagome allineate per bbox mostra differenze reali e concentrate al 66-70% nella zona gambe, che suggerisce il contrario. La contraddizione ha una spiegazione: di profilo il centroide è lo strumento sbagliato, perché scambiare quale gamba sta avanti non sposta la massa: è la stessa trappola in cui era caduto verifica_camminata.py, che misurava *quanto* due colonne differiscono invece di *cosa* fanno le gambe. Anche il ritaglio a colori delle sole gambe a 5× è risultato ambiguo. Conclusione dichiarata invece che mascherata: nessuna delle metriche provate qui risponde a «le gambe si alternano?», il giudice resta l'occhio e lo strumento è il VisoreNPC di SU-47 — ed è esattamente ciò che il progetto aveva già concluso. Il punto che però è certo: qualunque sia il verdetto, il ciclo generato ricalca quello del riferimento entro il 4,4%, quindi se il difetto c'è è ereditato da barbone.png, non introdotto da Nano Banana. Limite del test da dichiarare: una fedeltà così alta dimostra che il modello è un replicatore eccellente di un foglio esistente; non dimostra che sappia inventare un personaggio nuovo in quello stile, che è una prova diversa e non è stata fatta. Consegnati TMP/nanobanana/barbone_{nb2,pro}_160.png — 160×160, griglia 5×5 di celle 32×32, sprite appoggiati in basso, fondo magenta piatto — più le versioni _alpha.png con trasparenza. Provini: _sheet_confronto_160.png, _ciclo_E.png, _overlay_w1_w3.png, _gambe_w1_w3.png. Restano in TMP/: secondo il flusso sprite l'approvazione è di Ivan. - [test] Nano Banana sul barbone e le sue due varianti: sui personaggi va molto meglio che sugli edifici, e la coerenza fra varianti è il suo colpo migliore — ma resta un frame su venticinque. Secondo banco di prova chiesto da Ivan («proviamo a generare anche un personaggio, il barbone con le varianti ubriaco e col gatto in mano»): scelta fortunata, perché le tre varianti esistono già —
player_new_sheet.png, player_drunk_sheet.png, player_cat_sheet.png — quindi c'è verità a terra per tutte e tre invece che un giudizio a naso. Tre generazioni con NB2 (~0,20 $), la base col frame del gioco allegato come riferimento e le due varianti con la base generata riallegata, che è il modo in cui il modello dichiara di tenere l'identità. Prima correzione al metodo del giro precedente: gli sprite personaggio del gioco non sono a palette stretta come gli edifici Codex — hanno 348-376 colori e ~37 pixel semi-trasparenti a frame, quindi l'asticella dei 40 colori/alpha binaria qui non si applica e giudicarli con quella sarebbe stato sbagliato. Il salto rispetto agli edifici è netto ed è misurato: allegando il frame 32×32 ingrandito 16×, il modello ha finalmente disegnato su una griglia pixel vera (blocco ~2px misurato sulla moda delle run, contro l'assenza totale di griglia degli edifici) — la lezione trasferibile è che il riferimento va dato con la griglia visibile, non a risoluzione nativa. Coerenza del personaggio: promossa. Berretto, barba, cappotto oliva e palette restano gli stessi nelle tre varianti, e le variazioni chieste ci sono tutte: bottiglia verde e bollicina del singhiozzo nell'ubriaco, gatto rosso in braccio nel terzo. È la promessa dichiarata di Nano Banana ed è l'unica cosa che qui ha mantenuto meglio di quanto ci si aspettasse. Due difetti trovati misurando, non guardando — ed entrambi hanno smentito un'impressione a occhio, il che è il motivo per cui vanno misurati. Il primo: le uscite grezze hanno contrasto più basso del 40% (escursione luminosa 83-87 contro i 140-147 del gioco), ed è esattamente ciò che a 32px le fa leggere come macchie scure invece che come un personaggio; una correzione di livelli lo recupera per intero (134-155). Il secondo: l'alone violaceo attorno alle sagome non era la frangia magenta che sembrava — la prima metrica cercava magenta chiaro e dava 0, ma il magenta si mescola all'outline scuro e diventa viola cupo; contato come si deve è il 7,5% dei pixel contro il 2,8-4,8% degli sprite del gioco, che di viola ne hanno per conto loro nelle ombre. Un de-fringe applicato solo ai pixel di bordo lo porta a 0,2-0,8% senza mangiare le ombre buone. Con la pipeline completa i numeri combaciano: larghezza 14-18px contro 15-17 del gioco, 331-374 colori contro 348-376, escursione allineata. Il limite che decide, e che nessuna post-produzione tocca: un personaggio in questo gioco è 15 animazioni e 25 frame (idle/walk per 5 direzioni, più beg, sleep, arrest), mentre qui è stata generata una posa statica di profilo — cioè 1 frame su 25, e per giunta il più facile. La parte davvero difficile resta intatta e non provata: il ciclo di camminata con le gambe che si alternano e le 8 direzioni coerenti fra loro, che è esattamente dove anche Codex ha fallito (SU-188: walk_3 che duplica walk_1 su tutto il roster). Dichiararlo «promosso sui personaggi» sulla base di una posa ferma sarebbe disonesto. Residui a occhio dopo tutta la pipeline: il gatto in braccio è una macchia arancione molto meno leggibile di quello del gioco, la barba deriva verso il grigio invece del bruno, e resta un alone scuro sul contorno che gli sprite del gioco non hanno. Provini in TMP/nanobanana/_pg_finale_{3x,11x}.png, _pg_confronto.png, _pg_grezzi.png; sprite ridotti in pg_{base,ubriaco,gatto}_32.png. Convenzione 7 sempre intatta. - [test] Nano Banana contro Codex, misurato: dopo la post-produzione i numeri li fa tutti, ma la proporzione non la governa e a Codex non serve post-produzione. Confronto chiesto da Ivan e fatto sul banco di prova di SU-183 — edificio commerciale stretto, col
building_commercial_w48.png approvato allegato come legge di stile, cioè lo stesso metodo del giro Codex, altrimenti non sarebbe un confronto. Cinque generazioni in tutto (~0,44 $). Esito per modello. Nano Banana 1 (gemini-2.5-flash-image) è fuori gara per due fallimenti secchi: ha ricopiato il riferimento quasi identico — stesso tetto scuro, stesse cinque file di finestre, stessa insegna viola, stesso tendone verde — nonostante il prompt gli chiedesse esplicitamente un edificio *diverso*, e ha ignorato del tutto lo sfondo magenta consegnando un fondo rosa sporco non rimovibile. Nano Banana 2 (gemini-3.1-flash-image) è il migliore dei tre e l'unico candidato serio. Nano Banana Pro costa il doppio e qui non lo vale: stessi difetti, resa più fredda. La pipeline che serve, e che a Codex non serve: keying del magenta → ritaglio → downscale a h=153 → quantizzazione a 40 colori → alpha binarizzata. Applicata, i numeri si allineano: 37-40 colori contro i 39-40 di Codex e 0 pixel semi-trasparenti, cioè alpha binaria pulita. Verificata anche l'ipotesi che sembrava il rischio peggiore — che il magenta generato, non puro e rumoroso (154-186 colori distinti nel fondo invece di 1), lasciasse frange sui bordi: zero pixel magenta residui su tutte e cinque le uscite, il keying con tolleranza li toglie netti. Era una preoccupazione infondata e va detto. Il difetto vero è la proporzione, ed è strutturale: 9:16 è l'aspect ratio più stretto che l'API accetta e il modello riempie sempre la cornice, quindi al primo giro sono usciti 1:1.75 / 1:2.17 / 1:2.30 contro l'1:4 richiesto — 56px e 47px di larghezza contro la fascia 36-43 della famiglia _w48. Un secondo giro con la proporzione ribadita in modo aggressivo («esattamente 4 volte più alto che largo, colonna centrale, se ci stanno 3 finestre per piano è TROPPO LARGO») ha piegato NB2 a 34px — praticamente in fascia — ma al prezzo del soggetto: l'edificio è diventato un grattacielo pallido con dodici piani minuscoli e la vetrina ridotta a un francobollo, cioè ha perso la leggibilità a livello strada che è tutto il punto di questi sprite. Il Pro alla stessa richiesta è rimasto a 46px, disobbedendo. Il paradosso da guardare a occhio, non in tabella: l'uscita più bella e più vicina allo spirito del gioco è NB2 al primo giro — mattone caldo, tendone, insegne inglesi BOOKS & COFFEE e BAKERY perfettamente leggibili — ed è proprio quella fuori misura di 13px. Provini a 1×, 2× e 4× in TMP/nanobanana/_reale_{1x,2x}.png e _confronto_finale.png: a 4× Nano Banana impressiona, a dimensione reale il disegno è più fine e più rumoroso di Codex, perché nasce da un downscale invece che da una griglia pixel decisa (blocco pixel nativo misurato: 1px, cioè nessuna griglia). Conclusione onesta, ma la decisione è di Ivan: su sprite a specifica stretta Codex resta avanti — centra la fascia di larghezza e consegna pronto al gioco senza una riga di post-produzione (SU-183: 10 generazioni, 0 retry, 0 correzioni PIL); Nano Banana 2 è competitivo sulla resa e batte Codex in velocità ed esplorazione, dove la misura esatta non conta. La convenzione 7 non è stata toccata: dice ancora «immagini solo con Codex» e resta in vigore finché Ivan non decide. Script in TMP/nanobanana/genera_confronto.py, fuori da tools/ proprio per questo. - [change] Nano Banana configurato come MCP per un confronto con Codex, ma la generazione è ferma su un muro di quota. Richiesta di Ivan («vorrei usare nanobanana via mcp per provare a far generare anche a quella AI immagini e confrontarle col lavoro svolto»). Scelto
nanobanana-mcp-server (PyPI 0.4.5) fra sei candidati, per tre motivi verificati avviando davvero il server e interrogandone i tool via JSON-RPC, non leggendo il README: gira con uvx, cioè lo stesso pattern di elevenlabs già configurato su questo progetto; generate_image accetta fino a 3 immagini di riferimento (input_image_path_1..3), che è la condizione per riprodurre il metodo Codex di SU-183 — allegare il _w48 approvato dello stesso distretto come legge di camera, palette e outline; e salva su disco restituendo il path, con le immagini in risposta ridotte a thumbnail, quindi non intasa il contesto di base64. Registrato in ~/.claude.json a scope di progetto, con IMAGE_OUTPUT_DIR su TMP/nanobanana/ — TMP/ è già in .gitignore proprio per le immagini di ispezione rapida, quindi nessun rischio di committare roba pesante o di sfiorare LFS. Diventa attivo al prossimo riavvio di Claude Code. Il confronto però non è stato fatto, e il motivo non è aggirabile lato codice: i tre modelli immagine (gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-3-pro-image) rispondono 429 su ogni chiamata, con quote tutte marcate -FreeTier e valore zero. Non è un burst e non è stato dedotto: hanno fallito al primo colpo su tre modelli diversi — ognuno ha la sua quota al minuto — e il ritentativo a distanza di minuti ha dato lo stesso esito; la documentazione ufficiale conferma «Free Tier: Not available» per tutti e tre. La chiave in sé è buona: models.list risponde 200 con 50 modelli e gemini-flash-latest genera testo. Serve quindi attivare la fatturazione sul progetto Google dietro la chiave. Prezzi a immagine una volta attiva: $0,039 Nano Banana, $0,067 Nano Banana 2 a 1K, $0,134 Nano Banana Pro a 1K/2K — un giro da 10 sprite come SU-183 costa fra 40 centesimi e 1,34 dollari. Due inciampi d'ambiente risolti lungo la strada, entrambi ricorrenti e non ovvi: il Python 3.14 di python.org su questo Mac non ha i certificati CA (ogni HTTPS da urllib muore in CERTIFICATE_VERIFY_FAILED, mentre curl passa), e claude nella shell di Ivan è una funzione che passa da tmux, quindi claude mcp da un contesto non interattivo fallisce con «open terminal failed» e va invocato sul binario ~/.local/bin/claude. Cosa NON è stato toccato, apposta: la convenzione 7 continua a dire «immagini solo con Codex» e il divieto resta in vigore — questo giro apre una valutazione, non la chiude, e finché Ivan non vede il confronto lo strumento è sperimentale. Per lo stesso motivo lo script di prova sta in TMP/nanobanana/genera_confronto.py e non in tools/: se il confronto convince, promuoverlo è una riga di git mv. Provino di riferimento in TMP/nanobanana/_ref_commercial.png (base approvata + le due uscite Codex di SU-183, ingrandite 4×). Resta da fare a Ivan: attivare la fatturazione su https://aistudio.google.com e dire se la convenzione 7 va riaperta. - [feat] L'avviso ai beta tester impara Telegram: chi non ha WhatsApp adesso è raggiungibile. Richiesta di Ivan, nata da un caso vero: Marco Bianco ha solo Telegram ed è l'unico tester Attivo che a ogni release restava fuori dal giro (lo script lo segnalava già come
PROBLEMA, ma non aveva un canale con cui raggiungerlo). Lo script diventa tools/release/genera_link_messaggi.py (era genera_link_whatsapp.py, rinominato con git mv perché non parla più di un canale solo) e l'Excel guadagna due colonne: Q «Telegram», che compila Ivan a mano con lo @username o il numero usato su Telegram, e R «Link Telegram», generata come la P. La scelta del canale è automatica e non chiede niente a nessuno: WhatsApp se c'è il numero, Telegram altrimenti (--canale whatsapp|telegram forza la mano se serve). Il limite tecnico che cambia il protocollo d'invio, ed è il motivo per cui Telegram non è un copia-incolla di WhatsApp: Telegram non ha un equivalente di wa.me?text= per le chat personali — ?text= funziona solo per i bot (?start=) o per la finestra «condividi», che però fa scegliere il destinatario da una lista ed è esattamente il modo di finire nella chat sbagliata. Quindi il link porta alla chat giusta (web.telegram.org/k/#@username per lo username, t.me/+<numero> per il numero) e il testo lo digita Claude nel campo messaggio, rileggendolo prima di premere invia; per questo --tsv ora ha quattro colonne — nome ⇥ canale ⇥ link ⇥ messaggio — invece delle tre di prima. La verifica dell'intestazione della chat prima di ogni invio resta identica su entrambi i canali: con dieci amici veri il rischio che conta è sempre quello. Aggiunto anche un TELEGRAM_ILLEGGIBILE su stderr per le celle che non sono né uno username valido (5-32 caratteri, lettere/cifre/underscore) né un numero — la normalizzazione accetta @nome, nome, t.me/nome, il link completo e il numero in tutti i formati già gestiti per WhatsApp. Sull'Excel il criterio è stato la prudenza: colonne in coda invece che accanto a «WhatsApp / Tel.», perché inserire una colonna a metà foglio con openpyxl non sposta le celle unite né i menu a tendina (PIATTAFORME E1:I1, CANALI BUILD J1:K1, le validazioni su E3:K200 e M3:M200) e li avrebbe silenziosamente disallineati; le intestazioni nuove copiano lo stile di «Note» e la Legenda spiega le due colonne. Collaudato su copia del file vero con dati Telegram finti (username, link t.me/, numero, cella spazzatura): 11 tester smistati 9 WhatsApp / 2 Telegram, doppio giro per verificare l'idempotenza, celle unite e menu a tendina intatti a rilettura. Sul file vero sono state scritte solo le due intestazioni e la Legenda (nessun link finto), backup a parte prima di toccarlo. Resta da fare a Ivan: mettere in Q3 lo @username di Marco. tools/release/genera_link_messaggi.py (rinominato + riscritto), .claude/commands/avvisa-tester.md, WORKFLOW/04_RELEASE.md (passo 9), BETATESTING/Beta_Tester_Homeless_City.xlsx (fuori da git, è in .gitignore per privacy). - [feat] Il menu si riordina: V-Sync e schermo intero di serie, «NOTE E BUG» raccoglie due voci, e su desktop si può uscire dal gioco. Quattro richieste di Ivan dopo 12 minuti di gioco vero con V-Sync e motore
mobile a schermo intero, senza problemi. Default: fullscreen_enabled passa a true e fps_limit resta 0 (AUTO/V-Sync), sia su installazione pulita sia sul reset — e reset_to_defaults() ora li riapplica subito, perché un reset che rimette i valori ma li fa vedere solo al riavvio, per chi gioca, è un reset che non ha funzionato (aggiunto anche il refresh dei VirtualControls, che mancava). «NOTE E BUG»: NOTE DI RILASCIO e SEGNALA BUG O IDEA finiscono sotto un'unica voce del menu principale, con sotto-schermata sul pattern di RESET DATI; le schermate esistenti non sono state riscritte, solo spostate di un livello. «ESCI DAL GIOCO»: ultima voce, visibile solo se OS.has_feature("pc") — quindi nascosta su Android, iOS e nel browser, dove non avrebbe senso — con doppia conferma sul pattern dei reset. Trappola tecnica gestita: MENU_ITEMS è una const, quindi la lista effettiva si costruisce a runtime e tutto ciò che la usava è stato spostato sulla nuova (costruzione, altezza pannello, scorrimento della selezione, smistamento): se ne fosse sfuggito uno, la navigazione sarebbe andata fuori sincrono senza dare errore. Nuova voce RESET IMPOSTAZIONI, e nasce da un buco trovato lavorando: metà della richiesta di Ivan («anche per chi resetta dalle opzioni») era lettera morta, perché Settings.reset_to_defaults() esisteva ma non era richiamata da nessun pulsante — codice irraggiungibile. Voce sua e non innesto dentro RESET PROGRESSI, perché SU-173 ha stabilito che ogni reset tocca solo la propria parte: chi vuole rigiocare da capo non deve ritrovarsi la grafica cambiata senza averlo chiesto. Corretto anche il testo del menu, che diceva ancora «sono due azioni indipendenti». Verificato passando dal percorso vero (_on_reset_impostazioni_pressed, non la funzione di Settings): prima pressione arma soltanto, seconda azzera tutti e quattro i valori, ed Engine.max_fps torna a 0 — la prova che sono applicati e non solo salvati. Il primo provino aveva dato un falso negativo per un errore mio: _initialize() gira prima che gli autoload siano pronti, quindi Settings._ready() rileggeva da disco i valori appena sporcati. Sul fullscreen di default è stata riverificata anche l'esposizione ai due bug già noti (schermo nero all'avvio, finestra riscritta al boot): l'avvio è pulito, e i provini di screenshot sono stati corretti perché il fullscreen differito li sovrascriveva. Nota su project.godot: la riga allow_hidpi=true è sparita perché Godot riscrive il file omettendo i valori uguali al default dell'engine, e true è il default — comportamento invariato, verificato a runtime (framebuffer 4288×2168, scale 2.0, renderer mobile). MainMenu.gd, Settings.gd. Commit 5540055. - [change] SU-186 — Ivan sceglie: nitidezza sì, manopole no. Motore
mobile e HiDPI cablati, in GRAFICA resta lo schermo intero. Dopo aver provato la sezione appena costruita, la decisione è netta: «rimetti highdpi a true di default, togli l'opzione dal menu grafica e inserisci nel menu grafica l'opzione schermo intero, che cambia appena la tocchi, imposta anche mobile come motore di default e togli l'opzione di selezione motore grafico». Quindi in project.godot allow_hidpi=true (vince la nitidezza sulle scritte piccole delle carte: il carico si affronta altrove) e rendering_method="mobile" fisso, mentre la schermata GRAFICA si riduce a LIMITE FPS + SCHERMO INTERO, quest'ultimo applicato all'istante al tocco come richiesto, persistito e riapplicato all'avvio. Rimosse anche le MACCHINE dietro le due voci, non solo le voci: via renderer_choice con tutto il rilancio automatico del processo, via hidpi_enabled con la scrittura di override.cfg. Il rilancio in particolare andava tolto e non disattivato: confrontava la scelta salvata col motore attivo e, col default che passa a mobile, chiunque avesse già un settings.cfg con forward_plus si sarebbe visto il gioco riavviarsi da solo a ogni apertura, senza più nessuna opzione visibile che ne spiegasse il motivo. Due bug seri trovati provando, non leggendo il codice: DisplayServer.window_set_mode() chiamato in modo sincrono da Settings._ready() mandava la costruzione della scena a schermo nero, e anche differito con call_deferred riscriveva la finestra a massimizzata a ogni avvio, zittendo qualunque codice che imposti una risoluzione precisa subito dopo il boot — risolto non toccando affatto la finestra al boot quando l'opzione è spenta, mentre il toggle dal menu forza sempre lo stato esplicito. Verificato che il cambio di renderer non abbia rotto la resa in silenzio, che è il rischio vero di questo tipo di modifica: screenshot del mondo di gioco vero (edifici, HUD, barbone, gatto) con motore mobile e HiDPI acceso, arte e colori intatti; framebuffer rimisurato a 4288×2168 e motore confermato «Forward Mobile» nell'header dell'engine. FULLSCREEN e EXCLUSIVE_FULLSCREEN si sono comportati identici su questo Mac: tenuto il primo, più compatibile con Cmd-Tab. Nota UX lasciata a Ivan: da schermo intero non si esce con ESC (in menu ESC fa solo back-navigation) — non è stato aggiunto perché non richiesto. project.godot, Settings.gd, MainMenu.gd. Commit 072ac27. - [feat] SU-186 — le opzioni grafiche hanno una sezione loro, e la nitidezza Retina diventa una voce di menu. Richiesta di Ivan: «fai una sezione GRAFICA in opzioni con tutte le opzioni (limite fps, motore grafico e aggiungi anche l'opzione highdpi)». Nuova sotto-schermata
"grafica" sul pattern già collaudato di «RESET DATI…»: LIMITE FPS e MOTORE GRAFICO ci si spostano dentro e si aggiunge NITIDEZZA RETINA, con navigazione completa da frecce, stick e tap. Effetto collaterale voluto: la schermata OPZIONI torna da 10 a 9 voci e le righe riprendono respiro (altezza/passo 0,051/0,057 → 0,057/0,063) — erano state compattate due volte proprio per far entrare quelle due voci, e continuare così avrebbe portato dritti al difetto di rasterizzazione già visto sulla Baracca. La parte difficile era la nitidezza Retina, e stavolta è stata verificata PRIMA di costruirci sopra: display/window/dpi/allow_hidpi si legge all'avvio, non cambia a caldo, e non esiste alcun flag da riga di comando (controllato su --help) — quindi il trucco del rilancio che fa funzionare il motore grafico qui era inservibile. L'unica leva è override.cfg, il file con cui Godot sovrascrive le impostazioni di progetto a ogni avvio: provato sul campo prima di implementare, scrivendolo a mano senza toccare project.godot il framebuffer passa davvero da 2144×1084 a 4288×2168. Una volta scritto non serve nessun rilancio automatico: il motore lo rilegge da solo, basta chiudere e riaprire. Guardia macOS, che è il punto delicato: in una build esportata l'eseguibile sta dentro NomeGioco.app/Contents/MacOS/, e scriverci un file invaliderebbe la firma del bundle col rischio concreto che l'app non si apra più — quindi se il percorso contiene .app/ non si scrive nulla: la preferenza resta salvata e l'interfaccia dice che va applicata a mano. Verificato a runtime, non solo a lettura di codice. Conseguenza dichiarata: su Windows e Linux la voce si applica da sola, sulla build macOS no. Schermate guardate a 1024×768 e 1280×720. MainMenu.gd, Settings.gd. Commit a40bd2c. - [change] SU-186 (KO) — provato a cambiare renderer come chiesto, e la risposta è che il renderer non è la leva. Ivan: «proviamo anche a cambiare render (gl compatibility o mobile? si può mettere anche quello nelle opzioni?) e facciamo un test lì». Misurata la matrice completa renderer × HiDPI su questo Mac, stesse condizioni:
forward_plus 137 fps · mobile 135 · gl_compatibility 89 con HiDPI spento, e 135 / 135 / 89 con HiDPI acceso. Il framebuffer dipende SOLO dall'HiDPI, mai dal renderer (2144×1084 contro 4288×2168 in entrambe le terne): cade quindi l'ipotesi con cui era partito questo giro, cioè che un motore più leggero permettesse di riaccendere l'HiDPI gratis recuperando la nitidezza persa. Nessun renderer toglie il ×4 pixel. mobile e forward_plus sono indistinguibili su questo contenuto — non c'è clustering né luci dinamiche da alleggerire — e gl_compatibility, che su macOS passa da un layer di traduzione Metal→OpenGL, perde il 35% di fps: è il peggiore dei tre, non il più leggero. La voce MOTORE GRAFICO è stata aggiunta lo stesso (Opzioni: Forward+ · Mobile · Compatibilità, pattern di LIMITE FPS, persistita in Settings.gd), perché le misure qui sono su menu, finestra e sessione breve, mentre il difetto di Ivan nasce da sessioni lunghe sulla build esportata: deve poterlo verificare da sé. Difetto intercettato in revisione: l'opzione come consegnata non si applicava mai — salvava il valore e basta, e a un'app aperta col doppio clic nessuno passa --rendering-method; sarebbe stata una voce di menu bugiarda, esattamente il difetto per cui era stato (giustamente) escluso il toggle HiDPI. Aggiunto Settings._applica_motore_grafico_al_boot(): al primo avvio successivo rilancia il processo col flag giusto e chiude l'istanza corrente, con quattro guardie — marcatore che impedisce il ciclo di riavvii, un --rendering-method passato a mano che vince sulle impostazioni, mai dall'editor, mai fuori dal desktop — e nel caso di default (scelta = motore attivo) non fa assolutamente nulla, quindi il raggio d'azione è minimo. Corretto anche il testo mostrato all'utente, che affermava che Mobile e Compatibilità sono «più leggeri»: cioè il contrario di quanto misurato. Resta da decidere a Ivan il compromesso vero, che nessun renderer scioglie: HiDPI spento (¼ dei pixel, testo piccolo sgranato) oppure acceso (nitido, ×4 lavoro) — confronto affiancato in TMP/screenshots_claude/su186_nitidezza/. Opzioni verificate a schermo a 1024×768 e 1280×720, 10 righe senza sovrapposizioni. project.godot, Settings.gd, MainMenu.gd. Commit 342d2eb. - [fix] SU-186 — trovata la causa VERA del carico GPU, e stavolta è misurata: il framebuffer era quattro volte quello che serve. Con la finestra massimizzata su schermo Retina il gioco renderizzava a 4288×2168, cioè 9,30 milioni di pixel per fotogramma; ora a 2144×1084, 2,32 milioni — esattamente un quarto, a parità di tutto il resto (il canvas logico resta 1024×768 in entrambi i casi). La causa: in
project.godot mancava window/dpi/allow_hidpi, e in Godot 4 il default è attivo. Per un pixel art col filtro texture Nearest quel raddoppio Retina non comprava nessuna qualità: era lavoro buttato a ogni fotogramma, a ~120 fps. La correzione è una riga. Proxy coerenti misurati sul processo vero: CPU media 12,6% → 11,5%, RSS 656 → 573 MB; il GPU% diretto non è misurabile in questo ambiente (powermetrics vuole sudo interattivo), ma il conto dei pixel è deterministico e non è rumore di misura. Vale la pena dirlo: i due giri precedenti su questo ticket non avevano toccato niente di tutto questo — il primo aveva scritto vsync_mode=1, che è un no-op perché è già il default dell'engine, e il secondo aveva messo un cap a 60 fps che ha solo dimezzato la fluidità guadagnandosi un KO. Tarare senza misurare è costato due giri. Il compromesso da giudicare a occhio: con l'HiDPI spento è macOS a ingrandire il framebuffer fino al pannello, con un filtro morbido, quindi su pixel art può risultare meno nitido — se stona, si torna indietro togliendo quella riga. Seconda pista trovata e NON toccata, perché decide Ivan: il renderer davvero attivo è forward_plus, il più pesante dei tre, mentre config/features dichiara ancora «GL Compatibility» (tag residuo, non attivo); per un 2D puro il metodo consigliato è mobile, ma cambiarlo può alterare la resa dell'arte. project.godot (+1 riga), nuovo provino scripts/tools/su186_probe_framebuffer.gd. Commit 9a36927. - [feat] SU-47 (KO) — un visore per guardare gli NPC uno per volta, invece di girare la città sperando che passi quello giusto. Richiesta di Ivan dopo il giro precedente: «ne vedo ancora qualcuno sbagliato, ma viene difficile provarli tutti». In partita gli NPC compaiono a caso, e per vedere una direzione precisa di un archetipo preciso servirebbero ore.
scenes/dev/VisoreNPC.tscn scorre le 96 voci — e come chiesto ogni variante è un tipo a sé, variante ubriaca compresa — mostrando a schermo nome, variante, nome italiano, e soprattutto direzione + animazione risolta + flip_h (es. OVEST · walk_e · flip_h=true): è l'informazione che trasforma un «questo sembra storto» in «la riga EST di quell'archetipo è specchiata». Q/E cambia tipo, PagSù/PagGiù salta di 10, frecce o WASD guidano nelle 8 direzioni tenendo premuto per camminare, Spazio fa il giro automatico. Ci si arriva da F6 sulla scena o da F1 → NPC SYSTEM. Conteggio verificato a runtime: 96 voci contro 97 sheet su disco, e la differenza non è un buco — player_ghost_sheet vive fuori da NPCDatabase, lo carica direttamente Player.gd per lo spettatore post-collasso. NPC.gd e ModularNPC.gd non toccati: il visore monta un AnimatedSprite2D suo, così uno strumento di sviluppo non può rompere il gioco vero; il pulsante F1 è disabilitato in multiplayer, altrimenti il cambio scena smonterebbe la sessione agli altri peer. Lo zoom è stato alzato da 6 a 14 dopo aver guardato lo screenshot: a 6 il personaggio era troppo piccolo per giudicare i pixel, che è l'unico motivo per cui lo strumento esiste. Commit e8d85d3. - [docs] SU-188 — brainstorming sul ciclo di camminata, senza generare (quota Codex al 65%). Ivan ha bocciato il pilota — «il barbone verso sud ha sempre la gamba destra avanti» — e ha chiesto di ragionare invece di rigenerare. Il KO è stato confermato con una misura, e ha smontato la verifica precedente: nei due frame di contatto della riga SUD l'apertura dei piedi è larga uguale, 117 px in entrambi; non è un passo, è la stessa posa traslata (il centro scivola da 138 a 72). Ed è proprio lo scivolamento che aveva ingannato
verifica_camminata.py, che misurava *quanto* due colonne differiscono invece di *cosa* fanno le gambe: aveva dichiarato 15/15 OK guardando la cosa sbagliata. Provata anche una seconda metrica (asimmetria della massa gambe, che fra due contatti opposti dovrebbe invertire segno): valori dell'1-8% che non invertono quasi mai, né nei nuovi né negli originali. Conclusione: nessuna metrica a pixel semplice sa rispondere a «le gambe si alternano?», il giudice resta l'occhio — ed è la ragione per cui il visore di SU-47 non è un accessorio. Sulla proposta di Ivan («non bastano 3 pose invece di 5?»): sì, e si può fare senza toccare nulla della pipeline — i due «passaggio» del ciclo sono lo stesso disegno, quindi si chiede a Codex un foglio 5×4 (idle, contatto-D, passaggio, contatto-S) e la quinta colonna la duplica PIL. Il foglio finale resta 5×5, i 97 .tres non si toccano, e soprattutto il modo di sbagliare visto finora sparisce per costruzione: se si chiede una sola coppia di contatti, non la si può duplicare. Proposto anche un pezzo a costo Codex zero: per SUD e NORD il contatto sinistro si può costruire specchiando il destro, perché di fronte e di spalle lo specchio scambia davvero le gambe.
Giro KO: il cap a 60 fps era la cura peggiore del male, e gli sprite si generano grandi2026-07-27
- [fix] SU-186 (KO) — via il cap a 60 fps: era mia la regressione, ed è stata misurata. Ivan: «adesso mi dà l'impressione che giri a meno di 60 fps, tipo le animazioni quando arrivano le carte mi sembrano più basse». Aveva ragione, e il numero lo dice senza margini: stesso giro col bot, AUTO 115-121 fps (media 119, cioè il refresh ProMotion) contro esattamente 60,0 col cap — un rapporto 2:1, metà fotogrammi. Il perché si veda proprio sulle carte è nel codice:
LevelUpScreen le anima con un Tween a durata reale fissa (0,16s), quindi a metà framerate ha metà fotogrammi di interpolazione e sembra a scatti pur durando uguale. Correzione a quanto scritto ieri: window/vsync/vsync_mode=1 non serviva a niente, in Godot 4 è già il default dell'engine — scriverlo era un no-op, e l'unica riga con effetto reale era run/max_fps=60. Cioè ieri la fluidità è stata peggiorata senza toccare la causa del calore. Rimossa la riga: di default governa il V-Sync. Aggiunta la voce LIMITE FPS nelle Opzioni (AUTO / 30 / 60 / 90 / 120) col pattern di DIMENSIONE CONTROLLI, persistita in Settings.gd e applicata subito; le 9 righe della schermata sono state compattate come già fece SU-63, e verificate a schermo a 1024×768 e a 1280×720 prima di consegnare. Il ticket resta aperto nella sostanza: la causa vera del surriscaldamento non è stata trovata — nessuno smoking gun (popolazioni NPC e polizia già throttled con accumulatori, zero queue_redraw o Tween infiniti) — e qui si può misurare solo la CPU (~13 punti in più a 119 fps che a 60), non GPU né temperatura. Serve la prova di Ivan sulla build macOS: AUTO contro 60, e se il calore non cambia il colpevole è altrove. Nota pratica emersa: un editor Godot aperto sul progetto può reintrodurre il vecchio max_fps=60 dalla sua copia in memoria. project.godot, Settings.gd, MainMenu.gd. Commit bf5efe3. - [fix] SU-170 — in gioco entra finalmente la cabina approvata, perché il gioco caricava ancora quella bocciata. Ivan aveva approvato la v7 («ok approvato, vediamo in gioco»), ma
assets/sprites/world/bagno_pubblico_{sud,nord}.png erano byte per byte identici alla v6 — cioè proprio la versione che aveva respinto per mancanza di dettaglio. Senza accorgersene, «vediamo in gioco» gli avrebbe rimostrato la cabina brutta. Sostituiti coi PNG v7: pannelli verticali nervati, sportello con cornice, maniglia, indicatore libero/occupato, griglia di areazione sul retro. Nessuna modifica al codice: stesse dimensioni 21×45, stesso nome, stesso ancoraggio, quindi WorldGenerator e la meccanica di SU-181 non si accorgono del cambio. Provino in TMP/screenshots_claude/su_sprint13/su170_bagno_v7_in_gioco.png. Commit 67016f7. - [feat] SU-153 (KO) + SU-188 — gli sprite dei personaggi si generano GRANDI, e adesso camminano davvero. Due ticket lavorati insieme perché sono lo stesso lavoro tecnico: tenerli separati avrebbe fatto nascere le 8 skin nuove con la stessa camminata rotta che l'altro ticket doveva correggere, cioè un terzo giro garantito. Il KO di Ivan era sul formato: «qualsiasi sprite va generato con una risoluzione iniziale come sprite raw… genera tutti quelli che hai proposto più alta risoluzione sennò non si capisce». Aveva ragione a monte: le 8 skin erano state generate direttamente a 160×160, cioè nel formato di gioco, e a 32px per cella non c'è spazio per il dettaglio — da lì venivano i due rattoppi manuali del giro precedente (frangia magenta sui contorni, cappelli che sbordavano dalla cella). Rigenerate tutte e 8 a 1254×1254 su magenta, il formato raw vero, con dry-run della pipeline di import a confermare che riempiono il frame 32px come il barbone attuale — quindi gli overlay dei prop restano allineati. La camminata è stata sistemata nello stesso giro: 37 righe su 40 alternano le gambe. Sul pilota di SU-188 (barbone, poliziotto, turista) il miglioramento è da 11 righe buone su 15 a 15 su 15, e soprattutto l'identità dei personaggi è stata preservata — il criterio che poteva far fallire tutto — verificata affiancando gli idle prima/dopo. Difetti dichiarati e non nascosti:
m3_carrello ha un contorno nero spesso che le altre sette non hanno e resta con 1 riga rotta dopo aver esaurito i retry; f3_capelloni ha puntini chiari sul vestito; f2_pelliccia ha il passo poco leggibile perché il cappotto lungo copre le gambe. Asset in sprites_raw/PEOPLE/temp/skins_raw/ e temp/walk/, in attesa del «Fatto» di Ivan che vale come approvazione. - [test] Nuovo
tools/verifica_camminata.py: il difetto che ha trovato il bug diventa lo strumento che ne prova l'assenza. Confronta la colonna 2 con la colonna 4 di ogni riga di uno sprite raw e dice se sono troppo simili — cioè se il personaggio fa due volte lo stesso passo. Non confronta la tela grezza ma passa dal ritaglio del vero importer (tight_bbox/extract_and_resize): il personaggio "scivola" da una colonna all'altra e senza riallineare il confronto darebbe differenze finte. Le soglie sono euristiche dichiarate nel docstring, non un oracolo: il verdetto finale resta l'occhio di Ivan. Serve per i 93 archetipi che restano da rifare. Commit 012d55f.
Sprint 13: si spendono le lattine alla Baracca, la retata si può discutere in tribunale, e il barbone smette di costare 32 sprite a variante2026-07-27
- [feat] SU-81 — LA BARACCA: le lattine comprano vantaggi permanenti, mai contenuti. Nuova voce nel menu principale e nuova valuta ♻ in
MetaProgress (get_lattine/add_lattine/spend_lattine, dizionario shack persistito su user://meta.cfg accanto agli altri e incluso in reset_progress). Nove upgrade: FONDO CASSA ($25 in tasca all'avvio), COLAZIONE ABBONDANTE (fame ed energia oltre il 100), AMICO DEL BOTTEGAIO (−15% su tutto), INDECISO CRONICO (ripeschi il ventaglio), VETO DEL BARBONE (butti una carta e ne arriva un'altra), MICIO FEDELE (parti col gatto in braccio), VECCHIA VOLPE (un power-up di livello 1 a sorte), AVVOCATO DI STRADA (sblocca l'endless di SU-84), DOPPIA IDENTITÀ (secondo slot super). RIPESCA e VETO compaiono come bottoni sotto il ventaglio solo se comprati, con una carica a run ciascuno, e se la pesca è a vuoto la carica non si consuma. Acquisto a due tempi (tocchi = scegli, ritocchi = compri), come le carte del level-up: sul touch è l'unico modo per non comprare per sbaglio un upgrade da 250♻. Due upgrade su nove restano parziali e dichiarati: DOPPIA IDENTITÀ salva il flag e la capienza ma il secondo slot non è attivabile — servirebbe cooldown per slot, HUD a due caselle e un input nuovo, cioè il refactor di SuperPower che il ticket vieta — e AVVOCATO DI STRADA dipendeva dall'endless, arrivato con SU-84 nello stesso sprint. Una riga fuori perimetro, dichiarata: Interactable._tier_price() moltiplica per MetaProgress.shop_price_mult(), perché era l'unico punto prezzi del gioco e senza quella riga AMICO DEL BOTTEGAIO sarebbe stato un upgrade da 70♻ che non fa nulla. I costi in lattine sono una proposta da confermare: il brief R3 non era accessibile alla lavorazione, e i due valori fissati dal ticket (VETO 90♻, DOPPIA IDENTITÀ 250♻) sono stati usati come scala per gli altri sette; si tarano tutti in SHACK_UPGRADES. Debug F1: regala lattine, compra tutto, azzera. MetaProgress.gd, MainMenu.gd (additivo), GameState.gd, LevelUpScreen.gd, SuperPower.gd, DebugPanel.gd, Interactable.gd. Commit 03e6edf. - [feat] SU-84 — ENDLESS: con l'Avvocato di Strada, al minuto 30 la retata non chiude più la run. Implementato come quarta fase di
raid_phase (RAID_PHASE_ENDLESS = 3) e non come sistema parallelo: così net_sync(host_elapsed, host_phase) conserva la firma e i client attraversano le fasi 1→2→3 nell'ordine, senza un secondo canale da tenere allineato. Ondate CACCIA/TREGUA alternate, tutte tarabili in costanti in cima a RunManager.gd: 30s di caccia e 45s di tregua la prima, poi ×1,10 la caccia e ×0,90 la tregua, con pavimento 15s sulla tregua e tetto 180s sulla caccia; agenti in scena all'ondata N = min(30, 4 + (N−1)×2), portati fino a quel numero e non sommati, così la cosa resta idempotente rispetto allo spawn già esistente. La run finisce solo quando ti prendono, riusando on_local_captured(). Il multiplayer è stato implementato per intero senza toccare NetworkManager.gd, che era in mano a un altro lotto nello stesso sprint: i client dichiarano il proprio sblocco con World._srv_endless_unlock, l'host ricalcola a 1 Hz, e le sotto-fasi viaggiano in due interi — nessun problema col tetto di 1170 byte di EOS. Endless solo se tutti i giocatori hanno l'upgrade, come da design. Record endless separato e volutamente escluso da get_best_overall(), per non inquinare punteggi già salvati con run che per costruzione durano di più; ScoreSystem.register_raid_survival() ora tiene il massimo invece di sommare, perché in endless veniva chiamata più volte e avrebbe contato due volte gli stessi secondi. Un criterio del ticket è stato disatteso di proposito: il «paracadute carta dorata sempre attivo durante le ondate» non è stato reimplementato, perché quel paracadute è stato rimosso alla radice in SU-180 su richiesta esplicita di Ivan («solo la progressione e il giullare danno la super») — SU-84 è più vecchio di quella decisione, e reintrodurlo l'avrebbe annullata in silenzio. Da confermare: is_raid_active() resta vero anche in tregua, quindi NOVE VITE non salva mai in endless. Debug F1: «Endless ora», «Ondata +1», «Ondata +5». RunManager.gd, World.gd, MetaProgress.gd, ScoreSystem.gd, HUD.gd, DebugPanel.gd. Commit 0b407fe. - [change] SU-154 — il barbone passa a livelli sovrapposti: una skin nuova costa 1 sheet invece di 4. Il gatto in braccio non era un overlay, era disegnato dentro lo sheet (
player_cat_sheet), come ubriaco e fantasma: con le 8 skin di SU-153 il conto sarebbe stato 8 × 4 = 32 sheet, e ogni prop futuro (la lanterna, il kazoo) ne avrebbe aggiunti altri 8 — crescita moltiplicativa. Ora tre livelli: CORPO uno sheet per skin, PROP un overlay condiviso da tutte le skin, STATO solo tinta/alpha/barcollio. Crescita additiva. Il motore esisteva già nel progetto e non era mai stato acceso: ModularNPC._add_accessory_overlay(), con la cartella accessori vuota. L'overlay del gatto è stato RICAVATO, non generato: tools/extract_prop_overlay.py diffa lo sheet col gatto contro quello base: il diff nudo è rumore, perché i due sheet sono ridisegni indipendenti (11.693 pixel diversi su 25.600), quindi un filtro a cascata — soglia percettiva, anti sale-e-pepe, dilatazione di 1px, scarto delle macchie sotto i 30px per cella — isola i 4.423 pixel che sono davvero il gatto. Estratto l'helper condiviso scripts/util/SpriteOverlay.gd, e nel farlo sono emersi due bug latenti degli accessori NPC, mai visti perché quella cartella è vuota: la cella dell'overlay veniva *indovinata* con row*5 + frame invece di leggerla dalla region dell'AtlasTexture — e le walk_* partono dalla colonna 1, quindi sbagliava di una casella — e mancava l'aggancio a animation_changed, senza il quale fra due idle da un frame solo (idle_s→idle_e) l'overlay restava fermo. Il busking resta un caso speciale, con la motivazione scritta nel codice: _music_sprite non è un overlay ma un *corpo alternativo* (strip 128×32, senza direzioni) che sostituisce lo sprite, e trasformarlo in prop vorrebbe dire ridisegnare la posa «suona» sulla griglia 5×5 di ogni skin — cioè spostare il problema dei 32 sheet di un metro invece di risolverlo. In MP la skin sta nel registro peer accanto a nome e colore (NetworkManager.skin_of), dichiarata col pattern esistente e applicata una volta sola dai puppet: non in _net_state, che è per-frame e non affidabile. Rete di sicurezza: se il PNG del prop manca si torna allo sheet dedicato senza errori, quindi cancellare prop_cat.png riporta istantaneamente al comportamento vecchio. Player.gd, ModularNPC.gd, NetworkManager.gd, nuovi SpriteOverlay.gd, extract_prop_overlay.py, assets/sprites/player_props/prop_cat.png. Commit da174c9. - [fix] SU-187 — il casco della rider era bucato, ma non per il motivo scritto nel ticket. Il ticket incolpava la pulizia delle sacche magenta chiuse; la causa vera è il flood-fill permissivo: la banda fucsia del casco è cromaticamente troppo vicina al magenta di ritaglio e confina col contorno sottile verso lo sfondo, quindi il riempimento a soglia larga ci entrava dentro e la mangiava. Ricostruito il ritaglio con il solo flood-fill stretto dai bordi, senza passate permissive — cioè esattamente il «ritaglio perimetrale originale» che chiedeva Ivan: buchi interni da 17 a 3, zero residuo magenta, e vale solo per lei, le altre rider non sono state toccate. Nuovo
tools/ritaglio_perimetrale.py, riusabile sugli altri casi dello stesso tipo. arch_delivery_female_v01_sheet.png. Commit 091591e. - [fix] SU-47 — quattordici personaggi camminavano di profilo all'indietro. Audit riga per riga di tutti i 96 raw di
sprites_raw/PEOPLE/, fatto per provini di contatto e non file per file: tools/provino_direzioni.py estrae la stessa riga da tutti i raw e la impagina in un'unica griglia, così il verso si giudica a colpo d'occhio su 3 immagini invece che su 96. Trovati 14 raw con la riga 3 (EST) disegnata verso ovest — il barbone giocante, tutte e sei le nonne, tre catlady, tre office_zombie e il turista maiale ubriaco citato dal ticket — corretti col flip orizzontale della sola riga incriminata e re-import: nessuna rigenerazione, come imponeva il ticket. Gli altri 82 sono stati verificati e sono in convenzione. Trovato anche un bug sistemico che NON è stato corretto: su tutti e cinque i campioni controllati (player, police_male, delivery, tourist, junkie) il frame walk_3 duplica walk_1 invece di alternare la gamba — è la «camminata zoppa» del ticket, riguarda l'intero roster e non è correggibile in modo deterministico, quindi serve un ticket di generazione a parte. Censimento completo in TMP/su47/CENSIMENTO.md. Nuovi tools/provino_direzioni.py e tools/flip_riga_raw.py. Nota sui raw: sono stati committati benché stiano in git LFS, perché erano già tracciati e lasciarli fuori renderebbe il repo incoerente — sheet corretti e sorgenti no — col primo re-import che rimetterebbe le righe girate. Commit 091591e. - [test] Collaudo di fine sprint: PASS su tutti e cinque i ticket di codice, nessuna regressione. Compile-check globale 87 script / 35 scene, 0 falliti. 2 run SP col bot su seed e quartieri diversi: 0 SCRIPT ERROR,
orphans=0, conteggi NPC in fascia. Verificate dal vivo — non dedotte — le tre zone che questo sprint rendeva rischiose: meta.cfg è stato toccato da due lotti (baracca+lattine e record endless), quindi lattine, record, sblocchi e acquisti sono stati riletti da un secondo processo con la stessa HOME, e i contatori preesistenti sono tutti sopravvissuti; HUD.gd è stato toccato da due lotti (guardie anti-riscrittura e indicatore ondata), quindi orologio e countdown sono stati seguiti nel tempo per escludere la label congelata, che è il modo tipico in cui un'ottimizzazione «scrivi solo se cambiato» si rompe; l'overlay del gatto è stato guardato nelle quattro direzioni cardinali, sincrono su frame e flip. La controprova più importante è passata in entrambi i sensi: senza AVVOCATO DI STRADA la retata del 30' chiude la run come sempre (raid_phase=ACTIVE), con l'upgrade parte l'endless (raid_phase=ENDLESS, banner e HUD «♾ ONDATA 1» corretti). Screenshot in TMP/screenshots_claude/su_sprint13/, dettagli in TESTLOG.md. Limiti dichiarati: il multiplayer non è collaudabile in questa sandbox (hang noto di host_game()), quindi la replica MP di endless e skin resta da provare su device; il fallback «PNG del prop assente» è stato verificato a lettura di codice e non dal vivo, perché la cache di import di Godot maschera il file rinominato. - [fix] Un difetto trovato dal collaudo, diagnosticato e NON risolto: a 1280×720 due righe della Baracca perdono la prima fila di pixel. Sullo screenshot a 720p «AVVOCATO DI STRADA» si leggeva «HVVOCHTO DI STRHDH»: non è testo sbagliato, è la riga decapitata in alto — senza la punta la A resta due montanti e una traversa (una H), la T perde la traversa, la O diventa U, la S diventa 5. Il blocco di testo misura 11px invece di 12. Tre tentativi di correzione geometrica sono stati fatti e buttati: arrotondare le Y al pixel intero, poi rendere pari il passo di riga — ogni volta il difetto si spostava su un'altra coppia di righe senza sparire, perché il passo reso a schermo è ~33,5px contro i 32 del codice, cioè fra pixel logici e pixel schermo c'è una scala di canvas non intera e con quel fattore nessuna spaziatura salva tutte e nove le righe.
MainMenu.gd è quindi rimasto identico a com'era: una correzione che sposta il difetto non è una correzione. Perimetro accertato: alla risoluzione base del progetto (1024×768) la schermata è perfetta, tutte e 9 le righe leggibili, e il menu principale a 720p è pulito, nuova voce LA BARACCA compresa — quindi non è una regressione delle schermate esistenti, si vede solo dove le righe sono nove e fitte a font piccolo. Screenshot a confronto in TMP/screenshots_claude/su_sprint13/baracca/. Decide Ivan: la strada pulita è alzare font e passo delle righe, oppure agganciare il menu a una scala intera — materia dello sprint U1, che è il proprietario di MainMenu.gd. - [fix] SU-186 — la ventola del Mac girava per colpa di due righe che mancavano e di un HUD che si riscriveva sessanta volte al secondo. La causa è risultata strutturale e leggibile nella configurazione, senza bisogno di misurare: in
project.godot mancavano sia display/window/vsync/vsync_mode sia application/run/max_fps, quindi un top-down 2D pixel art a 1024×768 poteva girare a 120 fps inutili su uno schermo ad alto refresh. Aggiunti V-Sync e cap a 60. In più, HUD._process riscriveva Label.text e add_theme_color_override a ogni singolo frame anche a valore invariato per orologio, giorno e countdown della retata — l'orologio cambia una volta al minuto di gioco, il countdown una volta al secondo — forzando layout e redraw a vuoto: ora si scrivono solo quando il valore cambia davvero. Le particelle sono risultate innocenti e non sono state toccate: sono tutte CPUParticles2D già spente a riposo, e non c'è un solo PointLight2D negli script. low_processor_usage_mode è stato valutato e scartato: è pensato per app non interattive e in un gioco introduce scatti. Il limite FPS è persistito in Settings.gd (30/60/90/120/illimitato) ma non ancora agganciato alle Opzioni, perché MainMenu.gd era in mano a un altro lotto nello stesso sprint: resta un aggancio da fare. Da riprovare da Ivan sulla build macOS, che è dove il difetto è stato osservato. project.godot, HUD.gd, Settings.gd. Commit 41bd7f2.
L'export delle build si lancia da riga di comando, e non può più uscire senza note di rilascio2026-07-27
- [feat] Nuovo
tools/release/export_all.sh: il passo 6 della release smette di essere manuale, e si porta dietro la guardia che mancava. Nato da un difetto trovato durante la release v0.25: lo zip Windows stava per essere spedito ai tester senza assets/release_notes_it.txt e senza eos_credentials.cfg, cioè con la voce NOTE DI RILASCIO vuota e l'online morto. La causa non era l'export ma la configurazione: nel preset Windows Desktop include_filter era vuoto, mentre iOS e macOS avevano il valore giusto — e export_presets.cfg è gitignored, quindi il fix SU-57 vive solo in locale e nessuno se ne accorge quando si perde. Non è una novità della v0.25: il pck Windows non conteneva neanche il testo della v0.24, quindi i tester Windows non hanno mai visto le note in-game. Lo script fa tre cose in fila, e le prime due non sono ridondanti: pre-volo sull'include_filter di ogni preset che sta per esportare (guarda la configurazione, fallisce prima di sprecare un export e stampa il valore da incollare), export headless nel percorso già scritto nel preset — nessun percorso da ricordare a mano — e post-volo che riapre il .pck prodotto e verifica che i due file ci siano davvero (guarda il risultato: se un domani l'export ignorasse il filtro, il pre-volo direbbe ok e solo questo se ne accorgerebbe). Con --pacchetto incatena make_dmg.sh e make_winzip.sh, che creano dmg/zip e pubblicano su iCloud e Google Drive; con --controlla fa solo il pre-volo e esce, per verificare i preset in un secondo senza esportare nulla. Interfaccia: export_all.sh [mac|win|ios|all] [--pacchetto] [--controlla], default mac win, binario Godot sovrascrivibile con $GODOT. Entrambe le guardie sono state collaudate facendole fallire, non solo passare: preset sabotato a mano → pre-volo KO col messaggio giusto e file ripristinato; pck rotto della v0.25 dato in pasto al post-volo → «BUILD INCOMPLETA» con l'elenco dei file mancanti; pck inesistente → avviso e nessun crash. Poi il giro vero: Windows ri-esportato, post-volo verde e verifica indipendente sul pck (testo v0.25 presente, eos_credentials presente), zip da 96 MB pubblicato su entrambi i cloud. Nota di sicurezza: il permesso aggiunto in .claude/settings.local.json è Bash(bash tools/release/export_all.sh:*), cioè autorizza solo questo script e non Godot in generale — i flag giusti stanno dentro lo script. tools/release/export_all.sh (nuovo), WORKFLOW/04_RELEASE.md (passo 6).