Le panchine restano senza ombra, per ora2026-08-04
- [change] Ombre delle panchine sospese su richiesta di Ivan: «fanno pena, non sono allineate al resto, le rimettiamo nella prossima versione». Sono quelle aggiunte in SU-198 (KO 01/08 n.2, «le panchine non hanno ombra»): la proiezione c'era e seguiva il sole come le altre, ma sotto un arredo basso da 28 px usciva una macchia che non legava col resto della resa. 📌 La rimozione è chirurgica e sta in un punto solo: in
_add_interactable di WorldGenerator.gd l'ombra degli arredi nasceva da una soglia di altezza (OMBRA_ARREDO_ALT_MIN, 24 px) valida per tutti gli interactable con sprite — abbassare o alzare la soglia avrebbe portato via anche altro, quindi si esclude il solo tipo BENCH e la regola generale resta intatta. Conseguenza voluta: i camioncini, quando cadono sullo sprite statico di ripiego, tengono la loro ombra; il cestino resta fuori da solo com'era prima (16 px, sotto soglia). Niente è stato tolto a palazzi, vineria, negozio di vestiti, bagni pubblici, lampioni e camioncini animati, che passano da strade diverse (_spawn_ombra_palazzo, _ombra_camioncino, _spawn_ombra_generica chiamata a mano). ⚠️ È dichiaratamente TEMPORANEA, e il commento nel codice lo dice con la data: si rimette quando la proiezione degli arredi bassi sarà rifatta in modo che leghi col resto. Guardato, non dedotto: rilanciato shot_su198_arredi.gd (lo stesso provino che a suo tempo aveva dimostrato l'assenza delle ombre) alle 8 del mattino, quando l'ombra è lunga — le due panchine inquadrate a zoom 5× sono pulite, e il censimento lo conferma dal lato dei dati: la panchina più vicina ha l'ombra più prossima a 152 px di distanza, che è quella di un lampione (alt 46, semilarghezza 5), non più a distanza zero. Nello stesso giro di scatti lampioni, vineria (dist 4), negozio di vestiti (dist 8), hot dog e gelati (dist 28) hanno ancora la loro. Compile-check headless su WorldGenerator.gd: OK. ⚠️ Non toccato il tutorial: TutorialWorld.gd ha una panchina ma non ha mai generato ombre di nessun tipo, quindi lì non cambia nulla e non c'era niente da allineare.
Sprint, sesto giro2026-08-04
RIENTRI ATTESI: SU-83, SU-188, SU-276, SU-277, SU-281, SU-282 — le sei 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 ne sono rientrate tre — SU-83, SU-188, SU-276 — ma solo due sono KO veri: SU-188 è tornato indietro perché Ivan approvava e chiedeva l'import, non perché fosse sbagliato. Tasso 2 su 15, il migliore mai registrato contro 3-4/15, 2/7, 6/7, 2/11 e 1/6 dei giri precedenti.)* 📌 Cosa dicono i due KO veri, che è più interessante del numero: nessuno dei due era codice rotto. SU-83 era arte giusta, file sbagliato — Ivan non aveva scritto quale delle quattro versioni della barbona voleva, e il giro precedente ne aveva scelta una misurandola (era la più allineata): la misura era corretta e la scelta comunque sbagliata, perché la domanda non era «quale si allinea meglio» ma «quale ti piace». SU-276 era soggetto sbagliato a dimensione giusta: giocoliere e jolly erano disegnati come scene invece che come oggetti singoli, e a 16 px collassavano. ⚠️ Il buco dichiarato di questo giro: il multiplayer non è stato collaudato (non è collaudabile su questo Mac), ma nessuna delle sei consegne aggiunge una riga di rete — sono arte, navigazione dei menu e un font. E il web export non è provabile qui, il che pesa proprio su SU-277, dove il font delle icone dovrebbe dare il guadagno maggiore. ⚠️ Due giudizi che aspettano Ivan e non una correzione: le riserve di leggibilità a 16 px delle icone (gatto_imbronciato contro muso_gatto) e la nitidezza a 0,8 em.
- [feat] SU-277 — le emoji di sistema sono sparite dal gioco, e i CSV non sono stati toccati. Il ticket dichiarava aperto il punto che decideva la sua fattibilità: molte di quelle stringhe finiscono in Label normali, che le immagini non le mostrano — quindi o si convertiva mezzo gioco a
RichTextLabel con [img], o si inventava un segnaposto risolto a runtime. 📌 È stata presa una terza strada, e ha cambiato la dimensione del ticket: un font di icone a colori (bitmap CBDT, lo stesso formato dei font emoji) mappato sugli stessi codepoint delle emoji e agganciato come primo ripiego della FontVariation del tema, davanti al CJK. Conseguenza: nessuna cella dei 7 CSV modificata in nessuna delle 8 lingue, nessuna Label diventata RichTextLabel, e i due criteri più insidiosi — allineamento alla linea di base e scala col corpo del testo — li risolve il motore di testo invece del codice. Le 71 icone sono quelle approvate in SU-276; il font lo rigenera tools/build_icon_font.py, che riusa il ritaglio già scritto per il foglio di contatto e riduce con BOX. 📌 L'inciampo che vale ricordare: con glyf di lunghezza zero FreeType smette di considerare il font scalabile e accetta solo le dimensioni esatte delle incisioni (invalid pixel size) — è lo stesso difetto noto di NotoColorEmoji, risolto con un contorno degenere in .notdef. E Pillow dà quell'errore anche su Apple Color Emoji: un fallimento lì non dice niente su Godot. ⚠️ L'audit è stato esteso, ed è la parte che rende la verifica onesta: audit_emoji_pittogrammi.py escludeva i codepoint di StreetU-Pixel.ttf, quindi con le icone in un font NUOVO avrebbe continuato a contarle e non sarebbe mai arrivato a zero. Ora considera anche il font delle icone ed esce con codice 1 se resta un pittogramma scoperto — cioè proprio il caso in cui un testo tornerebbe all'emoji di sistema in silenzio. Provato nei due versi: col font ESITO: OK (exit 0), nascondendolo KO — 71 (exit 1). Rilanciato in proprio in chiusura: zero pittogrammi scoperti su 195 .gd, 46 .tscn e 7 CSV. Guardato prima/dopo su HUD, cerimonia del super, fine run, quaderno e carte, in italiano e in cinese — dove icona pixel e ripiego CJK convivono senza tofu e senza muovere l'interlinea. Divieto messo a regola in 00_CONTESTO_PROGETTO.md (convenzione 7, col motivo e la procedura per aggiungerne una nuova). ⚠️ Tre cose dichiarate: il pacchetto cresce di 449 KB (il font CJK già imbarcato ne pesa 3.900); il web export non è provabile su questo Mac, ed è proprio lì che il guadagno sarebbe maggiore perché le emoji di sistema spesso mancano; a corpo 16 l'icona esce a 12,8 px da un master di 16 — leggibile ma un filo morbida, è il prezzo degli 0,8 em scelti per non alzare la riga, e se si vogliono più nitide si passa a 1,0 em accettando un'interlinea più alta. - [fix] SU-281 + SU-282 — con pad e tastiera si esce anche da SCEGLI IL PERSONAGGIO e dalla METROPOLITANA, e i crediti non parlano più di musiche. SU-281: le due schermate avevano «← INDIETRO» raggiungibile solo col dito o col mouse — su/giù non erano proprio previsti, e chi gioca col pad virtuale su telefono non ha né ESC né B, quindi l'unica via d'uscita era centrare la scritta. Ora GIÙ porta il fuoco su INDIETRO e SU lo riporta sulla voce sopra, con INVIO/A che attiva quella che ce l'ha. 📌 Non è stato inventato un terzo modo di navigare: è la stessa grammatica delle schermate online, ridotta a un interruttore a due posizioni, e il pezzo è fattorizzato una volta sola (
_move_binary_focus, _style_indietro_focus, _focus_row_prefix) e usato da entrambe — due schermate, una sola implementazione, come chiedeva il ticket. Col fuoco su INDIETRO le frecce ←/→ non cambiano più personaggio o fermata di nascosto, e il tap riporta il fuoco sulla riga di sopra per non lasciare stati visivi incoerenti. ⚠️ Corretto in chiusura uno scostamento dalla convenzione: la prima versione scriveva «▶ ← INDIETRO» senza riempimento, e la scritta ballava di lato a ogni pressione — le altre sei schermate del menu usano da sempre la chiave MENU_INDIETRO_FMT con tre spazi al posto del marcatore, e adesso la usa anche questa (stesso giallo di selezione, stesso grigio a riposo). Guardato su entrambe le schermate, fuoco per fuoco. SU-282: nei crediti il blocco audio diceva «SUONI E MUSICHE → ElevenLabs», ma le musiche non sono sue: ora dice SUONI, in tutte e 8 le lingue del CSV — una passata sul solo italiano avrebbe lasciato la parola «musiche» nelle altre sette. ElevenLabs resta accreditata per i suoni, il resto dei crediti non è stato toccato. Guardato in italiano e in cinese (音效 / ElevenLabs, nessun quadratino). ⚠️ Il credito delle musiche resta un buco dichiarato: «per ora» sono parole di Ivan, si riapre quando si deciderà da dove vengono. - [feat] SU-276 (KO) — il set delle icone è completo: 71 pittogrammi, e i due bocciati rifatti isolando un oggetto solo. Il KO di Ivan era preciso e diceva anche il perché senza dirlo: «giocoliere metti solo una pallina, jolly solo il cappello da giullare». 📌 La diagnosi prima del rimedio: i prompt del primo giro chiedevano la *scena* — «un giocoliere che fa giocoleria», «una carta con sopra il jolly» — cioè tre o quattro elementi che a 16 px collassano in una macchia. Non era un problema di resa ma di soggetto: rifatti isolando un oggetto unico, e il difetto sparisce da sé. Generate poi le 43 restanti dell'audit, in 8 fogli multi-icona invece che 43 chiamate singole, per tenere lo stile coerente e non bruciare quota. Foglio di contatto rigenerato con 71 riquadri (26 già approvate + 2 rifatte + 43 nuove), ciascuno con la miniatura a 16 px vera di fianco, che è l'unica dimensione che conta. ⚠️ Le riserve di leggibilità sono scritte, non nascoste, perché è esattamente il giudizio che ha generato questo KO: rischio alto su
mani_giunte, capanna, folata_vento, water e fiocco_neve (bianco su sfondo chiaro), e soprattutto gatto_imbronciato che a 16 px è quasi indistinguibile da muso_gatto — stesso muso arancione, cambia solo il sopracciglio. ⚠️ Sette icone sono state generate ma non serviranno: fiocco_neve, fiamma, mattone, simbolo_riciclo, freccia_invio, spunta_bianca e segno_x compaiono solo in DebugPanel.gd e negli script di provino, cioè non le legge mai il giocatore — il divieto riguarda ciò che sta a schermo in partita, quindi l'import le salterà. Generate lo stesso invece di scartarle a fiuto: la regola del ticket era meccanica (audit meno consegnate). Nessun file di gioco toccato: le icone vivono in sprites_raw/temp/icon/ (fuori da git, come tutti i raw) e la consegna è il foglio in TMP/. - [fix] SU-83 (KO) + SU-188 — la barbona giusta e il giullare che cammina davvero, senza toccare una riga di codice. Due consegne di sola arte, tenute insieme perché sono lo stesso mestiere: portare in gioco un raw 1254×1254 già approvato senza spostare un pixel delle ancore condivise. SU-83: il KO diceva «perfetta l'implementazione ma la barbona era un'altra» e indicava
su153_arch_player_female_v4.png — quindi zero codice, solo sprite. Riusata la stessa identica pipeline che aveva prodotto la skin precedente (verificata byte-identica al file in gioco prima di partire: è quella che garantisce l'allineamento), non import_people_sprites.py, che ri-rileva le isole e ri-centra le celle introducendo proprio gli scostamenti per-skin che il vincolo di Skins.gd vieta. 📌 La misura che decide il ticket, rifatta in proprio e non presa dal report: bbox opaco cella per cella contro il barbone base, deviazione massima 1 px, zero celle fuori soglia su 25 — quindi gatto in braccio e lanterna, che sono overlay unici condivisi, restano attaccati dove devono. Guardato, non dedotto: la barbona nel menu è quella del v4 (berretto, capelli lunghi, cappotto verde oliva, tracolla) ed è la stessa che cammina in città. «Togli l'altra» non ha prodotto cancellazioni perché il file di gioco si chiamava già player_skin_f1_*: è stato sovrascritto il contenuto, e un grep conferma zero riferimenti residui alla candidata precedente. SU-188: importato il foglio del giullare che Ivan ha approvato («OK fatto importa questo ultimo sprite»). ⚠️ Qui il report dell'agente e lo strumento non dicevano la stessa cosa, ed è il motivo per cui il diff si guarda: il report diceva «5/5 righe OK», verifica_camminata.py rilanciato dice «nessuna riga rotta» ma 3 righe sospette (nord, est, nord-est). Guardate a ×6 cella per cella: le quattro pose sono distinte, nessun fotogramma duplicato — l'alternanza è solo poco appariscente di spalle e in diagonale, dove la gamba copre pochi pixel. Scala invariata (bbox medio 17,44 px contro 17,72 del vecchio foglio), zero residui di magenta. ⚠️ Un rischio dichiarato, che è una domanda per Ivan: mirrorfix3.png non è stato promosso a sorgente canonica in sprites_raw/PEOPLE/, che resta alla versione col nord-est rotto — se un domani si rilancia l'import in batch per un altro archetipo, il giullare torna indietro da solo. 📌 Trappola d'ambiente trovata e da ricordare: check_files.sh salta il reimport quando trova il marker .godot/.su_imported, quindi chi sostituisce un PNG su un checkout «caldo» continua a vedere la texture vecchia dalla cache — i primi provini mostravano ancora la skin precedente, e sono stati rifatti dopo un --import forzato.
Sprint, quinto giro2026-08-04
RIENTRI ATTESI: SU-83, SU-188, SU-255, SU-257, SU-259, SU-266, SU-270, SU-271, SU-272, SU-273, SU-274, SU-275, SU-276, SU-278, SU-279 — le quindici 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 ne sono rientrate quattro — SU-188, SU-255, SU-257, SU-268 — ma solo tre sono KO veri: SU-268 è tornato indietro perché Ivan rispondeva a una nostra domanda, non perché fosse sbagliato. Tasso 3-4 su 15, il migliore mai registrato contro 2/7, 6/7, 2/11 e 1/6 dei giri precedenti.)* ⚠️ Il buco dichiarato la volta scorsa non si è avverato, e vale la pena dirlo: «il multiplayer non collaudato è il candidato numero uno a rientrare» — e invece SU-256, SU-257 e SU-260 non sono rientrati per motivi di rete. SU-257 è tornato per un requisito nuovo (il rallentamento da scoreggia), non per un difetto MP. La previsione era ragionevole ed è stata smentita: il MP non collaudato resta un rischio dichiarato, ma finora non è la causa dei rientri. ⚠️ Come leggere il numero questa volta: delle quindici chiavi, due sono giudizi estetici di Ivan e non codice provato (SU-188 il giullare, SU-276 le icone: lì «Fatto» significa «approvo»), e una è una proposta di design (SU-83, il costo di sblocco in lattine) che può cambiare senza che nulla sia rotto. ⚠️ Il buco vero di questo giro, dichiarato: il multiplayer non è stato collaudato neanche stavolta (non è collaudabile su questo Mac, conflitto EOS/ENet noto dal 31/07) e tre consegne hanno percorsi di rete — SU-257/SU-278 (che però non aggiungono un solo RPC: si agganciano a un segnale già esistente, quindi il rischio è basso), SU-259 (relay nuovo, copiato riga per riga da quello della bottiglia) e SU-83 (la skin non viaggia in rete, ed è dichiarato: in MP gli altri ti vedono col barbone base).
- [feat] SU-83 — si sceglie chi essere prima di scendere in strada. Fermo da undici giri in attesa che le skin esistessero e fossero approvate: SU-153 e SU-200 sono passati a Fatto e Ivan ha riscritto il requisito. Nuova schermata di selezione, infilata prima di tutta la catena esistente (
_on_start_pressed → domanda tutorial → metropolitana), con «Barbone 1» sbloccato e «Barbona 1» bloccata: silhouette nera, punto interrogativo giallo pixelloso e «???» al posto del nome, come nei giochi classici. 📌 Non è stato usato import_people_sprites.py, ed è la scelta tecnica che salva il ticket: quello ri-rileva le isole e ri-centra le celle, cioè introdurrebbe proprio gli scostamenti per-skin che il ticket vieta — e con quelli il gatto in braccio e la lanterna, che sono overlay unici condivisi, fluttuerebbero. Le skin di SU-153 sono già nel formato finale: copia diretta. 📌 La skin donna non è stata scelta a gusto ma misurata: bbox opaco cella per cella contro il barbone base — f1_turbante devia ≤1 px su tutti i bordi in tutte e 25 le celle, le altre candidate arrivano a 4-6 px sui piedi. È l'unica che rispetta le ancore condivise senza una riga di offset. Proposta di sblocco (è una proposta, sta in un numero di Skins.SKINS): 150 LATTINE, comprabili dalla schermata — un personaggio è puro vestito e non tocca il bilanciamento, quindi legarlo a un'impresa lo metterebbe in fila con quartieri e carte e lo renderebbe improvabile senza una run intera; 150 sta sopra quasi tutta la Baracca e sotto ENDLESS (200) e DOPPIA IDENTITÀ (250). Comando in F1 per sbloccare tutto. Verificato a video, non solo a codice: la barbona scelta nel menu è la barbona che cammina in città. ⚠️ Tre limiti dichiarati: la barbona non ha le varianti «gatto in braccio» e «sbornia» (con lei quelle trasformazioni non avvengono — scelta deliberata, ma servono due ticket di generazione); la skin non viaggia in rete; touch e joypad non provati con input reale. - [fix] SU-270 + SU-271 — una sola regola di profondità: ci si ordina su dove si tocca terra. I due difetti che Ivan ha segnalato (il palo che taglia a metà gatti e bidoni; il personaggio che passa dietro al lampione coi piedi ancora davanti) avevano la stessa causa, e non era in nessuno dei due posti dove si guardava. Il mondo non usa lo Y-sort di Godot, ordina a mano con
z_index, e ogni categoria di oggetti aveva scelto per conto suo su quale punto ordinarsi: i palazzi sulla base, i camioncini sul top dello sprite, gli interactable sul centro del nodo, i personaggi sull'origine (cioè a metà busto), il lampione sul piede del palo. Non era un bug di un file: era l'assenza di una convenzione. Ora c'è, scritta in un blocco «REGOLA DI PROFONDITÀ DEL MONDO» in testa a WorldGenerator.gd, con l'elenco dei punti che la applicano: (1) ci si ordina sulla Y del punto in cui si tocca il suolo — per i personaggi sono i piedi, con l'offset misurato dallo sprite (offset_piedi(), non una costante: ModularNPC cambia frames a caldo), +8,0 px sotto l'origine su tutte le scene personaggio; (2) i pali non occludono gli arredi raccoglibili, quindi bidone e gatto attraversati dal rettangolo di un palo salgono a z(palo)+1. Sono due regole e non una perché z_index è un ordine totale: «arredo sempre sopra al palo» e «personaggio ordinato sui piedi anche contro l'arredo» non possono valere entrambe al 100%, e il compromesso sta scritto nel commento invece di restare implicito. 📌 La causa vera del gatto era un'altra ancora: CatPickup.tscn non aveva alcuno z_index e restava a 0, cioè sotto a tutta la città — guardando il codice del lampione non si sarebbe mai trovato, perché mancava una quota, non ce n'era una sbagliata. Per questo la correzione è finita in Interactable.gd, fuori dal perimetro del lotto e unico posto dove quella quota può stare. Guardato (non solo misurato): il ribaltamento cade esattamente sui piedi — 3px a sud della base del palo e il personaggio sta davanti, 3px a nord e il palo gli passa davanti, per barbone, NPC civili, poliziotto e Squadra Sgombero; nessuna regressione su facciate, camioncini e pensilina. Misurato: audit su 24 città (3 quartieri × 8 semi), 354 bidoni, 111 incroci reali bidone-palo, zero sbagliati. ⚠️ Non provato il multiplayer: i puppet usano le stesse scene, quindi la regola li segue, ma è verificato leggendo il codice. ⚠️ I camioncini restano ordinati sul top dello sprite e la loro soglia si sposta di 8px a nord: coerente a video, ma se si vuole l'appoggio a terra anche per loro è un ticket a parte. - [feat] SU-257 (KO) + SU-278 + SU-259 — la nuvola rallenta tutti, e lo spazzino tira il sacchetto. Il KO di Ivan chiedeva che la scoreggia rallentasse anche il barbone rivale e la ronda di bulli notturna, «come la polizia». La diagnosi ha trovato che il canale giusto c'era già:
Player._do_fart() emette player_misdemeanor, ogni poliziotto è iscritto e si auto-rallenta, e in MP World._srv_misdemeanor riemette il segnale sull'host, dove gli NPC vivono davvero — rivale e ronda semplicemente non erano iscritti a nessuno dei due canali. Agganciarli al segnale invece che alla rete fa arrivare single player e multiplayer host-authoritative insieme, senza un solo RPC nuovo; scartato un net_fart_slow_npc dedicato, che avrebbe aggiunto rete per niente. 📌 I numeri erano copiati in tre posti (PoliceOfficer, Player, World): il ticket chiedeva di non aggiungerne una quarta, e ora ce n'è una sola, in scripts/npc/fart_slow.gd, con gli altri file che tengono solo un alias — toccare un numero lì lo tocca per polizia, barboni MP, rivale e ronda insieme, che è giusto perché la nuvola è una sola. ⚠️ Un dettaglio che sarebbe stato un difetto invisibile: il fattore moltiplica ogni riassegnazione di walk_speed invece di essere un post-moltiplicatore — il rivale ha un ramo di allarme che non riassegna la velocità, e in due frame consecutivi l'effetto si sarebbe azzerato. SU-259: lo spazzino lancia il sacchetto sul pattern esatto della bottiglia del tossico, relay MP compreso; lo sprite approvato in SU-258 è stato importato campionando il centro dei blocchi (il raw è su griglia esatta da 40px, quindi la riduzione è senza perdita) per 8×14 finali, in scala con bottiglia (4×14) e birillo (6×14). Misurato in run vero: rivale da 80,0 a 36,0 px/s con spostamento reale di 36 px in un secondo, ronda da 78,0 a 35,1, entrambi tornano da soli dopo 2,6 s senza incastrarsi; spazzino 3 sacchetti in 12 s. Non regredito: costanti di polizia e barboni MP invariate. ⚠️ Non provato il multiplayer (conflitto EOS/ENet noto su questo Mac), ma non c'è codice di rete nuovo per il rallentamento, e il relay del sacchetto ricalca riga per riga quello della bottiglia. ⚠️ Da guardare in una run giocata: 3 sacchetti = 3 vite in 12 s col barbone fermo in linea di tiro, cioè lo scenario peggiore; la manopola è CLEANER_THROW_COOLDOWN. - [fix] SU-266 — le battute funzionano anche fuori dall'italiano, e nessuno muore più. Revisione idiomatica su tutte e otto le lingue, con censimento vero e non a campione: 890 chiavi uniche nei 7 CSV, di cui 624 idiomatiche (68%). Riviste 35 chiavi per 109 celle, con priorità alle lingue vecchie dove le rese letterali erano concentrate (de 21, en 19, fr 19, es 19, ru 11, pt 10, it 5, zh 5): HANDBAG SMACK diventa HANDBAGGED, che in inglese è un verbo vero; KEGELTREFFER diventa ALLE NEUNE!, il grido del birillo; *les mains trouées* diventa *les mains percées*, che è l'idioma che esiste davvero; GATLING FELINA diventa AMETRAGATADORA. 🔴 Il ritrovamento più grosso non era in programma: la regola «niente morte vera» era violata in quattro chiavi.
RUNHUD_UCCISIONI contava Bajas in spagnolo, Abschüsse in tedesco e Abates in portoghese, in un gioco dove nessun NPC muore mai e tutti dicono che si licenziano; e la schermata di fine partita annunciava «You starved to death.» e «Du bist verhungert.». Riscritte in tutte e otto le lingue — erano lì da prima di questo ticket. 🔴 Secondo difetto strutturale: 18 chiavi erano definite sia in ui_gioco.csv sia in ui_npc.csv, entrambi caricati in TranslationServer, quindi a schermo vinceva chi caricava per ultimo — e 8 avevano testi diversi nei due file. Allineate tutte, divergenze a zero; la rimozione delle righe doppie resta un follow-up strutturale. 📌 Decisa la regola che il ticket chiedeva di normare, sul conflitto fra adattamento culturale e ciò che si vede a schermo: vince ciò che è visibile, e l'adattamento non si butta ma si sposta nel testo libero accanto. Verificato nel codice che l'HUD disegna davvero nove icone (CAT_LIVES_MAX = 9), quindi spagnolo e portoghese tornano a NUEVE/NOVE VIDAS e le sette vite del gatto brasiliano diventano la battuta nella descrizione. Verifiche: verifica_i18n.py a zero, zero celle vuote su 908 righe × 8 lingue, segnaposto %s/%d di pari numero in ogni lingua riga per riga, formato CSV preservato byte per byte; provini guardati in en/de/ru/zh, nessun tofu, ё reso, nessun ripiego in italiano. ⚠️ Non coperto, dichiarato: le etichette tecniche di ui_menu (101) e ui_hud (91) scorse ma non riviste riga per riga, i blocchi lunghi del tutorial letti e lasciati intatti, nessuna validazione da madrelingua. ⚠️ Da guardare su telefono: HUD_FAME_CRITICA era un grido di 3-4 parole e ora è una frase; e i nomi di quartiere cambiano in es/pt/ru — non è stato verificato se i salvataggi memorizzano il nome tradotto o l'id. - [feat] SU-273 + SU-275 + SU-279 — liste che scorrono, una schermata Crediti, e il d-pad digitale di serie. Tre ticket in un lotto solo perché insistono sugli stessi file (
MainMenu.gd, Settings.gd, ui_menu.csv). SU-273: con otto lingue la lista della schermata Lingua sbordava dal riquadro e le ultime voci non si vedevano. La soluzione non è una pezza sulla lingua — MenuScrollList è un contenitore riusabile, applicato a tutte le liste che possono sbordare (opzioni, lingua, baracca, crediti). Quante voci entrino non è un numero scritto nel codice: lo misura finish() sulla geometria vera, quindi segue la scala del testo e il formato dello schermo. Si scorre col dito, e con su/giù la lista insegue la selezione, che non esce mai dal campo; una scrollbar in stile del gioco dice che c'è dell'altro. SU-275: nuova voce «Crediti» nelle opzioni, tradotta in tutte e otto le lingue, con sviluppo, motore Godot (MIT), font e multigiocatore. Il credito che non era un vezzo è quello del font cinese — «Fusion Pixel Font (c) TakWolf — SIL Open Font License 1.1»: la OFL lo pretende in gioco, non solo nel repo, ed era un follow-up già dichiarato in SU-262. La ricognizione delle terze parti non ha trovato altro da citare. La schermata usa lo scroll di SU-273, non un secondo modo di scorrere. SU-279: il movimento parte in d-pad digitale, l'analogico diventa una scelta nelle opzioni. Il default viveva in tre punti che ora dicono la stessa cosa (dichiarazione, lettura da settings.cfg, commento di intestazione), e reset_to_defaults() finalmente nomina la chiave, che prima si limitava a ignorare. Chi ha già scelto non viene scavalcato: la distinzione fra «chiave assente» e «chiave scritta a false» si fa con has_section_key(), perché il ripiego di get_value() appiattisce i due casi e riaccenderebbe il d-pad sotto le dita di chi l'aveva spento — stesso schema di SU-261. ⚠️ Non misurato: le 8 direzioni secche con un input reale (il provino mostra l'impostazione, non il movimento). 📌 Un dato dichiarato invece che nascosto: i tre formati provati fanno entrare lo stesso numero di voci (5 su 8), perché il pannello è dimensionato come frazione del viewport; il calcolo sull'altezza disponibile c'è e conta quando cambia la scala del testo, ma i tre scatti non lo mostrano come conteggi diversi. - [change] SU-272 — il preavviso di temporale scende in basso, e non litiga col pad. Il banner dell'ondata meteo compariva in alto al centro, sotto livello e vite, cioè proprio sopra la porzione di città che il giocatore sta guardando: ora è ancorato in basso al centro. Il punto delicato era non finire sopra al joypad virtuale, e la prima versione ha sbagliato proprio lì — riservava l'ingombro verticale del pad a prescindere, e il risultato era un banner a metà schermo: joystick e diamante vivono ai lati, mentre il banner è centrato e largo un quinto dello schermo, quindi non si incrociano quasi mai. Ora l'ingombro viene sottratto solo se i due rettangoli si intersecano davvero anche in orizzontale; altrimenti il banner sta in basso con margine 16 più safe-area. Il pulsante SUPERPOTERE passa dallo stesso test. Misurato, non stimato: Mac 16:10 e iPhone 19.5:9 non incrociano mai (banner a y=[640..752], gap 16); su iPad 4:3 forzando la scala del testo a «huge» l'incrocio scatta davvero (diamante a x=604, banner a x=682) e il banner sale — è la prova che la protezione funziona quando serve invece di essere sempre accesa. 📌 Corretto in corsa un difetto vecchio: l'altezza fissa del pannello (78px) era sotto il minimo reale del contenuto in preavviso (102px misurati); Godot fa crescere i Container oltre quel minimo verso il basso, quindi da bottom-anchor il pannello sforava fuori schermo. Nel vecchio layout in alto il difetto era identico ma sforava nel cielo vuoto, invisibile. Portato a 112px. ⚠️ Un difetto di collaudo che riguarda tutto il progetto:
--windowed e --resolution sono flag nativi, consumati dal motore prima di arrivare a OS.get_cmdline_args(), quindi l'escape-hatch di Settings.gd (SU-191) non li vede e il boot ricade a schermo intero. Tutti i provini lanciati con quei flag erano alla risoluzione nativa, qualunque cosa dicesse il nome del file — verificato su un secondo lotto dello stesso sprint, dove tre PNG «di formati diversi» avevano lo stesso MD5. Da oggi la dimensione si impone con DisplayServer da codice e si riafferma a ogni frame, perché la finestra scivola di nuovo a fullscreen a metà run. - [change] SU-274 — il cancello della beta solo dove serve: build di beta, Windows e macOS. Il gate girava su Windows, macOS e Android, e l'unica condizione di uscita era
OS.has_feature("editor") — cioè «non siamo nell'editor», che è vera anche in una build di produzione. Ora un interruttore unico, _gate_should_run(), governa tutti e tre gli effetti (schermata del Buttafuori, chiamata di rete a devices.json, banner di aggiornamento obbligatorio di SU-221) invece di tre condizioni sparse: serve la feature di export beta e una piattaforma desktop. Su Android, iOS e web non parte mai, quindi lì sparisce anche il costo — nessuna richiesta HTTP, nessuna attesa all'avvio. La logica sta in _gate_applies(), statica e pura (non chiama OS.*), isolata apposta per poter essere verificata senza esportare una build. Il punto da toccare quando arriverà Steam è scritto in testa al file: si spegne lì e basta. export_presets.cfg: custom_features="beta" ai soli preset macOS e Windows Desktop. ⚠️ Da sapere prima della prossima release: esiste un solo preset per ciascuno dei due, quindi oggi non c'è modo di produrre una build desktop *non*-beta senza togliere la feature o duplicare il preset. ⚠️ Non provati Android, iOS e web: nessun simulatore usato, e su device iOS reali non si installa per regola di progetto. La logica fail-closed di SU-219 è intatta. - [fix] SU-255 — la pensilina era già giusta: chiuso senza cambiare una riga, con le prove. Il KO chiedeva di importare
busshelter_NO_FLOOR.png al posto di busshelter.png, ma quell'import c'era già dal commit 9518149 delle 13:30 — e il KO è delle 14:03, 32 minuti dopo. Invece di rifare a comando si è stabilito quale delle tre ipotesi fosse vera (import non avvenuto / avvenuto male / corretto e KO scritto su una versione vecchia): è la terza. Misurato: gli script della pipeline puntano alla sorgente giusta, l'MD5 di bus_stop_base.png e bus_stop_roof.png combacia con quelli derivati da NO_FLOOR, e l'analisi riga per riga sotto i montanti non trova alcuna piastra. Guardato: nuovo provino in Main.tscn vera — non una copia isolata, che era il buco dichiarato del giro precedente — con contorni continui, nessun pavimento e il badge del bus tondo e riconoscibile. Resta riproducibile: scripts/tools/shot_su255_scena_vera.gd fotografa la pensilina in partita vera in un comando. 📌 Il valore di questo giro non è una modifica ma una diagnosi che ha evitato un quarto giro di rigenerazione su un'arte corretta. - [fix] SU-188 — la riga 5 del giullare guarda finalmente a nord-est, e non è stata rigenerata. Il KO chiedeva solo quello («il resto va bene»). Nord-ovest e nord-est sono l'uno lo specchio dell'altro e il costume è simmetrico (birilli in entrambe le mani, nessuna borsa su una spalla sola), quindi è bastato uno specchio orizzontale in PIL: convenzione di progetto, le correzioni geometriche su arte già approvata non si rigenerano — rigenerare avrebbe rimesso in discussione tutto il resto del foglio, che Ivan aveva già accettato. 📌 Il dettaglio che sarebbe stato un difetto silenzioso: lo specchio è stato fatto cella per cella e non sull'intera riga, perché ribaltare la riga avrebbe specchiato anche l'ordine dei fotogrammi — il passo sarebbe andato all'indietro, invisibile da fermo e visibile in movimento. Verificato bit a bit che fuori dalla riga 5 non cambia nulla. Consegna in temp (
arch_jester_v1_mirrorfix3.png), il Fatto lo mette Ivan. ⚠️ Terzo giro su questo ticket: se non è ancora giusta, il problema non è il disegno ma la convenzione delle direzioni del foglio 5×5, da fissare prima di generare altro. - [feat] SU-276 — il censimento delle emoji, e le prime 28 icone pixel che le sostituiranno. Lo strumento che mancava è
tools/audit_emoji_pittogrammi.py: scandisce le stringhe letterali dei .gd (commenti esclusi), le stringhe dei .tscn e tutte le celle dei CSV, ed esclude i codepoint già presenti in StreetU-Pixel.ttf. Esito: 70 pittogrammi distinti, 1175 occorrenze. È ripetibile apposta, perché SU-277 dovrà farlo tornare a zero: è la verifica che il ticket chiede al posto di un controllo a occhio. 📌 Una scoperta che cambia il conto: quattro voci che il ticket elencava da disegnare — infinito, radioattivo, sole e nuvola — sono già nel font pixel, coperte da SU-262 insieme a frecce, spunta e stella: non vanno disegnate né toccate. Generate 28 icone su 70, scelte per frequenza (coprono il 71% delle occorrenze reali), su base 16×16 px ricavata dal default_font_size=16 del tema e dalla griglia già usata da stat_icons.png/perk_icons.png, così stanno in linea dentro una frase. Fatte a fogli multi-icona invece che a 70 chiamate singole, per tenere lo stile coerente. ⚠️ Le 42 restanti mancano (tutte a frequenza ≤9) ed è una scelta dichiarata: meglio metà lotto approvabile che un lotto intero a metà. ⚠️ Riserve sulla leggibilità a 16px, guardate a dimensione reale: giocoliere e carta jolly diventano macchie, coriandoli si riduce a puntini, spumante confonde schiuma e tappo, folata di vento resta un turbine astratto — a quella dimensione o si semplifica il soggetto o si cambia simbolo. Nessun file di gioco toccato: la consegna è il foglio di contatto in TMP/.
Distribuzione sugli store: ambiente Android pronto, un blocco scoperto2026-08-04
- [docs]
DISTRIBUZIONE_STORE.md — la guida dagli account al rilascio, come persona fisica. Copre l'iscrizione a Google Play ($25 una tantum) e all'Apple Developer Program ($99/anno) con documento e carta a nome proprio, la configurazione degli ambienti, il primo rilascio su canale interno e il ciclo dei rilasci successivi. Dove STUDIO_BETA_STORE.md (SU-238) confrontava i canali e diceva *perché*, questo dice *come*, passo per passo, marcando cosa può fare solo Ivan. Tre cose che è meglio sapere prima di pagare, e che stanno nella guida per esteso: su Play il nome legale, il Paese e l'email diventano pubblici (e l'indirizzo di casa pure, il giorno in cui si aggiunge un acquisto in-app); su Apple, da persona fisica, il venditore sull'App Store è nome e cognome e non è aggirabile; e la chiave di firma dell'app su Play non si cambia mai più — sceglierla male significa che i tester con l'APK da Drive non potranno aggiornare dal Play Store senza disinstallare. - [build] Android portato ad API 36, dov'è la patch conta più di quale. Dal 31 agosto 2026 Play rifiuta app nuove e aggiornamenti che non puntino ad Android 16, canali di test compresi; il template di Godot 4.6.2 era fermo a 35. Alzati
compileSdk, targetSdk e build-tools a 36 e installati android-36 + build-tools 36.0.0 sul Mac. Il bump sta dentro installa_template_android.sh, accanto alle patch EOS, non solo nel template rigenerabile: messo altrove sarebbe sparito alla prima reinstallazione, in silenzio, come è già successo alle esclusioni degli asset. AGP 8.6.1 compila contro API 36 senza errori. ⚠️ Da provare su un telefono vero: Android 16 impone l'edge-to-edge e non lo si può più rifiutare — HUD sotto la barra di stato e controlli virtuali coperti dalla barra gesti sono i due punti da guardare, e su questo Mac non erano verificabili. - [build] Nuovo preset di export «Android Play (AAB)». Play rifiuta l'
.apk per ogni app nuova: serve un bundle con Play App Signing. Il preset clona quello Android — così eredita permessi, icone e include_filter già tarati — e cambia solo formato (export_format=1), percorso e target SDK. Export provato per davvero, non a lettura di codice: .aab da 61 MB, versionCode 26, targetSdk 36, solo arm64, jarsigner risponde «jar verified». Nota: armeabi-v7a era stato acceso e poi rispento — il plugin EOS non ha una libreria arm32, quella variante sarebbe uscita senza SDK nativo. export_all.sh non conosce ancora il target aab: per ora il bundle si fa a mano, col comando in guida. - [fix] ⚠️ Scoperto un blocco vero all'upload su Play: una libreria nativa non è allineata a 16 KB. Dal 1° novembre 2025 Play pretende
.so allineati a 16 KB per chi punta ad Android 15+. Delle quattro librerie del gioco tre sono a posto (libgodot_android.so, libEOSSDK.so, libc++_shared.so, tutte a 0x4000); libeosg.android.template_release.arm64.so — il binario GDExtension del plugin epic-online-services-godot — è a 0x1000. Non è aggirabile abbassando il target (la regola scatta da API 35 e dal 31 agosto il target minimo è 36) né correggibile a valle (l'allineamento si fissa al link). La 2.3.0 del 2026-06-09 è l'ultima release upstream e nessuno ha ancora segnalato il problema nel repo: va ricompilato con NDK r28+, o segnalato a monte. Fino ad allora l'AAB non si carica — ma account, moduli e privacy policy si possono preparare in parallelo.
Sprint, quarto giro2026-08-04
RIENTRI ATTESI: 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 — le quindici 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 sette chiavi attese — SU-153, SU-198, SU-203, SU-230, SU-232, SU-254, SU-255 — ne sono rientrate due, SU-153 e SU-255: tasso di rientro 2 su 7, contro il 6 su 7 del giro prima.)* ⚠️ Attenzione a come si legge il numero questa volta: quindici chiavi in un giro solo è il doppio del massimo storico, e cinque di esse non sono codice provato in partita ma consegne che aspettano un giudizio di Ivan (SU-153, SU-188 e SU-258 sono generazioni dove il «Fatto» è l'approvazione; SU-268 aspetta il suo login per itch.io; SU-236 è un documento). Il tasso di rientro del prossimo giro andrà quindi letto separando i difetti nostri dai giudizi estetici e dai cambi di idea, come già emerso il giro scorso. ⚠️ Il buco più grosso di questo giro, dichiarato: il multiplayer non è stato collaudato (non è collaudabile su questo Mac, conflitto EOS/ENet noto dal 31/07), eppure tre ticket lo chiedevano esplicitamente — SU-256, SU-257 e SU-260 hanno tutti percorsi di rete nuovi verificati solo a lettura di codice. È il candidato numero uno a rientrare.
- [feat] SU-256 + SU-257 + SU-260 — la notte diventa cattiva: la ronda toglie tre vite, il barbone rivale ti insegue e ti deruba, e ogni lancio NPC finalmente si sente. Tre ticket in un lotto solo perché insistono sugli stessi due file (
ModularNPC.gd e World.gd): tenerli separati avrebbe significato due agenti sullo stesso codice. SU-256: GameState.lose_life() prende un parametro amount in coda e opzionale, così tutte le cause storiche restano identiche (verificato: gli altri 8 chiamanti passano solo la causa). Dentro, però, le vite si scalano una per volta in un ciclo, così player_life_lost viene emesso tre volte e l'HUD fa scoppiare tre teste di gatto invece di fare un salto muto — e HUD.gd non è stato toccato, che era il vincolo. ⚠️ Il punto delicato era l'avviso «ULTIMA VITA» quando si saltano più soglie in un colpo, ed è stato verificato caso per caso leggendo il codice, non il report: 9→6 nessun avviso (cat_lives finale è 6, diverso da 1), 4→1 avviso una volta sola (valutato dopo l'intero colpo, non dentro il ciclo), 2→0 nessun avviso e caduta diretta su trigger_game_over(FINE_VITE). Il costo vive in NIGHT_GANG_LIFE_COST e viaggia in rete come parametro di gang_try_rob_puppets, così host e client non possono divergere sul numero. SU-257: il rivale smette di essere solo un concorrente sleale — aggro a vista 150 px, inseguimento a 80 px/s (la velocità d'archetipo viene memorizzata al primo aggro e ripristinata quando si calma), contatto a 29 px con −1 vita causa "rivale" e −15$ clampati, poi fuga di 3 s e 22 s di cooldown prima di poter riaggredire. Fuori aggro tiene tutti i comportamenti di prima: malus mance, bidoni, sfottò. Entra in ENEMY_ARCHETYPES e regge 2 colpi. 📌 La refurtiva in multiplayer è risolta senza round-trip: l'host ricorda solo a chi ha rubato, mentre l'importo esatto — clamp compreso — se lo ricorda il peer derubato in World._net_rival_loot; l'alternativa (il peer riporta l'importo all'host) avrebbe voluto un RPC in più e il routing per net_id. SU-260: throw.wav posizionale one-shot su gattara, tossico e giullare, con hook pronto per lo spazzino di SU-259, e il suono replicato ai client agganciandosi alle repliche esistenti. 📌 Una verifica che ha cambiato il progetto della soluzione: il ticket diceva che il lancio della gattara «non viene replicato», ma in World.gd un net_cat_thrown() esisteva già — solo che è quello del gatto del giocatore, porta un throw_id e vive di contabilità _net_cat_replicas/net_cat_despawn. Riusarlo avrebbe fatto entrare la replica della gattara nel gruppo dei bersagli del clash, producendo scontri gatto-contro-gatto fantasma sui client; è stato quindi aggiunto un gemello separato net_npc_cat_thrown() sul pattern esatto della bottiglia, senza toccare il percorso del player. ✅ Collaudato dopo il commit, e il collaudo è la parte che vale: compile-check completo 145 script + 39 scene, 0 falliti; tre run reali col bot, tutte concluse per fine vite, zero SCRIPT ERROR, zero orfani su 128 dump, fps stabili; provino dedicato 41/41 controlli OK. 📌 La prova migliore è un'attribuzione dal vivo, non una deduzione: agganciando EventBus.player_life_lost durante una partita vera, il colpo della ronda produce [VITEMON] causa=ronda restanti=8 / 7 / 6 nello stesso istante — cioè 9→6 in un colpo, tre segnali per far scoppiare tre teste. Nella stessa sessione il rivale ha derubato 6 volte e l'arresto 3, sempre −1 vita ciascuno: la retrocompatibilità di lose_life non è dedotta dalla firma, è misurata. Verificati anche i tre casi limite dell'avviso: da 4 vite scatta una volta sola, da 3 vite non scatta e si va a game over, e a vite già a zero un colpo ulteriore non genera un secondo game over. ⚠️ NON MISURATO: il multiplayer — non collaudabile su questo Mac (conflitto EOS/ENet documentato dal 31/07), quindi rival_try_rob_puppets, rival_refund_peer e net_npc_cat_thrown sono verificati solo a lettura di codice. I tre ticket chiedono «test obbligatorio SP + MP»: la metà MP resta da provare. 📌 Osservazione di bilanciamento, non un difetto: il bot survive non ha AI di fuga e si è fatto derubare dal rivale 6 volte in ~4 minuti — probabilmente pessimistico rispetto a un giocatore vero, ma vale la pena guardarlo in playtest. 📌 Effetto collaterale voluto ma da collaudare: da ora il rivale colpito dal gatto non è più un «passante», quindi niente wanted, niente panico di massa, niente monete a terra dal colpo — è ciò che chiede il ticket, ma cambia il valore di colpirlo per sbaglio in mezzo alla folla. Bilanciamento da playtest: il rivale è rare con peso 30 negli slums e con l'aggro attivo può appesantire quel quartiere; le due manopole (RIVAL_ROB_COOLDOWN, RIVAL_AGGRO_RANGE) sono costanti isolate in cima al blocco. File: ModularNPC.gd, World.gd, GameState.gd, EventBus.gd (solo commenti), NPC.gd (solo commenti), CatProjectile.gd, assets/translations/ui_npc.csv (18 chiavi nuove in 5 lingue, da 110 a 128). - [docs] SU-236 (domanda di Ivan) — se non è Firebase, l'analitica si fa con quello che è già gratis più un endpoint HTTP; il solo buco vero è il crash reporting su desktop. La Parte 1 di ieri diceva no a Firebase, e Ivan ha chiesto la conseguenza: «ma se non è firebase come potrei fare analitica sulle app installate in beta? o anche in quelle finali?». Risposta in coda a
SPIKE_FIREBASE.md (§8-13), 90 righe di pura aggiunta, zero righe rimosse: la Parte 1 non è stata toccata. Il chiarimento che regge tutto il resto: si chiamano tutte «analitica» tre cose diverse — telemetria di gioco (eventi custom), crash reporting, metriche di store — e nessuno strumento le fa da solo tutte e tre; è per questo che la domanda sembrava avere una risposta unica e non ce l'ha. Quello che è già gratis e nessuno sta guardando: Play Console con Android vitals e Xcode Organizer, che prende i crash anche da TestFlight (mentre App Store Connect Analytics solo dalla produzione — distinzione che cambia cosa si vede durante una beta). Restano scoperti Windows a mano e macOS fuori store: è lo stesso buco per cui la Parte 1 aveva già scartato Crashlytics, e non lo chiude nessuno dei due store. Per gli eventi di gameplay la raccomandazione è PostHog: HTTP puro, nessun plugin, nessun HMAC, 1M eventi/mese gratis, e soprattutto lo stesso pattern di BetaGate.gd che il progetto già usa — cioè zero tecnologia nuova da imparare. Scartati: GameAnalytics via SDK nativo (ridondante), l'endpoint scritto interamente a mano (reinventa quello che PostHog dà gratis), e il pattern «JSON su GitHub» in scrittura (il token lato client è estraibile). 📌 La scoperta della Parte 2: esiste ora un SDK Sentry ufficiale per Godot — ed è l'unico caso di tutto lo spike in cui un plugin nativo non ha alternativa via REST, perché un crash duro del motore non può telefonare a casa da solo. Se si farà, è un'eccezione alla regola «niente dipendenze esterne» da decidere consapevolmente, non un sì automatico. Privacy: per telemetria strettamente anonima non serve né un banner di consenso GDPR né il flusso ATT (che riguarda il tracciamento cross-azienda per pubblicità, non l'analytics di prima parte); servono comunque privacy policy, Data Safety di Play e Privacy Label Apple. ⚠️ Quattro punti dichiarati come NON confermati da fonte primaria e lasciati in coda al documento invece che nascosti: se Android vitals copra la track di beta chiusa come la produzione, la cifra «MAU illimitati» di GameAnalytics, la guida CNIL che è francese e non norma UE uniforme, e Sentry-Godot da provare con un crash vero prima della release. 📌 Nota di processo che vale la pena tenere: l'agente aveva parallelizzato la ricerca e uno dei rami ha ecceduto il mandato scrivendo lui il documento; invece di accettarlo, ha ricontrollato il testo contro le ricerche indipendenti degli altri rami e ha verificato di persona le cifre Sentry sulla pagina ufficiale, trovando e correggendo un punto dove il testo era più sicuro di quanto la ricerca giustificasse. Nessun file di codice toccato. File: SPIKE_FIREBASE.md. - [feat] SU-261 + SU-262 — il gioco parte nella lingua di chi lo installa, e il font pixel smette di prendere in prestito lettere dal sistema. SU-261: al primo avvio si legge
OS.get_locale_language(), si normalizza (minuscolo, via il suffisso di regione) e la si usa se è fra le LINGUE, altrimenti inglese (nuova LINGUA_RIPIEGO_SISTEMA). LINGUA_DEFAULT resta "it" ma solo come ripiego dei *testi*, che è quello che chiede il ticket. 📌 Due scelte non ovvie, entrambe nate da un difetto trovato in corsa: (1) detect_system_language() è statica, non un metodo d'istanza — la prima stesura faceva get_node("/root/I18n") da dentro Settings.load_settings(), che gira in _ready() e quindi scommette sull'ordine degli autoload: il collaudo ha mostrato che in certi contesti risponde null e la lingua «dedotta» finiva per essere quella precedente. (2) Il discrimine del primo avvio è cfg.has_section_key("general","lingua") e non il default di get_value(), perché quest'ultimo appiattisce due casi diversi: chiave assente (primo avvio, si rileva) e chiave presente ma vuota o storta (scelta del giocatore, intoccabile). SU-262: build_pixel_font.py passa da 89 a 243 codepoint — accentate di fr/es/de/pt, punteggiatura dei CSV, frecce e simboli dell'UI, e il cirillico completo. I glifi nuovi nascono dallo stesso pipeline dei vecchi invece che da un ritocco del TTF, e le lettere cirilliche identiche alle latine sono alias di cmap sullo stesso glifo, non disegni duplicati. 📌 La non-regressione è stata verificata al livello del font, non a occhio: confrontando il TTF vecchio col nuovo, 0 codepoint persi, 0 larghezze cambiate sui 89 glifi preesistenti, unitsPerEm/ascent/descent identici — una prova più forte di uno screenshot, perché copre tutti i glifi e non solo quelli inquadrati. Audit: prima mancavano 140 caratteri distinti (de 41 · en 34 · es 47 · fr 45 · it 36 · pt 32 · ru 74), dopo ZERO su tutte e sette le lingue; lo script tools/audit_font_glifi.py incrocia i caratteri realmente usati nei CSV con la cmap ed esclude le emoji per scelta dichiarata. Font CJK: Fusion Pixel 10px zh_hans, SIL OFL 1.1 — em 1000, cap 700, x-height 500, cioè esattamente la nostra griglia. ⚠️ Zpix scartato per licenza: 1000 USD per prodotto commerciale, modifica e ridistribuzione vietate — era il candidato citato nel ticket, e sarebbe stato un problema serio dentro una build venduta. Il fallback è agganciato con una FontVariation nel tema: 📌 provato prima nel .import, ma al reimport Godot riscrive il file perdendo uid e i parametri pixel (hinting=0, subpixel_positioning=0) — strada abbandonata e file ripristinato da git. Provini guardati (TMP/su262/): nel foglio glifi il cirillico passa da liscio (fallback di sistema) a pixel e il CJK da caselle esadecimali a ideogrammi veri (汉字 中文 你好世界 街头大学); nel confronto del menu impilato prima/sopra e dopo/sotto voci, posizioni e spaziature sono identiche — la FontVariation non ha spostato il layout, che era il rischio non coperto dal controllo sui glifi. ⚠️ NON MISURATO: l'installazione pulita su Android e iOS con telefono in tedesco (qui si è potuto forzare solo il locale macOS, dove il test automatico dà 19 controlli e 0 errori, caso tedesco e giapponese→inglese compresi); l'HUD e le schermate di partita con testi accentati (provini solo di menu e opzioni); il peso reale dell'APK con 3,9 MB di font in più. 📌 Follow-up dichiarati: l'OFL vuole il credito «Fusion Pixel Font © TakWolf, OFL 1.1» anche nei crediti in-game, non solo nel repo; e reset_to_defaults() rimette "it" anche a un tedesco — lasciato com'era perché il ticket vieta di toccare chi ha già salvato, ma è da decidere. Il settings.cfg di Ivan non è mai stato toccato: tutte le prove sono girate con HOME separata. File: build_pixel_font.py, I18n.gd, Settings.gd, default_theme.tres, assets/fonts/ (TTF rigenerato + font CJK + licenze), nuovi tools/audit_font_glifi.py, scripts/tools/check_su261_lingua.gd, scripts/tools/shot_su262_font.gd. - [feat] SU-263 + SU-264 + SU-265 — il gioco parla russo, cinese semplificato e portoghese brasiliano: da 5 lingue a 8. Tre lotti in parallelo hanno prodotto la parte creativa, l'orchestratore ha fuso le colonne. 879 chiavi per lingua (non 861:
ui_npc.csv è cresciuto durante la lavorazione per via del lotto NPC, e tutti e tre gli agenti se ne sono accorti da soli rileggendo il CSV vivo invece di fidarsi del numero nel brief — le 18 chiavi nuove sono tradotte). Zero celle vuote, zero segnaposto sballati. 📌 La fusione non riscrive i CSV, ci appende: i 7 file hanno quoting misto riga per riga (scritti da strumenti diversi nel tempo) e nessuno stile del modulo csv li riproduce — verificato: QUOTE_MINIMAL ricostruisce solo ui_mondo e ui_npc, QUOTE_ALL nessuno. Riscriverli avrebbe prodotto un diff enorme e falso che avrebbe nascosto le modifiche vere. Il nuovo tools/merge_i18n_columns.py preserva il testo grezzo di ogni record e ci attacca il campo in coda: ricostruzione byte-identica verificata su tutti e 7 i file, comprese le 46 celle multi-riga, e a fusione avvenuta le 6 colonne preesistenti risultano identiche al contenuto di git. Lo script si rifiuta di scrivere se manca una chiave, se una cella è vuota o se i segnaposto non coincidono con l'italiano — ⚠️ e il controllo dei segnaposto copre anche i formati con padding (%02d, %-3s, %6d, %.1f): una regex ristretta a %s|%d li avrebbe ignorati in silenzio, ed è proprio lì che le colonne dell'UI si sfasano. Traduzione creativa, non letterale, che era il requisito vero: GATTLING → КОТЛИНГ (ru), 猫特林 (zh, gioco di parole su 加特林), GATRALHADORA (pt, portmanteau vero gato+metralhadora); «l'acqua si è licenziata» → вода уволилась, 水都撂挑子不干了, a água pediu demissão; il proverbio «chi lascia la strada vecchia» sostituito ogni volta con l'equivalente autentico della cultura d'arrivo (от добра добра не ищут, 多一事不如少一事, antes um mau conhecido…). ⚠️ Una scelta del portoghese che va guardata: «NOVE VITE» è diventato «SETE VIDAS», perché in Brasile il gatto ha sette vite — culturalmente giusto, ma l'HUD disegna nove teste di gatto, quindi il nome della carta e il numero a schermo non combaciano. Sono 2 chiavi (CARTA_NOVE_VITE_NOME, META_NOVE_VITE) ed è una decisione di Ivan, non un difetto da correggere di nascosto. 📌 Un difetto trovato guardando il provino, e non era nel gioco: al primo scatto il pannello russo mostrava l'italiano. La diagnosi ha smentito il sospetto ovvio — caricando a mano le risorse, locale=ru restituisce НАЧАТЬ ИГРУ, quindi dati e locale erano a posto. La causa è che all'avvio l'autoload I18n non ha ancora finito di caricare i .translation, e I18n.t(chiave, ripiego) ripiega sull'italiano: il primo pannello esce in italiano qualunque lingua gli si chieda. Confermato invertendo l'ordine (con zh in testa, è il cinese a uscire in italiano). ⚠️ Il provino di SU-262 aveva lo stesso difetto e non si vedeva, perché lì la prima lingua era l'italiano e il ripiego coincideva col risultato atteso: lo strumento mentiva senza che nessuno potesse accorgersene. Aggiunto uno scatto di riscaldamento da buttare via, col perché scritto nel codice. Provino finale guardato: le tre lingue rendono tutte correttamente, cirillico in pixel e cinese senza un solo quadratino grazie al fallback CJK di SU-262. Bandierine ru/zh/pt aggiunte a tools/gen_flags.py (russa a bande, cinese col rombo e le quattro stelline, brasiliana con rombo e disco) e guardate: le 5 vecchie restano byte-identiche. 21 file .translation generati dal reimport, zero errori. ⚠️ NON MISURATO: il word-wrap del cinese nei box stretti (HUD, carte, notifiche) — il cinese non ha spazi e va a capo con regole sue; l'allineamento in colonna delle stringhe con %2d/%-3s quando i caratteri sono a larghezza piena; e la resa in partita, visto che il provino copre menu e opzioni. 📌 Piccola incoerenza fra lingue: HIGH SCORES è tradotto in ru (РЕКОРДЫ) e zh (排行榜) ma lasciato in inglese in pt — com'è in italiano. File: i 7 CSV di assets/translations/ + 21 .translation + 7 .import, I18n.gd (LINGUE), assets/ui/flags/flag_{ru,zh,pt}.png, tools/gen_flags.py, nuovi tools/merge_i18n_columns.py e scripts/tools/shot_su263_lingue_nuove.gd. - [feat] SU-267 — STREET UNIVERSITY gira in un browser: preset Web e variante di progetto senza EOS, prodotta da uno script e mai committata. Il blocco non era l'export ma il plugin EOS: è GDExtension nativa senza build wasm, e i suoi 10 autoload
H* usano tipi nativi nelle firme, quindi su web sono parse error all'avvio, non un degrado pulito. tools/build_web.sh fa rsync del progetto in una dir di lavoro fuori dal repo (escludendo l'addon EOS da 489 MB e le credenziali), lì tools/web_variant.py genera la variante di project.godot senza i 10 autoload e senza il plugin, ed esporta da quella copia. 📌 La prova che conta è che non ci sia prova: git diff --stat -- files/homeless_city/project.godot è vuoto, e i 4 preset esistenti (iOS/macOS/Windows/Android) non hanno una sola riga rimossa — il preset Web è in coda. Scartate: il fork a mano del progetto (si disallinea in una settimana) e i rami if OS.has_feature("web") dentro gli autoload EOS (sono nativi, il parse error avviene prima che il ramo venga valutato). Sia lo script Python sia lo shell script hanno un controllo finale che fa fallire la build se nella variante resta traccia di EOS. Threads: preset con thread_support=false (template web_nothreads), così gira su un server HTTP qualunque senza gli header COOP/COEP che itch.io non garantisce — scelta che eredita il ticket demo, confermata dal log del motore («single-threaded, no GDExtension support», zero pthread_create nel wasm). ⚠️ UNA DECISIONE CHE ASPETTA IVAN: la variante web toglie anche l'autoload BetaGate. Al primo giro la build si fermava sul buttafuori senza mai arrivare al menu. Non è una scorciatoia: sul web OS.get_unique_id() non esiste — lo dichiara Godot in console — quindi *tutti i browser del mondo calcolano lo stesso device_code()* e la lista dispositivi non distingue più nessuno, restando solo un interruttore globale su «chiuso». La politica d'accesso della demo pubblica è materia del ticket demo. Reversibile con SU_WEB_KEEP_BETAGATE=1. Verificato in Chrome: si avvia, console senza errori, MULTIGIOCATORE assente dal menu, menu → tutorial → metropolitana → mondo generato e giocabile con HUD, NPC e orologio che avanza; tastiera e mouse ok; user:// su IndexedDB sopravvive al reload. Nessuna regressione: compile-check 2/2 e export macOS rifatto e riuscito. ⚠️ NON MISURATO, e va detto: Firefox e Safari mai provati (gli strumenti qui pilotano solo Chrome); la run non è stata portata a game over; nessun numero di prestazioni — la scheda gira in background e requestAnimationFrame è strozzato dal browser, quindi ogni FPS misurato sarebbe falso; l'audio ha il driver vivo (AudioContext running a 48 kHz, due AudioBufferSourceNode.start()) ma che si senta va provato a orecchio. 📌 Follow-up sul peso: splash 2,6 MB + icona 1,9 MB pesano sul primo caricamento, e res://iOS/{icon,splash}.png finiscono nel pck malgrado l'exclude_filter (li tira dentro config/icon, ~4,5 MB recuperabili); i 36 MB di wasm vanno serviti con gzip/brotli. File: export_presets.cfg (solo aggiunte), MainMenu.gd (additivo: _is_web() e filtro voci MP), EOSBridge.gd (additivo, web-only), .gitignore, nuovi tools/build_web.sh e tools/web_variant.py. - [feat] SU-153 (KO Ivan) + SU-188 (KO Ivan) + SU-258 — generati in temp: le righe diagonali finalmente girate dalla parte giusta, e il sacchetto dello spazzino. Tre ticket di generazione, quindi il «Fatto» lo mette solo Ivan. ⚠️ Cos'era stato capito male, ed è la stessa cosa per entrambi i KO: nei giri precedenti le righe SUD-OVEST e NORD-EST erano state disegnate frontali, cioè non direzionali. Il difetto non si vede dalle misure — griglia e alternanza delle gambe passavano lo stesso — e nemmeno da un foglio di contatto normale: si vede solo guardando un foglio etichettato riga per riga, che è come è stato verificato stavolta. La causa è che Codex sbaglia le diagonali quando non gliele si descrive a parole: dire «riga 4 = sud-ovest» non basta, va scritto cosa deve mostrare (corpo girato in basso-a-sinistra, viso di tre quarti / di spalle in alto-a-destra, viso non visibile). SU-153 (la barbona, KO «le ultime due righe sono sbagliate, vanno rigenerate come per gli npc»): consegnata la v4 in
sprites_raw/PEOPLE/temp/su153_donna/. Guardata su foglio etichettato: la riga 4 è ora una vera diagonale di tre quarti (distinguibile a colpo d'occhio dalla riga 1, che è frontale) e la riga 5 è di spalle angolata, distinguibile dalla riga 2. Personaggio coerente in tutte e 25 le celle: stesso berretto, stesso cappotto, stessa tracolla. SU-188 (il giullare, KO «SUD OVEST in realtà è SUD est… stessa cosa NORD OVEST che è NORD EST»): rigenerato in temp/walk/, stesso esito — la riga SW era frontale in tutte e 5 le colonne e ora è girata, la NE è di spalle angolata. Costume arlecchino, cappello coi sonagli e birilli invariati. 📌 Il controllo di griglia è stato rifatto su entrambi perché in un giro precedente aveva scoperto due righe fuse senza gutter di magenta (l'import vero si sarebbe rotto): 1254×1254, 6 gutter per asse e 25/25 celle piene su tutti e due. SU-258 (sacchetto dell'immondizia dello spazzino): consegnato in sprites_raw/OBJECTS/temp/, sacco scuro col nodo in cima, risoluzione effettiva già da proiettile. Provino alla stessa scala di bottiglia e birillo: a 14 px di altezza il sacchetto è 8×14 contro 4×14 della bottiglia e 6×14 del birillo, quindi la scala è coerente. ⚠️ Da guardare prima di approvare: il sacchetto è uniformemente scuro, mentre bottiglia e birillo hanno contrasto (etichetta chiara, corpo bianco) — su asfalto notturno potrebbe leggersi come una macchia invece che come un oggetto. 📌 Assunzione dichiarata su SU-188: Ivan non aveva risposto alla domanda «rigenerare il giullare o rimandare»; si è proceduto con la rigenerazione perché nello stesso giorno, sul ticket gemello SU-153, ha chiesto esplicitamente di rigenerare le diagonali «come per gli npc». Nessun file di gioco toccato: solo generazione in temp, quindi niente di tutto questo entra in git (sprites_raw/ è LFS). Provini in TMP/lotto_orch/su153_v4_righe.png, su188_giullare_righe.png, su258_confronto_scala.png. - [fix] SU-255 (KO Ivan) — la pensilina senza pavimento entra in gioco, e il contorno smette di sbriciolarsi. ⚠️ Cos'era stato capito male: il giro precedente aveva ritagliato il pavimento dal vecchio raw con PIL, e Ivan ha giudicato il risultato «tagliata male», rigenerando lui lo sprite con Codex e chiedendo solo l'import: «la trovi in sprites raw al solito posto (nome busshelter_NO_FLOOR), importala e fai quello che devi fare… va comunque reimpostata con l'area di utilizzo precedente». Quindi zero generazione: la sorgente è la sua. 📌 La pensilina in gioco non è un solo sprite ma
bus_stop_base.png + bus_stop_roof.png, entrambi 80×76, usati sia dalla città (WorldGenerator.gd) sia dal tutorial (TutorialWorld.gd:901,909 — verificato: stesso SPR_OFFSET_Y=-22.0, nessun ancoraggio diverso che si rompa). La sorgente nuova è 1254×1254 contro i 1402×1122 della vecchia, quindi niente ridimensionamento 1:1: la scala si ritrova dagli elementi che stanno in piedi. ⚠️ Il punto del KO era il riposizionamento: senza il pavimento la sagoma è più corta, e centrarla nel riquadro avrebbe spostato la pensilina sulla mappa. L'allineamento è stato ricavato dalle àncore (bordo del tetto, base dei montanti), non dal bounding box dei pixel opachi. 📌 Un difetto trovato dall'orchestratore guardando lo zoom, e non era nel report dell'agente: la prima consegna aveva l'allineamento giusto ma l'arte sbriciolata — contorno scuro tratteggiato invece che continuo, frangia a scaletta sui montanti, e il bollino del bus ridotto a una macchia bianca illeggibile. La causa è il filtro: ridurre di ~11,6× (873 px → 76 px) con nearest-neighbor significa campionare un pixel ogni dodici, e un contorno spesso 2-3 px viene preso o saltato a caso. Affiancati NEAREST / BOX / LANCZOS sullo stesso riquadro, BOX (media d'area) ricostruisce il contorno continuo e rende di nuovo leggibile il badge. Rifatto con BOX, magenta tolto e alpha premoltiplicato prima del resize (altrimenti BOX mescola il magenta col bordo e lascia un alone rosa), soglia alpha tarata a 157 — perché la soglia sposta il bounding box di 1-2 px e la pensilina cambierebbe dimensione sulla mappa. Bbox verificati identici alla consegna corretta: base (4,29,75,66), roof (2,0,77,29). Guardato il confronto vecchio/nuovo a ×8: contorno continuo, frangia sparita, bollino tornato un badge tondo. ⚠️ Onestà sul limite: a ~10 px il pittogramma dentro il badge resta un'ombra, non è leggibile linea per linea — è un miglioramento netto, non piena leggibilità. ⚠️ NON MISURATO: il provino in-engine è stato fatto su una copia isolata, perché in quel momento la tree condivisa era rotta da un lavoro concorrente (autoload DemoMode non ancora registrato); va rivisto sulla tree vera. File: assets/sprites/world/bus_stop_base.png, bus_stop_roof.png, nuovi scripts/tools/shot_su255_busstop.gd e scenes/tools/shot_su255_busstop.tscn. - [feat] SU-268 — la demo web: solo il Centro, nessuna scrittura su disco, e l'invito a scaricare il gioco completo. Il flag è uno solo: l'autoload
DemoMode, acceso da tools/web_variant.py --demo che scrive config/demo_mode=true solo sulla copia di lavoro, più -- --demo da riga di comando per le prove (stesso schema di --bot). 📌 Due strade scartate, e la seconda ha fatto danno: la feature tag nel preset avrebbe richiesto di toccare export_presets.cfg, e una class_name globale senza autoload compilava solo con la cache classi fresca — è la cosa che per un'ora ha lasciato la tree condivisa non compilabile per gli altri agenti (DemoMode referenziato in 6 file, autoload non registrato). ⚠️ Il divieto di toccare project.godot era mio ed era sbagliato: nasceva dal criterio di SU-267 (la rimozione di EOS deve vivere solo nella variante) e non doveva impedire di registrare un autoload nuovo. Corretto: project.godot ha +1 riga sola, con DemoMode messo per primo perché GameState/Settings/Quartieri/MetaProgress/HighScore lo interrogano nel proprio _ready(). 📌 Le scritture sono state cercate tutte, non solo quelle nominate dal ticket: Settings.save_settings, save_keybinds, reset_keybinds_to_default, MetaProgress.save, Quartieri.save, HighScore.save_scores — e anche le letture corrispondenti, così un user:// lasciato da un'altra build sullo stesso dominio non resuscita dentro la demo. Gating dei quartieri in un punto unico (MetaProgress.is_unlocked: nemmeno l'unlock_all() del debug apre) più una rete di sicurezza in Quartieri.set_current. La prova della non-persistenza è una misura, non un'argomentazione: IndexedDB /userfs dopo una run giocata in demo = 0 chiavi, contro 29 chiavi (incluso settings.cfg) nella build piena provata con lo stesso metodo — cioè lo strumento di misura *vede* le scritture, e in demo non ne trova. In più un A/B su desktop: stessa run con e senza -- --demo, la prima scrive settings.cfg e meta.cfg, la seconda niente. Guardati in Chrome: menu col watermark DEMO, città in gioco, metro «in sciopero», schermata d'invito; e la build piena senza watermark né voce demo, per provare che non si accende da sola. check_files.sh 7/7 OK. 📌 Due sorprese trovate provando: (1) il watermark all'angolo finiva sopra il punteggio dell'HUD ed è stato abbassato; (2) ⚠️ nel browser le emoji non esistono — il tema le prende dal font di sistema, che sul web non c'è, quindi 🚇 usciva come rettangolo col codice esadecimale. Tolte dalle stringhe nuove, ma il problema vale per ogni emoji del gioco in qualunque build web, compresa quella piena di SU-267: merita un ticket a parte. ⚠️ NON MISURATO: Firefox e Safari (non pilotabili da qui); telefono e tablet nel browser; la pagina itch.io non è stata creata — serve il login di Ivan, la build è pronta in build/web_demo/ (68 MB) con le istruzioni di caricamento. 📌 Domanda aperta per Ivan: che URL mettere nell'invito. Oggi la build lo dichiara apertamente («LINK IN ARRIVO»), e quando si decide basta SU_DEMO_URL="https://…" tools/build_web_demo.sh, senza toccare codice. File: nuovo scripts/autoload/demo_mode.gd, GameState.gd, Settings.gd, MetaProgress.gd, Quartieri.gd, HighScore.gd, MainMenu.gd, assets/translations/ui_menu.csv (8 chiavi × 8 lingue), project.godot (1 riga), tools/web_variant.py, tools/build_web.sh, nuovo tools/build_web_demo.sh.
Sprint, terzo giro2026-08-04
RIENTRI ATTESI: SU-153, SU-198, SU-203, SU-230, SU-232, SU-254, SU-255 — le sette 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 sette chiavi attese — SU-153, SU-188, SU-198, SU-203, SU-230, SU-232, SU-233 — ne sono rientrate sei, tutte tranne SU-233: tasso di rientro 6 su 7, di gran lunga il peggiore mai registrato, contro 2 su 11 e 1 su 6 dei due giri prima.* ⚠️ La lettura onesta è che il numero non misura una sola cosa: tre dei sei rientri — SU-198, SU-203 e SU-232 — sono cambi di idea di Ivan su lavoro tecnicamente riuscito («reverta», «preferisco la versione prima», «cambio idea»), non difetti nostri. Gli altri tre — SU-153, SU-188, SU-230 — sono difetti veri, e hanno tutti la stessa forma: il criterio era stato misurato invece che guardato. SU-188 misurava l'alternanza delle gambe mentre il KO parlava di direzioni; SU-230 aveva guardato una sola direzione e da fermo; SU-153 aveva misurato la griglia senza confrontare le righe per direzione. Il rimedio non è misurare di più.*)
- [feat] SU-153 (KO Ivan) + SU-255 — generati in temp: la barbona rifatta sul barbone GIUSTO, e la pensilina senza pavimento fatta con PIL invece che con Codex. Due ticket di generazione, quindi il «Fatto» lo mette solo Ivan. SU-153: ⚠️ cos'era stato capito male — il riferimento allegato a Codex era il barbone rigenerato di SU-200, ma nello stesso sprint Ivan ha bocciato SU-203 e chiesto di tornare indietro: stavamo disegnando la compagna di un personaggio che non è più nel gioco. Stavolta il riferimento è stato estratto in modo deterministico dal git (
git show 5ea2520^:…player_sheet.png) invece che letto dalla working copy — che in quel momento era in mano al lotto del revert, quindi leggerla sarebbe stata una corsa. Il KO vero era la costanza del personaggio nelle 25 celle, ed è chiusa: variazione dentro riga ≤1,6 punti, stessi vestiti/colori/corporatura/altezza. 📌 Un difetto trovato cercandolo, non richiesto dal KO, e corretto con un secondo giro: nel v2 la riga NE mostrava il viso frontale invece della vista di spalle, e SW e NE erano quasi identiche quando devono essere l'una il contrario dell'altra — all'import la barbona avrebbe guardato dalla parte sbagliata. Il v3 la corregge, verificata riga per riga contro il riferimento su un foglio etichettato per direzione (il primo foglio, non etichettato, era fuorviante: affiancava lo sheet di gioco E/SE/S/NE/N con il raw S/N/E/SW/NE, cioè direzioni diverse). Costo: 2 chiamate Codex. ⚠️ Da guardare prima di approvare: il riempimento della cella varia fra le righe (S 90,9-92,1% contro SW 96,9-97,3%) — se l'import non normalizza cella per cella, la barbona può sembrare più grande camminando in diagonale. SU-255 (pensilina senza la piastra a terra, ticket aperto su richiesta di Ivan nel KO di SU-198): chiuso con zero chiamate Codex. La piastra era un blocco isometrico nettamente separabile, quindi si applica la regola di progetto — su arte già approvata si corregge con PIL, mai rigenerando: 241.029 px di area connessa rimossa, tettoia/vetri/montanti/insegna intatti, stessa dimensione 1402×1122. ⚠️ Senza la piastra i due montanti frontali restano come gambe sottili che finiscono a mezz'aria: è la geometria vera che c'era sotto, ma è il punto da guardare. 📌 Scoperta che serve al futuro import: la pensilina in gioco non è un solo sprite ma bus_stop_base.png + bus_stop_roof.png (WorldGenerator.gd ~3441). Nessun file di gioco toccato da nessuno dei due ticket. File nuovi (fuori da git): sprites_raw/PEOPLE/temp/su153_donna/su153_arch_player_female_v3.png, sprites_raw/WORLD/temp/su255_busshelter_no_floor_v1.png. - [docs] SU-188 (KO Ivan) — il giullare guarda dalla parte sbagliata perché la riga SW è disegnata frontale, non perché il codice sbagli lo specchio. Diagnosi, nessun rimedio applicato: la causa è nell'arte e chiuderla vuol dire rigenerare, che è una decisione di Ivan. Cos'era stato capito male: il pilota del giro scorso aveva misurato l'alternanza delle gambe ed era verde su quella — ma il KO parlava di direzioni, che è un'altra proprietà: una riga può avere le gambe che alternano benissimo ed essere disegnata guardando altrove. L'ipotesi più ovvia è stata smentita per prima:
NPC.gd (_play_dir_anim + FLIP_MAP, righe 1539-1568) è l'unico punto che sceglie animazione e flip_h, ed è condiviso identico da tutti gli archetipi — se fosse il codice sarebbero rotti tutti, e il poliziotto invece è corretto. La causa vera, guardata: nel raw del giullare la riga SW è frontale in tutte e 5 le colonne, senza nessuna torsione a sinistra. E quella riga alimenta sia se (la pipeline la specchia) sia sw (specchiata di nuovo a runtime): due specchi si annullano e le due diagonali sud mostrano la stessa immagine non direzionale — esattamente «SUD OVEST in realtà è SUD est». La riga ne ha lo stesso difetto in forma lieve: è di spalle ma poco diagonale, quindi il suo specchio nord-ovest le somiglia troppo, che è la seconda metà del KO. 📌 Il pattern che vale più del singolo ticket: nello stesso giro la barbona di SU-153 è uscita con la riga NE frontale invece che di spalle. Codex sbaglia le righe diagonali quando non gliele si descrive a parole — va messo nel prompt standard di generazione dei personaggi, altrimenti il difetto si ripete a ogni sheet. Nessun file toccato; provini in TMP/screenshots_claude/SU188_LOTTO_F_*. - [change] SU-203 + SU-230 (KO Ivan) — torna lo sprite vecchio del barbone, e la lanterna si ritara su quello. Due ticket in un lotto solo perché insistono sulla stessa risorsa, e nell'ordine: il revert cambia l'arte sotto i piedi della taratura. SU-203:
player_sheet.png riportato alla versione pre-5ea2520 («preferisco la versione prima del barbone, reverta»), verificato identico byte a byte (39.952 byte). Controllato che il commit revertato non avesse toccato nient'altro di importato: player_frames.tres e player_sheet.png.import hanno diff vuoto fra 5ea2520^ e 5ea2520, e il commit successivo che tocca assets/sprites/ aggiunge solo asset UI. SU-230: il KO era «avvicinala ancora di un paio di pixel in modo da nascondere completamente la mano che si muove» a est/ovest. ⚠️ Il punto che rende questo giro diverso dai precedenti: le ancore erano state tarate sullo sprite nuovo, che ora non c'è più — rifare la stessa misura sull'arte vecchia era obbligatorio, non un di più. Misurati a pixel i punti-pelle della mano su idle più le 4 pose di walk_e: cadono nelle righe 15..24 dello sheet, colonne 18-20, con due pixel isolati di highlight (riga 15 nella posa f3, riga 24 nella posa f2) che sporgevano di mezzo pixel dal riquadro 7×9 centrato in (4,−4). Ancora spostata a (2,−4.5): il riquadro copre ora esattamente le righe 15..24 e in x si stringe di due pixel verso il fianco, che è letteralmente ciò che chiedeva il KO. s, se, ne, n invariate — il KO parla solo di est/ovest, e s Ivan l'aveva già confermata. ⚠️ NON MISURATO in modo conclusivo: la prova visiva. Il provino è stato rieseguito dall'orchestratore e i frame guardati, ma di notte la pozza di luce della lanterna slava la zona proprio dove andrebbe guardata la mano: a zoom nativo il criterio «si vede ancora un pelo» non è giudicabile a occhio, e ciò che regge questo giro è la misura sui pixel, non l'immagine. È un limite dello strumento, non un dettaglio da nascondere. File: assets/sprites/player_sheet.png, Player.gd (solo LANTERNA_ANCORE e i suoi commenti). - [change] SU-232 (KO Ivan) — le 9 vite gatto risalgono in alto e la barra del livello si allarga fino a combaciarci. Il KO era un cambio di idea, non una correzione: «cambio idea e proviamo a mettere in alto 9 vite gatto e sempre in alto ma sotto il livello (facciamo anche una prova di allargare la barra per farla larga quanto le 9 vite». Il blocco vite+barra torna quindi in alto (veniva dal fondo schermo) e
XP_BAR_W smette di essere una const fissa a 220px: ora è assegnata a runtime in _setup_progression() come _lives_row_width() = 9×32 + 8×4 = 320px, cioè esattamente la fila delle nove teste — le due cose leggono come un blocco unico invece che come due elementi impilati a caso. LIVES_BOTTOM_MARGIN è diventata LIVES_TOP_MARGIN (stesso valore, semantica coerente col nuovo ancoraggio alla safe-area superiore, che è il lato dove il notch conta davvero). 📌 Un effetto collaterale trovato e chiuso prima che diventasse un KO: _big_notif_stack_top() riusava una quota fissa calcolata quando il blocco stava in fondo; riportandolo in alto quella quota sarebbe caduta dentro il blocco stesso, facendo partire le notifiche grosse sopra le teste dei gatti. Ora parte dal fondo reale del blocco. ⚠️ Una ambiguità vera del KO, lasciata decidere a Ivan con due figure invece che con un secondo giro su device: «in alto ... ma sotto il livello» si può leggere in due modi. Variante A (consegnata): vite sopra, barra livello sotto. Variante B: barra livello sopra, le nove vite sotto — lettura più letterale, e spiega perché il KO dice «cambio idea» e «ma», visto che nel layout precedente le vite erano *già* sopra la barra. Prodotte entrambe come screenshot (su232_hud_9_vite.png, su232_hud_3_vite_perse.png, su232_variante_B_livello_sopra.png), guardate una per una: in tutte e due le vite si svuotano da destra, le larghezze combaciano e non c'è sovrapposizione col pannello stat a sinistra né col meteo/punteggio a destra. Se Ivan sceglie la B è lo scambio di due corpi-funzione (_lives_row_top_y() ↔ _xp_bar_top_y()): nient'altro, perché _big_notif_stack_top() e _hud_blocker_rects() dipendono già dai rect reali e non dall'ordine fisico. ⚠️ NON MISURATO: la safe-area su iPhone vero — nel codice il blocco usa _safe_insets(), ma su device non l'ha visto nessuno, ed è il lato dello schermo dove sbagliare si nota di più. Il comportamento delle vite (svuotamento, game over a zero) non è stato toccato: era già approvato. File: HUD.gd. - [change] SU-254 — il gatto ricompare quando lo LANCI, non quando lo raccogli, e la nuvoletta non è più esclusiva del GATTO MAGNETICO. Richiesta diretta di Ivan: «il respawn del gatto se attualmente è fatto quando viene raccolto deve passare a quando lo lancio». Il perno non è togliere una chiamata, è cambiare cosa si conta: la città non deve avere
CAT_TARGET gatti *in strada*, ma CAT_TARGET gatti in giro = in strada + in braccio ai barboni (nuova _cats_in_arms(): GameState.has_cat per il barbone locale, _net_puppet_cat letto con get() dai puppet per i remoti — Player.gd non è stato toccato, era in mano a un altro lotto). Così alla raccolta il totale non cambia (7 in strada + 1 in braccio = 8) e nessuno rimpiazza; al lancio il totale scende e il rimpiazzo parte. ⚠️ La strada ingenua non avrebbe funzionato: togliere request_cat_refill() da _use_cat() lasciava in piedi la rete di sicurezza dei 2 secondi in _process, che avrebbe rifatto il difetto identico due secondi dopo — è il motivo per cui il conteggio è l'unico punto giusto in cui intervenire. Il rimpiazzo lo innesca ora GameState.has_cat_changed(false), quindi consegna al gattile, arresto e collasso sono coperti per costruzione e non come casi particolari. 📌 Uno scostamento voluto e documentato: la chiamata in _use_cat() non è stata rimossa ma rinominata in note_cat_pickup() → _ensure_cat_count(pickup_only = true), perché quel frame è l'unico istante in cui il nodo raccolto esiste ancora ed è lì che SU-157 annota il punto di raccolta per bruciarlo 60s; toglierla avrebbe fatto tornare «a volte il gatto ricompare esattamente dove l'avevi raccolto». La nuvoletta cambia significato: prima era la firma della carta GATTO MAGNETICO, ora spetta a ogni rimpiazzo che finisce in inquadratura di qualcuno — è la richiesta di Ivan per il caso futuro «8 giocatori e nessun punto davvero libero», dove il gatto dovrà poter comparire anche vicino a un barbone. I gatti dello spawn iniziale della città restano senza pouf (era la regressione già corretta in SU-151). In MP il bool viaggia in _cl_cat_spawned, così ogni peer decide sul proprio schermo invece di ereditare la decisione dell'host. Verificato con 12 asserzioni su città vera, 0 errori, rieseguite dall'orchestratore: spawn iniziale 0 nuvolette; senza magnete 8 → raccolta 7 → ancora 7 dopo 2,6s → lancio 8, rimpiazzo a 821px fuori inquadratura e 0 pouf; con la carta 8 → 7 → 7 → lancio 8, rimpiazzo a 75px, a video, 1 pouf. ⚠️ NON MISURATO: il multiplayer reale a 2 peer (non collaudabile su questo Mac) — il bool nell'RPC, il pouf visto dal client e la finestra di grazia di 0,7s contro la corsa col flag del puppet. ⚠️ Limite noto: se a lanciare è un giocatore remoto, l'host non riceve has_cat_changed e il rimpiazzo arriva dalla rete dei 2s, quindi con un ritardo ≤2s. File: World.gd, Interactable.gd, nuovo scripts/tools/check_su254_gatto.gd. - [change] SU-198 (KO Ivan) — le ombre tornano esattamente com'erano prima dell'opt-out, e la pensilina diventa un ticket suo. Il KO era una richiesta di ritirata, non di correzione: «reverta a come erano prima dell'ultima modifica le ombre poi ti dico cosa fare in un nuovo task». Revertato
WorldGenerator.gd allo stato immediatamente precedente a b8d8965 — quello che aveva reso le ombre opt-out (una passata unica a fine generazione che le dava a tutto ciò che è disegnato e sta in piedi). Si torna quindi al sistema opt-in: chiesa, fontana e rifugio sono di nuovo senza ombra, ed è l'esito atteso del revert, non una regressione — erano proprio i punti che l'opt-out aveva chiuso. 📌 La prova che vale più del provino: il file è stato verificato identico byte a byte a b8d8965^, e git log b8d8965..HEAD conferma che nessun commit successivo lo aveva toccato — quindi il revert diretto non ha potuto seppellire lavoro di altri, che era l'unico rischio reale dell'operazione. Compile-check verde ([CHECKF] OK, 0 falliti) e città rigenerata e guardata: 37 footprint, 154 ombre, navmesh a 214 poligoni, palazzi/chiesa/food truck/lampioni/NPC tutti al loro posto. I due provini shot_su198_censimento.gd e shot_su198_ko5.gd restano in scripts/tools/ anche se ora non girano (usano ombra_modo/ombra_copre, API nate col commit revertato): serviranno al ticket nuovo che Ivan ha annunciato. 📌 Seconda metà del KO, fatta: «si genera un task per generare la nuova pensilina come da mia richiesta, mettendolo già in sprint attuale» → creato SU-255 nello Sprint 6, con la motivazione tecnica scritta dentro (lo sprite ha una piastra a terra cotta nel disegno, quindi qualunque ombra proietta anche il pavimento: è un difetto dell'arte, non del codice, ed era l'unico punto di SU-198 non chiudibile senza rigenerare). File: WorldGenerator.gd.
Sprint, secondo giro2026-08-03
- [docs] Nuovo design
OPUS_BRIEFS/I1_IMPATTO_E_RICOMPENSE.md ed epic SU-244 «I1 impatto/ricompense»: il motore di dipendenza che il gioco non ha. Nasce da un'osservazione di Ivan («mi sa che manca l'impatto vero nel gioco»), dopo una conversazione su come sono nati Vampire Survivors e Balatro. Progettato con la skill progetta-feature, a domande singole: nessun file di gioco è stato toccato. La diagnosi è misurata, non intuita, ed è il contenuto che regge tutto il resto: nel progetto screen shake, hitstop ed escalation sonora sono a ZERO occorrenze — pitch_scale compare due volte in tutto il codice, entrambe sulla *musica*, quindi nessun effetto sonoro cambia mai di tono e per trenta minuti ogni ricompensa suona identica alla precedente. Non manca il feedback: manca la progressione del feedback. Il tessuto invece c'è già e va riusato, non riscritto — Player.spawn_stat_fx() con 13 wrapper e 32 chiamate in 9 file, e soprattutto Pickup.gd ha il magnetismo già scritto e mai usato, col commento «per monete XP, cibo, birra»: era nato per questo. ⚠️ Una promessa che il gioco fa e non mantiene, trovata per strada: la schermata della Baracca dice al giocatore «Le lattine si raccolgono per strada e si spendono qui», ma MetaProgress rimanda la raccolta a «un altro ticket» che non è mai arrivato. Il design in cinque strati: la scala (serie di ricompense che alza tono, dimensione e scossa, riusando size_mul che spawn_stat_fx già accetta), le monete che cadono con l'XP dentro (stessa quantità, consegna diversa — così la taratura di R2 non va rifatta), la carta CALAMITA che sostituisce SESTO SENSO con uno scambio 1:1 sulla stessa meccanica di raggio crescente, il forziere (il bidone chiuso: la metafora è nativa, per un barbone un bidone chiuso *è* un forziere) e la banca, che era già in gioco come landmark del CENTRO e finora era puro arredo. Due decisioni di design che vale la pena aver scritto: il bidone d'oro è garantito al raggiungimento della soglia e non tirato a sorte — se il deposito può non pagare si è costruito il gioco d'azzardo dentro un gioco sulla povertà, e chi salta i pasti per niente smette; e la quasi-vincita si fa solo onesta (costruzione sempre identica, esito variabile), mai mostrando un premio per poi toglierlo. Sul riferimento alle slot machine chiesto da Ivan: se ne prende la psicologia, non l'iconografia — niente rulli, fiches o «JACKPOT», che alzerebbero la fascia d'età su App Store e Play. 📌 Multiplayer parcheggiato di proposito (sezione I1.6): Ivan ha detto che deve girare diversamente dal single player, quindi le soluzioni MP nel brief sono default provvisori e i ticket implementano il solo single player con hook conservativi. 📌 Resta aperto, segnalato da Ivan: un brainstorming dedicato alla banca. Ticket creati e lasciati nel backlog (la prontezza la decide Ivan spostandoli nello sprint): SU-245 la scala · SU-246 CALAMITA · SU-247 le monete · SU-248 il forziere · SU-249 la banca, più quattro ticket di generazione immagini (SU-250 icona CALAMITA, SU-251 monete e lattina, SU-252 bidone chiuso e d'oro, SU-253 arcobaleno) collegati come bloccanti, perché il «Fatto» sulle immagini lo mette solo Ivan. - [fix] SU-230 (KO Ivan) — la lanterna non si aggancia alla mano ma al bordo del corpo, ed è il motivo per cui camminando si staccava. ⚠️ Cos'era stato capito male il giro scorso: il ticket nominava solo il sud, quindi è stato cambiato solo l'offset di
"s" e guardata solo quella direzione, da fermo. Le altre quattro non sono mai state riverificate — né contro lo sprite nuovo di SU-203, né durante il movimento. Causa radice, misurata e non intuita: LANTERNA_ANCORE è un dizionario di offset fissi, ma nelle quattro pose del ciclo di camminata la mano non solo si sposta, in metà delle pose sparisce (il braccio va dietro il busto e si scurisce). Un offset fisso non può inseguire un bersaglio che a volte non c'è: da fermo sembrava giusto, camminando si staccava — che è esattamente il sintomo descritto nel KO («nei movimenti a sinistra e destra la lanterna fluttua in aria perché il barbone ha le mani vicino al corpo»). Il rimedio è cambiare bersaglio: si misura il bordo della sagoma (primo pixel non trasparente) invece della mano, perché resta stabile entro 1-2 px su idle più tutte e quattro le pose, mentre la mano no. È la strada «b» fra le due proposte da Ivan nel KO: la lanterna (7×9 px, centrata sull'ancora) resta addosso al fianco e la sua metà bassa copre la mano quando è visibile, invece di rincorrerla e sbagliare quando non lo è. I numeri: verso est la vecchia ancora (9,−6) cadeva 5-6 px oltre il bordo del corpo — quello era il KO; sud-est ~2 px, nord-est ~3 px (ma dietro al corpo, quindi si notava meno), nord quasi giusta. "s" resta invariata: era già sul bordo, Ivan l'aveva confermata funzionante, e la rimisura lo conferma. Il diff tocca solo il dizionario e il suo commento: _aggiorna_lanterna(), FLIP_MAP e posizione_lanterna() non sono stati sfiorati, quindi World.gd continua a centrare la pozza di luce esattamente come prima. Verificato guardando tre fogli di contatto, tutti di notte: otto direzioni da fermo, le quattro pose del ciclo per est e ovest (nessun distacco in nessuna), e sud con e senza gatto — lanterna a sinistra, gatto a destra, nessuna sovrapposizione. 📌 Una nota metodologica onesta dell'agente: il *primo* giro di provini mostrava un distacco netto, ma era un artefatto del suo script (la camera veniva riposizionata solo agli scatti, mentre World._aggiorna_pozze_lampioni legge la trasformazione camera-schermo ogni frame). Corretto aggiornando la camera a ogni frame e rifatto il giro — è la stessa categoria di errore del giro precedente, «guardato male, non guardato abbastanza», e valeva la pena scriverlo. 📌 Promemoria, non azione: player_cat_sheet.png è ancora arte vecchia, non toccata da SU-203; se un giorno verrà rigenerata anche quella, l'ancora del sud va riverificata. 📌 Segnalazione fuori scope, non toccata: _aggiorna_lanterna() gira in _physics_process prima di _handle_movement(), che aggiorna _last_dir — nel frame esatto di un cambio direzione la lanterna legge per un istante la direzione precedente. Invisibile in partita a 60 fps, visibile solo perché il provino scatta a frame esatto; riordinare _physics_process era un cambio più largo del KO. File: Player.gd, nuovo scripts/tools/shot_su230_ko1_lanterna.gd. - [feat] SU-153 (KO Ivan) + SU-188 — generati in temp: la versione donna del barbone e il pilota della camminata, con 5 chiamate Codex e 1 solo retry. Due ticket di generazione, quindi il «Fatto» lo mette solo Ivan: quelle immagini sono una proposta, non una consegna. ⚠️ Lo scopo di SU-153 l'ha riscritto Ivan a metà sprint: non più le 8 skin («KO vanno rigenerate»), ma «per ora crea solo una versione donna del barbone, con stesso stile e sempre pattern 25 sprite di movimento, poi vediamo». E SU-188 si è sbloccato con la sua risposta all'altra domanda ferma da giorni: «accetto l'accessorio specchiato», cioè i ~30 archetipi «sporchi» non si rigenerano per il problema dello specchio. La donna (
sprites_raw/PEOPLE/temp/su153_donna/): 1254×1254, magenta, griglia 5×5, generata allegando come riferimento il barbone maschio approvato in SU-200 — stessa palette terrosa, stesso spessore di contorno, stesse proporzioni; si distingue per berretto rosso-ruggine, capelli e taglio del cappotto. 📌 Una tecnica nuova per questo progetto, e ha funzionato al primo colpo (0 retry): oltre al riferimento è stata allegata a Codex una seconda immagine, un «righello» con due linee guida su testa e piedi misurate sul riferimento. Serve perché un vincolo di altezza scritto solo a parole viene ignorato — è il difetto che rese «tozzo» il giullare in SU-201. Risultato: la figura riempie 32 px su 32 in tutte e 25 le celle, scarto zero rispetto al riferimento, misurato con la pipeline di import vera e non a occhio. Il pilota di SU-188 (temp/walk/pilot_v3/): rigenerati arch_police_male_v1 e arch_tourist_v1; il terzo archetipo del pilota, il barbone, non è stato rigenerato — è già quello approvato in SU-200 e ripassarlo in Codex sarebbe stata quota buttata: è stato solo ripassato nella verifica automatica (5/5 OK, invariato). Il difetto che il ticket doveva chiudere — la colonna 4 che duplica la colonna 2, cioè la «camminata zoppa» — non è più misurato come ROTTO su nessuna delle 15 righe controllate (il poliziotto ne aveva una a 12,4%, ora è a 25,7%). Identità preservata su entrambi, verificata guardando i confronti prima/dopo alla stessa scala: divisa, distintivo e cinturone del poliziotto, berretto, occhiali da sole, camicia a quadri e zaino del turista. ⚠️ Un difetto vero trovato e corretto in corsa: al primo tentativo il poliziotto è uscito con la riga SUD-OVEST disegnata più grande delle altre, al punto da fondersi con la riga NORD-EST senza gap di magenta — find_islands_1d ne rilevava 4 invece di 5, e l'import vero si sarebbe rotto. Corretto con un retry mirato («non zoomare quella riga, mantieni il gap»); il file difettoso è stato rimosso dalla consegna e ne resta solo l'evidenza in TMP/. 📌 Tre difetti residui che Ivan deve guardare prima di approvare: l'alternanza è presente ma poco leggibile di spalle sulla donna (riga NORD) e sul turista (NORD), e sul poliziotto anche in profilo (EST) perché la divisa blu uniforme non dà contrasto fra le due gambe. Sono le stesse categorie di vista che il progetto ha già documentato come strutturalmente al limite della misurabilità, non un difetto di questi personaggi. Nessun file di gioco toccato: niente import, niente .tres, niente assets/; gli originali di riferimento risultano invariati in git e tutto l'output vive in cartelle escluse da git, quindi non entra in questo commit. File: nuovi sprites_raw/PEOPLE/temp/su153_donna/su153_arch_player_female_v1.png, temp/walk/pilot_v3/arch_police_male_v1_walkfix_v3.png, arch_tourist_v1_walkfix.png (tutti fuori da git); anteprime in TMP/lottoD/. - [fix] SU-198 (5° giro, KO Ivan) — le ombre diventano OPT-OUT: nessun oggetto può più essere dimenticato. ⚠️ Cos'era stato capito male le due volte precedenti: al 4° giro l'ipotesi era «c'è una sola chiamata, generalizziamola», ed è stato fatto — ma sono comunque rimasti fuori chiesa, fontana e rifugio, perché il sistema restava opt-in: l'ombra arrivava solo se qualcuno, in quel punto di piazzamento, si ricordava di chiederla. Finché è opt-in ci sarà sempre un oggetto che nasce da una strada nuova e resta scoperto: è successo due volte di fila. Ora a fine
generate() c'è una passata unica (_ombre_passata_finale) che percorre l'albero e attacca l'ombra a tutto ciò che è disegnato e sta in piedi; per restarne fuori bisogna dirlo, con tre regole corte: suolo (z_index < -5), troppo basso (OMBRA_ARREDO_ALT_MIN, che tiene fuori il cestino 12×16), oppure meta ombra_no motivata sul posto — insegne al neon (appese al muro, il piede è a mezz'aria), pensilina, cabine dei bagni (ombra unica per la fila). Le sette chiamate sparse sono sparite; ne resta una dichiarata. Un oggetto composto (rifugio, camioncino, fontana) proietta una ombra sola, quella del pezzo che tocca terra più in basso, così il cartello OPEN/CLOSED sul tetto del rifugio non ne fa una sua a mezz'aria. L'asimmetria che rende l'opt-out la scelta giusta: un'esclusione dimenticata si vede subito (compare un'ombra assurda), una chiamata dimenticata no. ⚠️ La causa unica dei quattro «resta un pezzo chiaro» (fabbrica, palazzo fatiscente, banca, vineria) non era una chiamata mancante: _ombra_base_rows() tagliava le righe in fondo alla texture meno coperte del 72% della più piena, e ci ancorava l'ombra. Misurato sugli sprite veri: landmark_bank perdeva 13 righe su 104 — la scalinata è larga 51 px contro i 91 del corpo, quindi finiva sotto quota ed era scambiata per un vaso di contorno —, landmark_factory 5, liquor 4, building_slums_v2 4. La cura non è un'altra soglia ma smettere di tagliare: ogni colonna viene riempita dal suo pixel più basso fino alla riga di terra se il salto sta entro 16 px (OMBRA_BASE_CHIUSURA — ci stanno scalinata della banca 13, ruote dei camioncini 12-14, gambe della panchina 13, vasca della fontana 12; restano fuori i pezzi davvero sospesi, testa del lampione 36 e tettoia 67, che un'ombra vera non riempie fino a terra). I 45°: la pendenza della linea di terra si misura sulla sagoma con una regressione ai minimi quadrati sull'inviluppo inferiore, invece di essere una costante per oggetto — panchina −0,217 (≈12°), gelati −0,097, hotdog −0,042, palazzi ≈0 quindi identici a prima; diventa l'asse X della proiezione, che ora si scrive per assi (Transform2D) perché lo skew da solo inclina l'asse Y ma non l'asse X. Non dipende dall'ora: vale «in tutte le condizioni», come chiedeva il KO. Sparisce anche l'ultimo caso speciale rimasto: il dithering del bordo si dosa su scala e spessore misurato, e su un palo largo 2 px si spegne da solo. Costo, misurato su sei semi: 158-179 ombre (media ~168) contro le 154 del giro scorso — l'opt-out aggiunge circa il 9%, non moltiplica —, passata 54 ms alla prima città (cache delle sagome fredda) e 4-6 ms dalla seconda in poi; mezzogiorno 0 punti in ombra come da progetto, e di notte le facce si ripristinano tutte (46/46 … 71/71). Verificato guardando i fogli di contatto, undici soggetti × tre posizioni del sole: fabbrica, banca, palazzo fatiscente e vineria hanno l'ombra a filo della base; chiesa, fontana e rifugio ce l'hanno e prima no; i bagni l'hanno larga quanto le tre cabine, con bordo netto, e le cabine stesse sono visibilmente più scure (OMBRA_FACCIATA_BAGNI = 0.60). 📌 Il palazzo fatiscente esiste solo nel distretto slums: con la generazione di default non compare mai, e va forzato per vederlo. ⚠️ Punto 11 del KO NON FATTO, di proposito: la pensilina chiede prima uno sprite rigenerato senza la piastra a terra (com'è ora, proiettarla darebbe l'ombra del pavimento disegnato dentro lo sprite) e poi un'ombra che tenga conto del lato sinistro e del lato dietro e che a mezzogiorno copra tutto lo sprite — cioè per lei mezzogiorno non è «niente ombra» come per gli altri. È una generazione di immagini, che su questo progetto vuole il suo ticket con l'approvazione di Ivan: niente è stato generato. ⚠️ NON misurato: il costo GPU su device, che resta il rischio aperto di questo ticket. 📌 Trappola trovata nei provini, non in partita: rigenerando due città nello stesso processo, _detect_district_for_rect pesca ancora le DistrictZone della città precedente (solo queue_free(), vive fino a fine frame). Il gioco non lo fa mai, ma è lì. File: WorldGenerator.gd, nuovi scripts/tools/shot_su198_censimento.gd e shot_su198_ko5.gd. - [feat] SU-232 — nove vite come i gatti: ogni colpo e ogni arresto ne toglie una, a zero la run finisce. Le vite vivono in
GameState (cat_lives, CAT_LIVES_MAX = 9) esattamente come wanted_level, ed è la scelta che rende il ticket gratis in multigiocatore: stato locale per giocatore, quindi «vite per-player» non richiede una riga di rete. L'unico punto d'ingresso è GameState.lose_life(causa), chiamata dove il colpo viene già applicato sul peer del colpito — i tre proiettili ostili (gatto della gattara, bottiglia del tossico, birillo del giullare), la rapina della ronda, e per l'arresto un aggancio unico a EventBus.player_arrested, che è emesso sul peer dell'arrestato sia in SP (PoliceOfficer._do_arrest) sia in MP (World._cl_arrested): zero RPC nuovi. Scartate due alternative: un sistema HP sul Player (avrebbe duplicato stato già replicato e chiesto rete) e far decidere le vite all'host (inutile: l'host decide già il colpo, e i relay ci sono). ⚠️ Il dettaglio che tiene fuori il tutorial: lose_life() esce subito se run_active è falso — stesso cancello di timer, XP e retata — quindi nel tutorial le vite non si muovono. Un aggancio differito e il perché: GameState è il primo autoload di project.godot, quindi nel suo _ready() /root/EventBus non esiste ancora; la connessione passa da call_deferred. Una notifica sola, non nove: ogni colpo ha già la sua notifica e la fila di teste che si svuota, quindi si parla solo sull'ultima vita («🐈 ULTIMA VITA! Il gatto ti guarda male»). A zero, trigger_game_over con la causa nuova FINE_VITE e un finale in tono col gioco — «Nove vite bruciate: il gatto cosmico ti ha ritirato la tessera» — dentro il flusso esistente, non un flusso parallelo. La carta NOVE VITE che salva una run finita a vite zero ne restituisce CAT_LIVES_REVIVE = 3 e mai meno di quelle che si hanno già: con 1 il colpo dopo richiuderebbe la run nello stesso secondo, con 9 sarebbe un azzeramento invece di una seconda occasione. HUD: fila di nove teste di gatto 16×16 sopra la barra di livello, che scende in fondo allo schermo rispettando _safe_insets(). Verificato guardando i provini: nove piene a inizio run, sei piene e tre vuote dopo tre colpi, con la barra livello sotto. 📌 Due scelte di perimetro da confermare, dichiarate invece che nascoste: il borseggio del tossico a contatto non toglie vite (è un borseggio, non un colpo — le sue bottiglie sì) e il gatto lanciato da un altro giocatore in MP nemmeno (non è un NPC ostile). NON misurato: il multigiocatore reale e la safe-area su iPhone vero. File: GameState.gd, EventBus.gd, HUD.gd, World.gd, CatProjectile.gd, BottleProjectile.gd, PinProjectile.gd, ModularNPC.gd, nuovi assets/sprites/ui/vita_gatto_{piena,vuota}.png, tools/import_icone_vite_super.py, scripts/tools/shot_su232_vite.gd, check_su232_vite.gd. - [change] SU-233 — via la barra della super dall'HUD, al suo posto un fulmine sopra la testa che si riempie mentre carichi. Lo slot HUD della super se ne va del tutto da
SuperPower.gd (−221 righe nette), e non è un caso che tocchi proprio a lui: occupava la fascia basso-centro che ora serve alle nove vite. Il feedback della carica diventa due Sprite2D figli del Player — stessa tecnica della lanterna (SU-211) e della nuvoletta (SU-206), quindi nessun frame nuovo nello sheet e vale per tutte le skin: una sagoma scurita sotto, e sopra la stessa texture ritagliata a runtime con region_rect sulla fetta già caricata, che cresce dal basso. ⚠️ Lo shader è stato scartato apposta, ed è la scelta più importante del lotto: un ShaderMaterial che non compila fa ripiegare Godot sul materiale di default in silenzio, e il provino sembrerebbe giusto — su questo stesso progetto è già successo. Un ritaglio non può fallire di nascosto. Il riempimento è arrotondato al pixel con floorf, così cresce a scalini come si conviene alla pixel art invece di sfumare su mezzi pixel. Il posto della chiamata non è arbitrario: _aggiorna_simbolo_super() sta prima degli early-out di stordimento/sonno/busking, perché SuperPower è un autoload e la carica avanza comunque — fermare il simbolo lì mostrerebbe una carica falsa; e sta dopo gli early-return di is_puppet e is_ghost, verificato riga per riga, altrimenti in multigiocatore la tua carica comparirebbe sopra la testa degli altri. Nessun RPC nuovo, indicatore puramente locale come chiedeva il ticket. Verificato guardando i provini allo zoom di gioco vero: a metà carica il fulmine è giallo per metà e scuro sopra, a carica piena è tutto giallo, e al rilascio anticipato sparisce. Logica e tempi di carica, attivazione e SuperCeremony non sono stati toccati di una riga. ⚠️ Due cose che il ticket ha causato e che vanno decise: sparendo lo slot non c'è più nessun display permanente di «PRONTO / in ricarica» né il promemoria «✨ MATURO — GIULLARE: SCEGLI» (resta solo la notifica al momento della maturazione); e il tutorial adesso mente — TutorialManager.gd:517 e :758 dicono ancora «guarda la barra in basso: oro = pronto, verde = in azione, blu = in ricarica». Non toccati di proposito: i testi stanno nei CSV ui_gioco/ui_mondo in cinque lingue, serve un ticket dedicato. File: SuperPower.gd, Player.gd, nuovi assets/sprites/ui/super_simbolo.png, scripts/tools/shot_su233_super.gd. - [change] SU-203 — il barbone rigenerato entra in gioco, e la camminata smette di zoppicare: 0 righe rotte su 5. Sbloccato dall'approvazione di SU-200 (il «Fatto» di Ivan *è* l'approvazione). L'import porta
sprites_raw/PEOPLE/temp/giocabili/su200_arch_player_v1.png (1254×1254, magenta, griglia 5×5) dentro assets/sprites/player_sheet.png (160×160, celle 32×32) passando dalla rimappatura righe S/N/E/SW/NE → E/SE/S/NE/N con mirror SW→SE, che è il punto in cui questo import si sbaglia — non l'import in sé. ⚠️ La trappola che avrebbe reso il lavoro un no-op invisibile: di script di import ce ne sono due, e quello che il ticket nominava (files/homeless_city/tools/import_people_sprites.py, che esiste davvero — la mia indicazione «non esiste» era sbagliata, cercavo nella tools/ di root) scrive in assets/sprites/npc/bodies/player_new_sheet.png, che non è l'asset letto da Player.tscn: usarlo avrebbe prodotto un import riuscito, un compile-check verde e un barbone identico a prima in partita. La rimappatura vera sta in build_sheet() di KIT_CHATGPT/strumenti/import_chatgpt_sprites.py, richiamata con i path espliciti invece di duplicarne la logica. Nessuna modifica a player_frames.tres: le region AtlasTexture erano già compatibili cella per cella, quindi la sostituzione è del solo PNG. Verificato dall'orchestratore, non sulla parola: lo script di controllo confronta la colonna 2 con la colonna 4 di ogni riga — è lo stesso controllo che aveva trovato il difetto originale, quindi è quello che prova che è sparito — e dà 0/5 righe rotte, con differenze di contenuto dal 34,7% (nord) al 61,2% (sud-est) contro una soglia di rottura del 15%; in engine l'indice di frame va 1→3→1 mentre il barbone cammina davvero, con input veri e non frame forzati. Direzioni controllate una per una su tutte e 8 (SW→walk_se con flip_h=true, NE→walk_ne senza flip): un errore di rimappatura righe si vede solo se lo si cerca apposta, e non c'è. Overlay dei prop: gatto e kazoo restano attaccati alle mani — sono sheet separati (player_cat_frames.tres, homelessmusic_sheet.png), quindi per costruzione questo import non può regredirli, ma sono stati comunque guardati. ⚠️ La lanterna invece si stacca dalla mano col nuovo sprite, anche a sud: non è un difetto di questo ticket ma il campo d'azione di SU-230, che va perciò rilavorato sopra queste sprite e non più solo sulle direzioni est/ovest del suo KO. 📌 Resta indietro di proposito: assets/sprites/npc/bodies/player_new_sheet.png conserva l'arte vecchia — lo usa solo il visore NPC di sviluppo, non il gioco; da rigenerare il giorno in cui SU-83 lo riattiva. NON misurato: il multigiocatore (i puppet leggono la stessa risorsa, nessun cambio di protocollo, ma su questo Mac non è collaudabile) e il collaudo completo run_check/TestBot, rimandato alla fine dello sprint perché WorldGenerator.gd è in lavorazione da un altro lotto proprio adesso. File: assets/sprites/player_sheet.png, nuovo scripts/tools/shot_su203_barbone_nuovo.gd. - [test] Collaudo di fine sprint: VERDE, nessuna regressione sui cinque commit.
run_check.sh force → 141 script, 39 scene, 0 falliti. ⚠️ SHADER ERROR cercato in 5 run su 5 e trovato in 0, di cui due finestrate con GPU vera (Metal 4.0, Forward Mobile, Apple M2 Pro): era il rischio numero uno del giro, perché SU-198 riscrive proprio il codice che costruisce i ShaderMaterial delle ombre, e un materiale caduto sul default sarebbe passato inosservato — su questo stesso ticket è già successo una volta. Run col TestBot 120 s: blocchi_totali = 34 su ogni dump, mai 0 — il rischio esplicito del brief, dopo la riscrittura pesante di WorldGenerator.gd, non si ripresenta —, orphans = 0 su tutti e 24 i dump, fine run pulita. Censimento ombre su sei semi: 161, 174, 166, 158, 169, 179, tutti dentro il range; mezzogiorno 0/4900 punti in ombra su 6 semi su 6; di notte 0 punti in ombra e facce ripristinate 1:1, nessuna rimasta scura. Banco delle 9 vite: 13 controlli su 13, compreso il caso che il ticket dichiarava a rischio — nel tutorial (run_active = false) le vite non si muovono e nessun segnale parte. 📌 Chiusa una domanda rimasta aperta dal giro precedente: SOUP_VAN e GATTILE non mancavano per sfortuna di seme. GATTILE è un edificio garantito del solo quartiere GATTOPOLI, e SOUP_VAN non è generato dal WorldGenerator affatto — è uno spawn a runtime legato all'evento casuale charity_van. Forzandoli, entrambi compaiono al primo seme provato; verificata la comparsa, non il press-test fisico. Non verificato, e dichiarato: il multigiocatore (su questo Mac eos_credentials.cfg manda NetworkManager sul ramo EOS e run_mp.sh non collauda più il percorso di rete — disallineamento d'ambiente noto dal 31/07, non una regressione); il costo GPU su device; il press-test su SOUP_VAN e GATTILE; e la causa per cui il palazzo «fatiscente» non compare in nessuno dei sei semi del censimento (ipotesi: il quartiere di default pesca dal pool downtown, non da slums — dichiarata, non confermata riga per riga). Dettaglio completo in TESTLOG.md. - [fix] I sette script di provino nati oggi entrano in git col loro
.uid. Stessa cura del giro precedente: in scripts/tools/ c'erano 72 .uid per 79 .gd, e i sette mancanti erano esattamente quelli di questo sprint — gli agenti lanciano Godot con HOME/SU_TMP separate, quindi l'identità non viene mai scritta nel progetto vero. Generati con un import dell'editor, verificando prima e dopo che export_presets.cfg non venisse riscritto (l'editor lo riscrive da memoria alla chiusura, ed è già costato un giro a questo repo): stesso shasum, file identico, e l'import non ha toccato nient'altro. File: nuovi check_su232_vite.gd.uid, shot_su198_censimento.gd.uid, shot_su198_ko5.gd.uid, shot_su203_barbone_nuovo.gd.uid, shot_su230_ko1_lanterna.gd.uid, shot_su232_vite.gd.uid, shot_su233_super.gd.uid.
RIENTRI ATTESI: SU-153, SU-188, SU-198, SU-203, SU-230, SU-232, SU-233 — le sette chiavi messe In revisione in questo giro. Al prossimo sprint, prima dei lotti: quante sono tornate in «Da fare» e con quale KO. È l'unica metrica di qualità dello sprint che non sia vera per costruzione. *(Giro precedente: delle undici chiavi attese — SU-198, SU-229, SU-230, SU-231, SU-234, SU-235, SU-236, SU-237, SU-238, SU-239, SU-243 — sono rientrate SU-198 e SU-230, tasso di rientro 2 su 11; le altre nove Ivan le ha messe Fatto o lasciate In revisione.)*
Sprint2026-08-03
- [fix] SU-243 — dentro i passanti non ci si passa più, e la causa non era quella scritta nel ticket: il barbone non li urtava proprio. Il ticket puntava allo scansamento logico (
_nav_avoid_player) ed è una traccia parziale; la causa è fisica, ed è una riga che nel .tscn semplicemente non c'è. Il nodo Player di Player.tscn non ha alcun override di layer/mask, quindi resta su collision_layer = 1, collision_mask = 1, mentre i passanti stanno su layer = 2: il barbone non urta nessun passante, mai. L'affermazione del ticket «Player.tscn ha collision_mask = 2» era riferita a BegArea, che è un Area2D figlio, non al corpo — verificato riga per riga. Il contrario invece funziona (la mask dei passanti include il layer del barbone), ma la depenetrazione la esegue move_and_slide(), quindi avviene solo nei frame in cui il passante si muove davvero. Ecco l'«alcuni sì, altri no»: chi cammina o scappa si stacca, chi è fermo, in attesa o allarmato viene attraversato da parte a parte. Rimedio: la separazione la fa il passante, non il barbone, e sta prima di ogni ramo della macchina a stati — perché gli stati attraversabili sono proprio quelli che escono in anticipo senza muoversi. ⚠️ Due trappole tecniche che valeva la pena scrivere: move_and_collide non risolve la compenetrazione già profonda (il motore vede i due dischi sovrapposti e torna «viaggio zero», misurato mosso=0.00 hit=true), e non si può togliere il barbone dalla mask perché barbone e muri condividono il layer 1. Soluzione: PhysicsServer2D.body_test_motion con exclude_bodies = il solo barbone — i muri continuano a fermare, lui no. Scartato invece collision_mask = 3 sul Player: due kinematic si bloccano a vicenda e il barbone resterebbe impantanato nella folla, cioè l'opposto di «girano intorno». L'eccezione del poliziotto è voluta e commentata: chi insegue o sta arrestando NON si scansa e deve poter chiudere fino alla manetta (vale anche per la Squadra Sgombero, che eredita); da stordito invece si toglie di mezzo come chiunque. Misurato prima/dopo con lo stesso banco: fermo in attesa 0,0 → 15,0 px · allarmato 0,0 → 15,0 · poliziotto in avvertimento 0,0 → 15,0 · busking 6,1 → 15,0 · in cammino/fuga/ubriaco già a posto e restano; 5 KO → 0 KO, tutorial 0 KO con run_active=false e zero errori a runtime. Costo: 40 passanti = 32,6 µs per frame fisico, lo 0,20% di un frame a 60 fps — il caso normale è una distanza al quadrato e si esce, la query di movimento scatta solo per chi è davvero compenetrato. ⚠️ Non provato dal vivo il multigiocatore: la spinta è sotto la guardia is_net_puppet (l'NPC si separa solo dove è vero, mai sulle repliche) e non genera un byte di traffico nuovo, ma va confermato in una partita a due. 📌 Nota onesta: lo stato «in chiacchiera» del ticket non esiste nel codice — è stato mappato su «fermo con nuvoletta / allarmato» invece di dichiararlo verificato per finta. File: NPC.gd, PoliceOfficer.gd, RaidOfficer.gd, ModularNPC.gd, nuovo scripts/tools/su243_separazione.gd. - [fix] SU-229 — una pressione di [E] usava due o tre bagni insieme, e non c'entrava la distanza: era una cascata dentro lo stesso frame. La prima diagnosi (due cabine esattamente equidistanti, spareggio sulle distanze) era sbagliata e il fix relativo è stato buttato: non spiegava il «due o tre insieme» segnalato da Ivan, perché tre cabine in fila non possono essere tutte equidistanti — è geometricamente impossibile. La causa vera si legge in tre righe messe in fila:
_process() di ogni interactable controlla Input.is_action_just_pressed("interact"), che è vero per tutto il frame e per chiunque lo chieda; _use() arma il proprio _cooldown_left prima di dispatchare (riga 360, e il bagno rientra fra gli is_shop); e _is_closest_to_player() salta i rivali in cooldown. Quindi il bagno A si usa e si mette in cooldown; il bagno B, che processa subito dopo nello stesso frame, non vede più A come rivale e «vince per abbandono» pur essendo più lontano; C ripete lo stesso contro A e B. Tre gettoni pagati, igiene tre volte, con un solo tocco. Riprodotto prima di scrivere il rimedio, con le cabine a distanze tutte diverse (nessun pareggio): due usi e 10$ spesi invece di 5. Rimedio: una static var _press_consumed_frame, marcata da chi consuma la pressione e controllata prima di guardare le distanze — chi arriva dopo nello stesso frame si ferma senza nemmeno porsi la domanda. _is_closest_to_player() è tornata identica a com'era prima del ticket. Il fix è generico: lo stesso difetto valeva per due panchine o due cestini vicini, non solo per i bagni. In MP ogni peer ha la propria copia della statica, coerente con _player_ref/_player_inside che sono già locali. 📌 Resta fuori scope, preesistente e non toccato: in un pareggio esatto di distanza potrebbero comparire due *prompt* sovrapposti a schermo, anche se poi ne scatta uno solo — la segnalazione era sull'uso multiplo, non sul prompt. File: Interactable.gd, nuovi scripts/tools/su229_bagni_cascata.gd e su229_bagni_tie.gd. - [feat] SU-231 — generate in temp le icone delle 9 vite (testa di gatto piena/vuota) e il simbolo della super. Primo dei due ticket del flusso sprite: si genera in
sprites_raw/UI/temp/, il «Fatto» lo mette solo Ivan — quel Fatto *è* l'approvazione, e finché non arriva i due ticket di implementazione (SU-232 e SU-233) restano bloccati. Tre PNG a 256×256 RGBA: testa di gatto piena (interno rosso, contorno nero), vuota (stessa silhouette, interno trasparente) e un fulmine per la super. La scelta che vale la pena raccontare è la vuota: non è stata rigenerata con Codex ma derivata con PIL dalla piena, perché la piena è uscita già ridotta a una griglia logica 16×16 simmetrica a tre soli colori, e rigenerarla avrebbe rischiato di non combaciare pixel per pixel — che è il vincolo che fa fallire il ticket, visto che le due icone si alternano nello stesso posto e uno scarto di un pixel farebbe «ballare» la fila di 9 a ogni colpo incassato. Verificato dall'orchestratore, non sulla parola: 0 pixel di contorno diversi fra le due e bbox alpha identica (32,32,224,224). Il fulmine non era prescritto dal ticket (diceva solo «una silhouette chiara che si presti al riempimento progressivo»): è stato ancorato a una convenzione che il gioco già usa, la notifica «⚡ Superpotere in ricarica» in SuperPower.gd. Anteprime in TMP/lottoG/, la più utile è la fila di 9 a dimensione HUD vera con piene e vuote mescolate — una barra di vite si giudica in fila, non a icona singola. 📌 Da guardare prima di approvare: i due piccoli denti laterali a metà altezza della testa sono una scelta dell'agente (letti come ciuffi di guancia); se ricordano un pipistrello più di un gatto è un ritocco a costo zero sulla stessa griglia, senza rigenerare. Nessun file di gioco toccato: niente import, niente .tres, niente assets/; sprites_raw/**/temp/ è gitignored, quindi non entra in questo commit. File: nuovi sprites_raw/UI/temp/vita_gatto_piena.png, vita_gatto_vuota.png, super_simbolo.png (fuori da git). - [feat] SU-234 — «MANTIENI JOYPAD NEI MENU»: su iPad il pad può restare nella scelta carte, su telefono resta com'è. SU-224 aveva nascosto il joypad virtuale sopra le finestre modali perché ci finiva addosso — giusto sul telefono, ma su iPad il pad nella scelta carte era comodo, e SU-208/SU-209 permettono proprio di navigare le carte col pad. Ora decide il giocatore: nuova voce nelle OPZIONI, default OFF, quindi chi non tocca niente vede esattamente il comportamento di oggi. La distinzione che rende il lotto non banale:
_modal_open di SU-224 è vero per due motivi diversi — una modale «vera» iscritta al gruppo touch_modal, oppure il ripiego get_tree().paused che raccoglie i pannelli del tutorial. L'opzione deve agire solo sul primo: nel tutorial il pad non serve, e legarla anche alla pausa avrebbe riacceso i controlli dove non c'entrano. Da qui _group_modal_open come sottoinsieme di _modal_open, e _hidden_by_modal() come unico punto di decisione. ⚠️ Il caso limite che il polling avrebbe mangiato: la stessa _modal_open == true può ora voler dire «nascondi» o «lascia visibile», quindi confrontare solo lei per capire se qualcosa è cambiato non basta più — passando direttamente da un tipo di modale all'altro, senza un fotogramma «nessuna modale» in mezzo, visible sarebbe rimasto indietro. Si confronta anche group_now. Cura anche il rilascio delle dita: _release_all_inputs() scatta solo quando il pad sparisce davvero, non a ogni giro, altrimenti a opzione ON gli input sarebbero stati rilasciati di continuo sotto le dita. Nelle OPZIONI la voce esiste solo dove ci sono i controlli touch (_ha_porta_keep_pad()), stesso trattamento che GRAFICA ha su mobile — e qui c'è la lezione di SU-228 applicata: una voce condizionale in mezzo alla lista sposta gli indici di tutte quelle dopo, ed è esattamente così che si finisce con «tocco DIMENSIONE TESTO e si apre GRAFICA». Nessun indice è scritto a mano: _opt_item_count() somma la voce quando c'è, _opt_text_idx() resta count - 2, ← INDIETRO resta count - 1, e la nuova voce si legge da _opt_keep_pad_idx(). Verificato guardando i provini a 2556×1179 (telefono) e 2048×1536 (iPad): a OFF il pannello di fine run è pulito come dopo SU-224, a ON joystick e diamante A/B/X/Y sono visibili sul ventaglio delle carte senza coprirle, provato anche con la scala dei controlli forzata a ENORME. 📌 Da decidere: il gruppo touch_modal contiene anche il menu pausa e la mappa, non solo le tre modali citate dal ticket — a opzione ON il pad resta visibile pure lì. File: Settings.gd, VirtualControls.gd, MainMenu.gd, nuovo scripts/tools/shot_su234_keep_pad.gd. - [docs] SU-235 — sì, il giro iOS si fa tutto da terminale: build, firma,
.ipa e installazione su iPhone vero sono stati eseguiti davvero, non descritti. Lo spike chiedeva «si può fare? come?», e la risposta non è una procedura dedotta: xcodebuild build (12 s, firma automatica, zero password), xcodebuild archive + -exportArchive (.ipa vero da 95 MB) e devicectl device install app su un iPhone reale sono riusciti in sessione, senza aprire Xcode una volta. Il documento segna ogni passo come PROVATO o DOCUMENTATO, che è la sola cosa che rende utile uno spike del genere: l'unico passo non rieseguito è l'export Godot iniziale, di proposito — un --export-release reale scrive dentro files/homeless_city/ (cache .godot/, possibile riscrittura di export_presets.cfg) e il ticket vietava di toccarlo, quindi è stato riusato un export già esistente fuori dal repo. Due scoperte sull'ambiente: xcode-select punta ancora alle Command Line Tools e non a Xcode.app, quindi xcodebuild/devicectl «nudi» falliscono — aggirato per tutta la sessione con export DEVELOPER_DIR=/Applications/Xcode.app/Contents/Developer, che non richiede sudo e non tocca la configurazione globale; e il token dell'account Xcode è scaduto (account che risale al 2014), cosa che non disturba build/archive ma fa fallire -exportArchive se gli si chiede un team esplicito — rimedio proposto: una App Store Connect API key, ~10 minuti su web, che smette di dipendere da quella sessione. L'unico muro strutturale, verificato con un errore vero e non ipotizzato: il telefono deve essere sbloccato nel momento in cui si lancia l'app per leggerne i log; l'installazione invece funziona anche a schermo spento. Proposta di integrazione: una cartella tools/ios/ sul modello di tools/android/, con il controllo di lockState prima del lancio per non far fallire tutto con un errore criptico. ⚠️ Cosa resta fuori portata della CLI: la distribuzione ai tester esterni con la comodità di dmg/zip/apk non esiste su iOS — o si registra lo UDID di ogni device sul portale Apple, o si passa da TestFlight (vedi STUDIO_BETA_STORE.md). È una decisione di prodotto, non un comando mancante. Nessuno script creato, export_presets.cfg e export_all.sh solo letti. File: nuovo SPIKE_IOS_CLI.md. - [docs] SU-236 — Firebase: «in parte», e la parte che conta è che la via ufficiale sul desktop non esiste. Lo spike serviva a decidere se rompere la regola «niente dipendenze/plugin esterni», e la risposta è che i due servizi con più valore percepito sono proprio quelli che non arrivano dove serve: Crashlytics non copre Windows (uno dei due canali desktop principali della beta) e FCM non ha alcun canale per un eseguibile desktop nativo — un
.exe/.app non è né Android, né iOS/APNs, né Web. Aggiungere un plugin per una copertura disomogenea è l'eccezione che non si giustifica. Il dato che ha cambiato il quadro è venuto dall'ispezione del sorgente, non dalla descrizione: GodotFirebase (685 stelle, ultimo push il giorno stesso della ricerca) ha auth.gd, firestore.gd e database.gd che fanno tutti extends HTTPRequest — è un wrapper GDScript sopra le REST API, non un binding nativo, quindi gira anche su macOS e Windows. E specularmente l'SDK C++ ufficiale di Google compila per desktop ma fornisce solo «stub implementations» che non si connettono davvero al backend (issue firebase-cpp-sdk#442): chi vuole Firebase sul desktop è *già* costretto alla via REST, non è una nostra scorciatoia. Da qui il sì condizionato: Firestore via REST pura, senza SDK né addons/, come *candidato* per il db delle chiavi di SU-237 — ma dichiarato pari merito con l'estendere il pattern GitHub JSON già in uso, che copre gli stessi requisiti a costo zero e senza un account nuovo da gestire. ⚠️ Un limite che vale per entrambe le strade: un crash «duro» del motore (il processo muore) non può telefonare a casa da solo, serve un handler nativo — quindi nessuna soluzione via HTTPRequest, con o senza Firebase, copre il crash reporting automatico. Quote Spark verificate sulla pricing page ufficiale; le cifre del piano Blaze e quelle di Supabase/Sentry/Apps Script sono dichiarate come provenienti da aggregatori terzi e non da fonte primaria, invece di essere spacciate per certe. File: nuovo SPIKE_FIREBASE.md. - [docs] SU-238 — beta program sulle tre vetrine: ≈$224 il primo anno, e una scadenza che riguarda già il flusso di oggi. Confronto Steam / TestFlight / Google Play con costi, tetti tester, review e requisiti di build, ogni cifra con fonte e data. Ordine consigliato Google Play → Steam → Apple, per costo crescente e severità della review: $25 una tantum Google, $100 una tantum Steam (recuperabili sopra $1.000 di ricavo), $99/anno Apple — l'unico ricorrente, e l'unico canale con una vera review d'ingresso. Il punto meno ovvio del confronto: nessuno dei tre sostituisce
BetaGate.gd, perché controllano chi può *installare*, non spengono chi ha *già installato* — il cancello fail-closed a ogni avvio resta l'unico interruttore immediato, che è esattamente ciò che SU-237 vuole far evolvere e non buttare. ⚠️ Scoperta collaterale con una scadenza, fuori dallo scope del ticket ma addosso al flusso attuale: la Android Developer Verification di Google, enforcement dal 30 settembre 2026 in una prima fascia di regioni, riguarda l'installazione di qualunque app su dispositivi Android certificati — sideload compreso, cioè l'APK che oggi passa da Drive. Esiste un tier gratuito «Limited Distribution» (nessuna fee, nessun documento d'identità, fino a 20 dispositivi) centrato proprio su questo caso d'uso: da valutare indipendentemente dalla decisione su Play Store. Segnalati anche due vincoli Google che cambiano il lavoro: dal 31 agosto 2026 le app nuove devono targettare API 36, e Play accetta solo .aab per un'app nuova, mentre export_all.sh produce un .apk — servirebbe un preset di export in più. File: nuovo STUDIO_BETA_STORE.md. - [docs] SU-237 — chiavi beta: il disegno c'è, e il punto che il ticket non nominava è che la lista pubblica va pubblicata come HASH, non in chiaro. Brainstorming come chiesto, non codice: nessuna riga di
BetaGate.gd o genera_whitelist.py toccata. Il flusso proposto riusa quasi tutto — fail-closed, cache di 7 giorni, lista di scorta DEVELOPER_DEVICES — e cambia solo da dove viene il valore confrontato: non più l'hash dell'hardware (che su Android il sistema rigenera, chiudendo fuori il tester: è il difetto che ha aperto il ticket) ma una chiave random che il device salva in user://beta_key.cfg e che sopravvive al cambio di id. ⚠️ La trappola resa esplicita: oggi devices.json è tutto pubblico ed è sicuro *solo* perché ogni codice è l'impronta di un hardware specifico — leggerli tutti non serve a niente. Con chiavi copiabili, continuare a pubblicarle in chiaro trasformerebbe quel file in un mazzo di chiavi scaricabile da chiunque, che è molto peggio di «un amico gira la chiave a un amico». Rimedio proposto: pubblicare l'hash salato della chiave con la stessa ricetta che BetaGate già applica all'hardware id — zero tecnologia nuova, e funziona identico sia su GitHub JSON sia su Firestore, quindi non resta appeso all'esito di SU-236. La transizione è senza flag day: hash-di-hardware e hash-di-chiave sono indistinguibili nello stesso array, quindi convivono e si migra per persona quando serve, cioè quando un tester Android si blocca davvero. Il compromesso della chiave copiabile è affrontato in faccia e accettato con cinque motivi (§7), non nascosto. File: nuovo DESIGN_CHIAVI_BETA.md. - [fix] SU-198 (3° giro, KO Ivan) — l'ombra smette di essere un privilegio dei palazzi: negozio di vestiti, vineria, camioncini, bagni, lampioni e panchine adesso ce l'hanno. Il KO era un elenco di sei oggetti, e la diagnosi ha spiegato perché erano proprio quelli: il sistema ombre funzionava benissimo ma aveva una sola chiamata, dentro
_place_building_sprite_slot(). Tutto il resto della città nasce da strade diverse e restava scoperto per omissione, non per una regola — il negozio di vestiti e il gattile passano dall'*altra* funzione di piazzamento (_place_building_sprite, che l'ombra non la chiamava); la vineria non è nemmeno uno Sprite2D, è un TextureRect che ripete liquor.png sull'isolato; i camioncini sono scene AnimatedBuilding che si cambiano i frame da sole; bagni e lampioni sono Sprite2D costruiti a mano (il lampione per giunta con centered = false); e la panchina è figlia della sua Area2D, quindi la sua position è (0,0) locale e nessuno le calcolava una base in coordinate mondo. Da qui la scelta: un solo sistema generalizzato, non un secondo parallelo. Nasce _spawn_ombra_generica(), che prende espliciti i quattro dati che negli arredi cambiano (sagoma, base, scala, z della sorgente); _spawn_ombra_palazzo() resta come involucro sottile e sui palazzi il risultato è identico a prima. ⚠️ Il pezzo che nessuno aveva previsto e senza cui la sera si rompeva tutto: l'ombra del tardo pomeriggio va all'insù, e panchine e camioncini sono y-sortati sul centro e non sul piede — con la regola dei palazzi (z = int(base.y) - 1) l'ombra finiva sopra il proprio oggetto. Risolto memorizzando allo spawn mini(int(base.y) - 1, z_sorgente - 1), che sui palazzi coincide con il valore di prima. Verificato guardando i provini delle 17: le ombre passano dietro e nessun oggetto è coperto dalla propria. Dove si ferma, e perché: OMBRA_ARREDO_ALT_MIN = 24 tiene fuori il cestino della spazzatura (12×16), la cui ombra sarebbe una macchia di 7 px pagata su ogni cestino della città; e il bucket della query «questo punto è in ombra?», che scurisce i personaggi, resta a OMBRA_BUCKET_ALT_MIN = 80, cioè solo i palazzi — un barbone che si scurisce passando accanto a un lampione sarebbe rumore, non realismo. Per i soli lampioni il dithering dei bordi è spento (bordo_texel = 0 su un secondo ShaderMaterial): il palo è largo 2 texel e la fascia sgranata lo riduceva a una scacchiera. I due materiali condividono un unico programma shader, quindi resta una sola set_shader_parameter per giro. Costo, misurato: si passa da ~39 ombre a 154 (42 vetrine + 112 arredi, di cui 88 lampioni), ma aggiorna_ombre() non gira per frame — con l'early-out esistente il ciclo pieno parte ~1,2 volte al secondo, 142 µs a giro = 0,17 ms di CPU al secondo; il bucket dei personaggi resta a 42 voci come prima, quindi la query di scuritura non costa niente in più. Leva già pronta se pesasse: OMBRA_ARREDO_ALT_MIN da 24 a ~50 toglie gli 88 lampioni. ⚠️ NON misurato: il costo GPU su device. SU-226 ha stabilito che su telefono il gioco è limitato dal riempimento dei pixel, e qui si aggiungono ~115 sprite semitrasparenti: la misura sopra è di CPU, non di fill rate, e il colpo d'occhio con ~84 ombre a schermo va visto sull'Honor 10. File: WorldGenerator.gd, nuovo scripts/tools/shot_su198_arredi.gd. - [change] SU-230 — verso sud la lanterna passa nella mano destra del barbone, che è la sinistra dello schermo. Un solo valore:
LANTERNA_ANCORE["s"] da Vector2(8, -3) a Vector2(-8, -3) in Player.gd. Sembra un dettaglio da niente ed è invece l'unico modo di far convivere due prop condivisi: il gatto in braccio sta sul lato opposto, quindi con la lanterna a destra i due si accavallavano proprio nella direzione in cui il barbone si guarda di più. Il segno negativo qui è definitivo, e vale la pena aver capito perché: il commento in testa al dizionario dice «prima del flip orizzontale», che su tutte le altre direzioni significa che il valore verrà specchiato — ma "s" non è mai in FLIP_MAP, quindi flip_h resta false e quella x negativa è davvero la x finale. A sud il barbone è frontale e guarda la camera: la sua destra *è* la sinistra dello schermo, e la nota è ora scritta accanto alla costante perché è esattamente il punto in cui il prossimo si convincerebbe di aver trovato un bug. Le altre quattro direzioni non sono state toccate di un pixel, come chiedeva il ticket. Verificato guardando i provini, non deducendo: shot_su230_lanterna.gd carica la scena vera, forza game_hour = 2 (la lanterna si accende con l'orologio, non si può aspettare la notte) e fotografa il barbone a sud con e senza gatto — lanterna accesa a sinistra in entrambi, gatto sul lato opposto, nessuna sovrapposizione. Vale per tutte le skin perché la lanterna è un Sprite2D unico figlio del Player, non un frame dello sheet. File: Player.gd, nuovo scripts/tools/shot_su230_lanterna.gd. - [test] Collaudo di fine sprint: VERDE, nessuna regressione sui sei commit di codice.
run_check.sh force → 134 script, 39 scene, 0 falliti. ⚠️ Cercato apposta SHADER ERROR in oltre 10 run indipendenti (headless e finestrate con GPU vera): mai comparso — ed era il rischio numero uno del giro, visto che SU-198 fa condividere un solo programma shader a due ShaderMaterial e che su questo stesso ticket un materiale caduto sul default era già passato inosservato una volta. Il provino conta 154 ombre su 154 accese. Run SP completo su due giorni di gioco: livello 19, orphans=0 per tutta la run, zero errori. Smoke sugli interactable uno per uno — la regressione più insidiosa di SU-229, che tocca *tutti* i tipi: 10 su 12 provati con una pressione vera e tutti a posto, compreso il gate orario del rifugio. Tutorial: run_active=false, 0 KO, zero errori. Due falsi negativi del banco di prova, diagnosticati e non del gioco: un level-up in pausa che bloccava la sequenza, e la priorità elemosina>interact che dirottava la pressione quando un NPC entrava per caso nel raggio. Non verificato, e dichiarato invece che dato per buono: SOUP_VAN e GATTILE non sono stati generati in nessuno dei due semi provati; il costo GPU delle 154 ombre su device; il multigiocatore reale, perché eos_credentials.cfg su questo Mac manda NetworkManager sul ramo EOS invece che su ENet e run_mp.sh non collauda più il percorso di rete — disallineamento d'ambiente già scritto in TESTLOG il 31/07, non una regressione di oggi. 📌 Segnalazione incidentale, riga non toccata da nessun commit del giro: Player.gd:652 (z_index = int(global_position.y)) non ha un clamp contro il tetto z di Godot (4096) — irraggiungibile su una mappa 1632×1632, da ricordare se un giorno le mappe crescono. Dettaglio completo in TESTLOG.md. - [fix] I sei script di provino nati oggi entrano in git col loro
.uid. Stessa cura del 2026-08-03 mattina: in scripts/tools/ c'erano 66 .uid per 72 .gd, e i sei mancanti erano esattamente quelli di questo sprint — gli agenti hanno lanciato Godot con SU_TMP/HOME separati, quindi l'identità non è mai stata scritta nel progetto vero. Generati con un import dell'editor headless, verificando prima e dopo che export_presets.cfg non venisse riscritto (l'editor lo riscrive da memoria alla chiusura, ed è già costato un giro a questo repo): risultato identico byte per byte, e zero errori nel log di import. File: nuovi shot_su198_arredi.gd.uid, shot_su230_lanterna.gd.uid, shot_su234_keep_pad.gd.uid, su229_bagni_cascata.gd.uid, su229_bagni_tie.gd.uid, su243_separazione.gd.uid.
RIENTRI ATTESI: SU-198, SU-229, SU-230, SU-231, SU-234, SU-235, SU-236, SU-237, SU-238, SU-239, SU-243 — le undici chiavi messe In revisione oggi. Al prossimo giro, prima dei lotti: quante sono tornate in «Da fare» e con quale KO. È l'unica metrica di qualità dello sprint che non sia vera per costruzione. *(Giro precedente: delle sei chiavi attese — SU-198, SU-212, SU-222, SU-223, SU-224, SU-225 — è rientrata solo SU-198, tasso di rientro 1 su 6.)*
- [change] SU-239 —
IVAN_DEVICES diventa DEVELOPER_DEVICES: nel codice non ci vanno nomi di persone. Regola nuova di Ivan, e questa costante era l'unica a violarla: la lista di scorta del BetaGate, quella che lascia entrare i dispositivi dello sviluppatore sempre, anche a devices.json irraggiungibile o malformato. Tre occorrenze in un file solo (dichiarazione, il controllo sul dato fresco, il ramo di ripiego), più le due righe di commento che la descrivevano: quelle citavano «i dispositivi di IVAN» in maiuscolo, cioè si leggevano come un riferimento all'identificatore, e ora dicono «dello sviluppatore». Il nome di Ivan resta dove racconta una decisione («la riempie Ivan», «Ivan deve poter riparare»): la regola vieta i nomi personali negli *identificatori*, non nei commenti. Lasciata in testa alla costante una riga che dichiara il vecchio nome, perché IVAN_DEVICES compare ancora in CHANGELOG.md e in tre punti di TESTLOG.md — storico che non si riscrive — e chi ci arriva da lì deve poter ritrovare la costante. Cambio puramente lessicale: la costante non è serializzata da nessuna parte, non compare in nessun .cfg, .tscn o JSON, quindi non c'è modo che una stringa rimasta indietro la sganci in silenzio. ⚠️ Il gate a runtime non è stato provato e non era provabile qui: OS.has_feature("editor") è il primissimo controllo di BetaGate e lo spegne dall'editor, quindi la verifica è compile-check più la lettura del diff. 📌 Segnalazione fuori perimetro, per Ivan: grep -ri IVAN trova ancora "IVAN" in scripts/tools/su207_shot.gd:95, dove è il nome di una classifica finta usata da un provino — non è un identificatore, quindi il ticket non lo chiedeva, ma sta accanto a GIACOMONE, PIERPAOLO e TOSETTIWEB e finisce dentro gli screenshot. File: BetaGate.gd.
Il telefono smette di essere un posto scomodo: il joypad si toglie di mezzo e i pannelli si leggono2026-08-01
- [feat] SU-222 — «GRAFICA LEGGERA»: un interruttore in Opzioni per i telefoni che non ce la fanno. Su Honor 10 il gioco stava a 29,5 fps di mediana, 0% dei frame a 60, con la CPU al 40% di un solo core: non è la logica, è il fill rate — il viewport di progetto è 1024×768 ma
stretch/mode="canvas_items" fa disegnare alla risoluzione nativa, 2,46 megapixel per frame invece di 1,24. Dimezzando la risoluzione di sistema lo stesso identico APK faceva 59,1 fps. Il ticket proponeva di cambiare stretch/mode in project.godot; Ivan l'ha scartato perché avrebbe penalizzato anche un Android nuovo che regge benissimo, e ha chiesto un acceso/spento nelle Opzioni. Nuovo low_graphics in Settings.gd con lo stesso pattern di fullscreen_enabled: acceso mette content_scale_mode = VIEWPORT (metà pixel disegnati), spento torna a CANVAS_ITEMS. project.godot non è stato toccato: il default resta quello di oggi, l'interruttore agisce a runtime e vale su ogni piattaforma, così chi ha un telefono potente resta nitido e chi ha un catorcio scende quando vuole lui. La sorpresa che valeva il lotto: la voce-porta GRAFICA dentro OPZIONI era nascosta del tutto su mobile — eredità di SU-186, dove conteneva solo voci desktop (LIMITE FPS, SCHERMO INTERO) — quindi l'interruttore sarebbe stato irraggiungibile proprio sui telefoni per cui esiste. Ora la porta c'è ovunque e _opt_item_count() torna a valere lo stesso su tutte le piattaforme: l'eccezione mobile sparisce invece di raddoppiarsi. Traduzioni in 5 lingue, e la riga di aiuto dichiara anche il prezzo invece di venderlo solo bene: «Per telefoni che hanno visto giorni migliori: disegna meno pixel, più fps. Testi e immagini un po' più grandi e sgranati.» File: Settings.gd, MainMenu.gd, ui_menu.csv, tre su222_shot_*.gd. - [change] SU-223 — l'APK scende da 149 a ~118,7 MB: tre brani che non suonava nessuno se ne vanno, e la musica lunga passa a Vorbis. Contava per i tester Android, che scaricano da Google Drive e su 149 MB spesso non arrivano in fondo. Tre pezzi, tutti misurati e non stimati. (1) Asset iOS fuori dalle build altrui:
iOS/splash.png e iOS/icon.png viaggiavano dentro l'APK Android — e nelle build Windows e macOS — perché il preset ha export_filter="all_resources". Aggiunto exclude_filter="iOS/*" ai preset macOS, Windows e Android; il preset iOS è rimasto intatto di proposito, lì quegli asset servono. (2) Tre brani orfani cancellati, ognuno dopo un grep di conferma su tutto il progetto: menu.wav (39,9 MB, e MainMenu.tscn usa main.wav, non lui), main2_normalized.wav (31 MB, Main.tscn usa main2.wav), fight.wav. (3) Musica lunga in Vorbis — e qui il ticket chiedeva una cosa che non esiste: l'importatore WAV di Godot 4.6 offre solo Disabled / IMA-ADPCM / QOA, il Vorbis lì non c'è. Serviva convertire i sorgenti in .ogg veri (q4, ~128 kbps) e aggiornare gli ext_resource in tre scene — path *e* uid: MainMenu.tscn, Main.tscn, TutorialWorld.tscn. Ivan ha autorizzato l'estensione. Loop invariato: i nuovi .import hanno loop=false come i vecchi e il riavvio resta gestito via music.finished in MainMenu/World/TutorialWorld, quindi niente doppio loop; ffprobe conferma durata, canali e sample rate identici. Verifica: run_check.sh force → 123 script, 39 scene, 0 falliti (prima del fix degli uid Main.tscn falliva con «invalid UID»). ⚠️ Margine stretto: ~118,7 MB è la somma di delta misurati sui file, non un export vero — va confermato costruendo l'APK. L'audio non è stato ascoltato: resta da verificare a orecchio su device. Fuori dal ticket ma nato da lì: export_presets.cfg era in .gitignore, quindi le esclusioni sarebbero vissute su una macchina sola — e l'editor Godot riscrive quel file da memoria alla chiusura, cioè bastava aprirlo e chiuderlo per perdere il risparmio in silenzio. Ora è tracciato, con l'avvertimento sulle password del keystore accanto. - [fix] SU-198 (2° KO, stesso giorno) — via le facce rosse: il palette swap prende la tinta dal vertice, non da
COLOR. Ivan ha visto in gioco, sulla build appena installata, i volti di alcuni passanti diventati rossi. Regressione mia, del commit di poche ore prima, e sono due errori in fila. (1) In un canvas_item shader di Godot 4 il COLOR in ingresso a fragment() non è il modulate: è già texture(TEXTURE, UV) * modulate. Leggerlo come tinta e rimoltiplicarlo alla fine dava src * src * modulate, cioè il colore dello sprite elevato al quadrato — la pelle arancione diventava rosso acceso, i blu si smorzavano, il nero restava nero. (2) Il primo rimedio usava il built-in MODULATE, che in Godot 4.6 non esiste: lo shader non compilava e il motore ripiegava in silenzio sul materiale di default. È l'errore insidioso, perché il provino sembrava *risolto* — colori naturali — mentre in realtà la palette swap non veniva più applicata affatto. Preso solo cercando apposta SHADER ERROR nell'output di Godot. Rimedio buono: la tinta si cattura in vertex(), dove COLOR *è* il modulate del nodo già composto coi genitori, e viaggia al fragment con una varying. Entrambe le trappole sono ora scritte in testa al file, che è l'unico posto dove qualcuno le rileggerà. Verificato: nessun SHADER ERROR; run_check.sh force → 124 script, 39 scene, 0 falliti; provini in TMP/fix_shader/ guardati, con il controllo che vale davvero — che maglie e pantaloni non siano dei colori marker (magenta #FF66FF, verde-acqua #006666), perché è quello a distinguere «lo shader funziona» da «è caduto sul default». 📌 Perché era passato il primo giro: shot_ombre_pers.gd non è deterministico, pesca NPC diversi a ogni esecuzione — scoperto trovando 142.000 pixel di differenza fra due run dello *stesso* shader. Non è utilizzabile per confronti pixel a pixel. Nota di processo: né check_files.sh né run_check.sh validano gli shader, quindi un errore lì non lo intercetta nessuno dei due cancelli. File: palette_swap.gdshader.
RIENTRI ATTESI: SU-198, SU-212, SU-222, SU-223, SU-224, SU-225 — le sei chiavi messe In revisione oggi. Al prossimo giro, prima dei lotti: quante sono tornate in «Da fare» e con quale KO. È l'unica metrica di qualità dello sprint che non sia vera per costruzione.
- [docs]
RICOSTRUZIONE_DA_ZERO.md — un clone pulito non basta, e non se ne lamenta. Nato dallo stesso filo di SU-223, su richiesta di Ivan: LFS, binario Godot ed export template, plugin EOS (489 MB), credenziali, template Android, dipendenze Python stanno tutti fuori da git. Il progetto clonato si apre lo stesso e poi cede nei punti sbagliati — il multiplayer non si connette, l'export non parte, gli sprite sorgente sono file di testo da 130 byte. Il documento dice cosa manca, in che ordine rimetterlo (alcuni passi dipendono da altri), come accorgersi che manca, cosa invece è ignorato ed è giusto così, e una verifica finale in 5 punti. Le credenziali EOS e i dati dei beta tester li rimette l'utente: scritto a chiare lettere che un agente non deve chiederli in chat. Puntatore aggiunto in CLAUDE.md, così si trova a inizio sessione invece che dopo il danno. - [feat] SU-198 (KO) — chi cammina nell'ombra di un palazzo adesso si scurisce, esattamente come il barbone che entra sotto la pensilina. Le ombre accese ieri erano puro decoro: nessuno le interrogava, e un personaggio dentro l'ombra restava illuminato come fuori. La resa delle ombre non è stata toccata di un pixel (quella Ivan l'aveva approvata); si è aggiunto solo il test «sono dentro?». Come, e perché così: l'ombra è un parallelogramma (scale + skew, rotazione 0), quindi la si inverte in due righe di aritmetica —
v = dy/cos k, u = dx + v·sin k, dentro se |v| ≤ mezza altezza e |u| ≤ mezza larghezza. Scartate un Area2D per ombra (decine di corpi fisici) e una light-map a shader (costo GPU su mobile, cioè proprio il rischio che il ticket chiedeva di non correre). Bucket spaziale da 256 px costruito una volta allo spawn con l'ingombro massimo del giorno. Costo misurato: 0,554 µs a chiamata → 40 personaggi a 6 Hz fanno 0,0022 ms/frame, e per il renderer non cambia nulla: è solo aritmetica in GDScript, niente di nuovo sulla GPU. Vale per player, polizia e NPC col linguaggio visivo della pensilina (tint sullo sprite). I sei punti di non-regressione: di notte lo scurimento si spegne col night_factor (alle 2:00 il modulate è (1,1,1,1)); sotto la pensilina è un elif e non un prodotto, quindi non raddoppia; in MP la query è locale su geometria deterministica dal seme, zero byte in rete; nel tutorial la lista è vuota e i 14 personaggi restano bianchi senza errori; dopo generate() la query torna subito false, niente personaggi scuri per sempre. Bug trovato per strada e sanato, senza il quale la feature non funzionava: palette_swap.gdshader sovrascriveva COLOR, quindi il corpo di un ModularNPC con palette swap era l'unica cosa in città immune a qualsiasi modulate — niente ombra, e nemmeno la tinta da ubriaco, che era già rotta da prima e nessuno se n'era accorto. Ora il tint in ingresso si legge e si rimoltiplica alla fine. Da guardare al collaudo: quella riga tocca *tutti* i ModularNPC, quindi vale la pena controllare che i colori di maglia e pantaloni siano quelli di sempre. Limite dichiarato: il test usa il rettangolo dell'ombra, non la sagoma del palazzo — con una pianta molto irregolare qualcuno può scurirsi 2-3 px prima del dovuto (mitigato con un margine 0,94 sulla larghezza). File: WorldGenerator.gd, Player.gd, NPC.gd, palette_swap.gdshader, DebugPanel.gd (bottone «Pers.»), nuovo shot_ombre_pers.gd. - [fix] SU-224 — quando si apre una finestra modale il joystick e i tasti A/B/X/Y spariscono, e tornano appena si chiude. Su Android il gruppo di tasti stava disegnato *sopra* le modali: alla schermata di fine run X e B finivano letteralmente addosso al bordo destro del cartiglio del punteggio. La causa è banale e generale — il CanvasLayer dei controlli touch sta a quota 128, sopra tutto — quindi il difetto non era della sola schermata di fine run ma di tutte le modali. Nuovo gruppo
touch_modal: VirtualControls fa un poll a 20 Hz e nasconde joystick e diamante finché c'è almeno una modale visibile nel gruppo. La strada scartata è la parte interessante: legare la sparizione a get_tree().paused avrebbe curato solo il single player, perché in multiplayer il ventaglio delle carte e la cerimonia del superpotere si aprono *senza* pausa (_paused_mode = not is_mp and not is_bot) e il pannello di fine run arriva senza pausa in modalità spettatore — la regola ferrea «mai pause globali in MP» si sarebbe portata dietro anche questo bug. Il criterio è quindi la presenza a schermo, non lo stato del mondo; paused resta come ripiego in OR e copre gratis i pannelli del tutorial. Coperte: fine run (e il name entry che le sta dentro), pausa, mappa, ventaglio carte, cerimonia super, quaderno. Escluse apposta: il banner MP «SEI COLLASSATO» (non è modale, il mondo continua), DebugPanel e BetaGate. Prima di nascondere il pad si è verificato che ogni modale resti usabile a solo tap. File: VirtualControls.gd, HUD.gd (_register_touch_modals(), additivo), LevelUpScreen.tscn, SuperCeremony.tscn, Quaderno.tscn. - [fix] SU-225 — su telefono i pannelli dei menu smettono di essere vetro: il testo non cade più sul logo e sulla faccia del barbone. «VUOI FARE IL TUTORIAL?» e la scelta del quartiere erano quasi del tutto trasparenti, e su schermo piccolo testo e arte hanno scala simile e si impastano. L'alpha del fondo passa da
_panel_bg_alpha(): 0,70 su desktop — identico a oggi, il layout su Mac e Windows non cambia di un pixel — e 0,93 su telefono. Il taglio è is_phone_screen(), che misura i pollici del lato corto e non i pixel: l'Honor 10 ha 1080 px sul lato corto esattamente come tanti monitor, quindi contare i pixel avrebbe incupito anche i desktop. Il tema globale non è stato toccato: il pannello incriminato è uno StyleBoxFlat costruito a mano in MainMenu, non la variation CardboardPanel — intervenire su default_theme.tres avrebbe incupito le schermate cartone che oggi vanno bene. Per il criterio «i controlli non coprono elementi di gioco» (la fontana dietro i tasti A e B) è stato alleggerito il riempimento dei tasti da 0,82 a 0,58 lasciando bordo e lettera invariati: la forma chunky resta, la campitura che nascondeva il mondo no. Fatto solo in parte, e va detto: resta scoperto il caso dell'oggetto proprio sotto il dito, dove il tocco lo prende comunque il pulsante — spostare la UI di gioco è un follow-up, non una toppa da infilare qui. File: MainMenu.gd (_panel_bg_alpha(), _make_panel(), additivi), VirtualControls.gd. - [fix] SU-212 (3° KO) — la copertina del manuale torna grande, e le icone delle carte smettono di sbavare. La parte che vale è la diagnosi: la copertina rimpicciolita non era una regressione del manuale.
build_manuale_pdf_a3.py disegna la copertina a larghezza fissa e ricava l'altezza dall'aspect ratio dello splash (sh = sw*ih/iw); quando i PDF in root sono stati generati (2 luglio) splash.png era 1448×1086, cioè 4:3, ma il 14 luglio il commit 614d2eb — un cambio del *gioco*, «sfondo fit-to-height», non del manuale — l'ha portato a 1941×810, panoramico. Con la formula invariata l'altezza in copertina è scesa da ~452 pt a ~252 pt: il −44% che Ivan ha visto è comparso da solo, senza che nessuno editasse una riga del manuale. Rimedio: center-crop dello splash al 4:3 storico dentro make_cover_splash.py, prima del fade, senza toccare l'asset di gioco. Le icone del pannello SALI DI LIVELLO venivano invece ritagliate dal foglio perk_icons.png (celle 16×16, 1,5 KB per l'intero foglio): ingrandite a 28 pt su un PDF da stampare erano un pastone. Ora process_perk_icon() legge gli 8 PNG singoli a 256×256 da sprites_raw/UI/perk_icon_*.png — gli stessi 8 poteri già scelti al giro precedente, kazoo tuttora escluso. I nomi dei file di output non cambiano, quindi build_manuale_pdf.py non è stato toccato: il layout resta identico, come impone il ticket. I 3 PDF nella root non sono stati toccati (data 2 luglio 11:08 invariata): il provino aggiornato è in TMP/manuale_provino/, coi due crop di confronto in png/. File: tools/manual_sprites/make_cover_splash.py, tools/manual_sprites/extract_sprites.py.
L'avvocato sparisce anche dal salvataggio, e i palazzi smettono di essere adesivi2026-07-31
- [change] SU-84 — l'ultimo pezzo di «avvocato» se n'è andato: adesso l'upgrade si chiama
endless anche dentro user://meta.cfg. Il giro scorso l'id di salvataggio era stato lasciato apposta com'era, per non far perdere l'acquisto a chi l'aveva già fatto; Ivan ha deciso il contrario — siamo in beta, chi l'aveva comprato lo ricompra, e in cambio il codice smette di parlare di un concetto che non esiste più. Rinominata la chiave in SHACK_UPGRADES e in BARACCA_CHIAVI, e con lei le due chiavi di traduzione BARACCA_AVVOCATO_* → BARACCA_ENDLESS_* in tutte e cinque le lingue. Nessuna migrazione del file di salvataggio: è una scelta, non una dimenticanza. La prova che serviva davvero non era il compile-check ma che la voce del negozio risolvesse ancora i testi tradotti invece di mostrare la chiave grezza: verificata a motore acceso (nome = "MODALITÀ ENDLESS", descrizione intera, avvocato non più fra gli id, posizione nel listino invariata). File: MetaProgress.gd, RunManager.gd, World.gd (commenti), ui_meta.csv, i due script di prova su_sprint13_*. - [feat] SU-198 — i palazzi proiettano l'ombra sulla strada, e adesso lo fanno davvero: la feature è accesa di default. Il ticket era nato come brainstorming («NON implementare») ed era rimasto tre giri dietro un flag spento, per scegliere *come* fare le ombre prima di scriverle: pista scelta l'ombra procedurale per sprite, sole che gira con l'orologio di
GameState (nord-ovest la mattina con l'ombra davanti, niente ombre a mezzogiorno, sud-ovest la sera con l'ombra lunga dietro), ancoraggio alla base vera del palazzo misurata texture per texture, facciata scurita quando l'ombra cade davanti, bordi pixellosi col dithering delle luci. Ivan ha guardato il provino e ha detto di accenderla: ombre_palazzi passa a true e il linguaggio da «provino, spento di default» sparisce da commenti ed etichette. Il bottone del DebugPanel resta — ora si chiama «Ombre» — insieme ai regolatori di lunghezza, angolo, opacità e modo del sole: serve a spegnerle e a tarare. Quattro punti di non-regressione verificati nel codice invece che dati per scontati: il tutorial ha un piazzamento edifici tutto suo e non genera ombre; le finestre accese di notte (SU-215) sono nodi separati e non litigano con la scuritura della facciata; il piazzamento è deterministico dal seme e non serve nulla in rete; generate() azzera la lista, niente nodi orfani al cambio quartiere. Resta da misurare su iPhone: sul Mac il costo era <0,2 ms/frame, su GPU mobile no. - [fix] SU-212 (KO) — nel manuale il gatto volante scende dove si parla di gatti lanciati, e sotto la Baracca compare la fila delle icone delle carte. Due correzioni chirurgiche al provino, niente riscritture: nel pannello IL GATTO in alto resta solo il gatto che dorme, e il volante si sposta a fianco della riga «Gatto lanciato = gatto perso» (larghezza del paragrafo ristretta per non farli accavallare); nel pannello SALI DI LIVELLO spariscono carta e trombetta e al loro posto, nello spazio vuoto sotto la riga della Baracca, va una fila di 8 icone delle carte — quelle vere del ventaglio, ritagliate dal foglio
perk_icons.png con gli stessi indici che usa il gioco — alte 28 come la trombetta di prima e distanziate per coprire tutta la colonna. Il kazoo è escluso apposta dalla fila: è un ottone, e rimetterlo a due centimetri dalla trombetta appena tolta sarebbe stato un KO annunciato. Nuova process_perk_cell() in extract_sprites.py: ritaglia le 16 celle 16×16 senza chroma-key né bbox-crop, così restano tutte della stessa misura. I 3 PDF nella root non sono stati toccati (data 2 luglio invariata): il provino aggiornato sta in TMP/manuale_provino/, con le pagine in PNG accanto per guardarle senza aprire il PDF. - [fix] SU-206 (2° KO) — i pallini di pensiero escono dalla testa del barbone e salgono verso destra, e la nuvoletta è la loro continuazione. Il difetto non era di taratura ma di ancoraggio: i pallini erano definiti come scostamenti dal centro della nuvola, e quel centro era inchiodato a x=0 sopra la testa — con quel vincolo la catena non poteva che pendere in basso a sinistra, e allargando la nuvola i pallini se ne andavano con lei. Ora l'ancoraggio è invertito: i pallini vivono in coordinate locali al Player (agganciati alla testa, fermi) e la nuvola si aggancia col bordo sinistro, quindi cresce solo verso destra restando attaccata alla catena. La posizione della testa è stata misurata sugli screenshot invece che stimata (cima a y=−24, larga x∈[−5,5]): era proprio la stima sbagliata a far sembrare il gruppo sganciato dal personaggio. Rispettata anche la convenzione del fumetto, che prima era rovesciata: dalla testa alla nuvola i tondi crescono (2,6 → 3,6 → nuvola) invece di rimpicciolire, e nessuno dei tre si interseca — ogni tondo ha il suo bordo nero chiuso (distanze verificate: 1,9 px dalla testa, 3,35 px fra i pallini, 4,7 px dal corpo nuvola). Il provino offline
tools/preview_status_cloud.py è stato riscritto nelle stesse coordinate e ha ora tre self-check invece di uno: icone dentro il bianco, tondi mai intersecati, catena che sale verso destra. Nessuna soglia, isteresi o gate toccati; resta locale, zero RPC. Limite dichiarato: un clamp per tenere la nuvoletta dentro lo schermo non è mai esistito, e ora che il gruppo si estende fino a ~78 px a destra del barbone il bordo destro si tocca prima — vale un ticket a parte, non una toppa improvvisata al terzo giro. - [fix] SU-199 (3° KO) — la scheda «LE TUE BARRE» resta dentro lo schermo, e le carte del tutorial si attivano camminandoci attraverso. Due correzioni. (1) Il pannello modale usciva dal bordo inferiore per un motivo preciso: con l'ancora verticale al centro Godot congela l'offset alla dimensione del pannello *nel momento del preset* — cioè quando è ancora vuoto — e poi fa crescere il contenuto verso il basso da quell'offset. Più il testo si allungava (ed era cresciuto proprio col KO precedente, fra energia dal bidone e le tre vie dell'igiene), più sforava. Ora l'ancora verticale è fissa in alto e la posizione la ricalcola
_reflow_modal_layout() dopo aver misurato il contenuto vero, clampata dentro il viewport con 24 px di margine; se un testo non ci sta nemmeno a piena altezza, il corpo scorre invece di sforare — scelta dichiarata: meglio una scrollbar che un font rimpicciolito e illeggibile su telefono. Vale per tutti i pannelli, non solo quello delle barre, ed è agganciata sia all'apertura sia al cambio lingua a caldo. (2) Le stazioni CARTE e SUPER non si attivano più premendo un tasto: il trigger scatta al passaggio del barbone, una volta sola. La forma dell'area passa da un quadrato 64×64 a una banda stretta e alta quanto la corsia (stesso principio del cancello FINE), così l'attivazione avviene proprio quando si incrocia il centro del cartoncino, qualunque sia la posizione verticale nella corsia. Testi rifatti di conseguenza in tutte e 5 le lingue («cammina attraverso» al posto di «premi il tasto»), e via il ramo di input diventato codice morto. Nuovo provino su199_shot.gd/.tscn per fotografare la scheda senza passare da screencapture (che su questo Mac restituisce nero: manca il permesso di registrazione schermo al processo che guida la sessione) — verificato a schermo in italiano e in tedesco, la lingua coi testi più lunghi.
Il Buttafuori alla porta, EOS che si spegne come si deve, e un birillo che era stato schiacciato2026-07-31
- [test] Collaudo unico dello sprint: VERDE sui rischi grossi, GIALLO sul contorno. Compile-check completo: 117 script, 38 scene, 0 falliti. Il rischio più grave del giro era che la retata sparisse anche per chi *non* ha l'endless, e le due run col bot lo escludono con numeri: senza lo sblocco
raid_phase→2, 4 raiders, cattura a 30:00+5 s; con lo sblocco raid_phase→3, 0 raiders, run ancora viva a 30:00+297 s. Zero SCRIPT ERROR, zero Lambda capture, zero Invalid access. Il cancello del salvataggio di SU-216 è verificato in entrambe le direzioni — due run vere di fila fanno salire i contatori (arresti, bidoni_rovistati, panchine_diverse, retate_sopravvissute, sblocchi di quartiere) e il tutorial non crea nemmeno il meta.cfg — con un limite dichiarato: il bot non si può guidare dentro il tutorial (la sua autopartenza rimanda a Main.tscn ogni volta che run_active è false), quindi le interazioni vere lì dentro restano verificate a lettura di codice. Notte e finestre: 100 lampioni, 22 insegne, 205 finestre in tabella, nessun errore nel ciclo per-pixel; determinismo garantito dal codice (hash puro su seme+posizione, zero randf()). Font IT/DE verificati a schermo a risoluzione iPhone su HUD, carte e fine run — niente tagliato né traboccante, col titolo tedesco più lungo («STRASSENCHARISMA») che sta su una riga. Corretto un falso positivo del collaudo: la frase di morte sembrava senza traduzione tedesca, ma FINE_FAME ha tutte e cinque le lingue — era su207_shot.gd a cercare una chiave inesistente (HUD_GO_MORTO_FAME) e a ricadere sull'italiano. Non verificato, e va detto invece che darlo per buono: multiplayer reale, walkthrough interattivo del tutorial, contatore del gattile (il bot non ha completato una consegna), e il ramo *bloccante* di BetaGate (provato solo inerte in editor). Segnalato inoltre che su questo Mac run_mp.sh non collauda più il percorso ENet — EOSBridge.available() è vero e NetworkManager.host_game() sceglie il ramo EOS: disallineamento d'ambiente, non regressione di questo sprint. - [feat] Di notte qualche finestra è accesa, e a distinguerla da un cornicione è la griglia (SU-215). Il ticket nasceva da cinque euristiche già fallite — soglia di luminosità, percentile per-texture, erosione morfologica, profili di riga/colonna, glow per cella — che guardavano tutte il singolo pixel e su facciate dipinte a mano non distinguono una finestra da un muro scuro. Il rimedio non è una sesta euristica ma un cambio di livello: le finestre di un palazzo sono quasi sempre una griglia regolare, quindi
tools/building_windows/detect_windows.py tiene un gruppo di macchie solo se sono tutte della stessa taglia, almeno quattro, incolonnate e a passo costante. È la struttura, non il colore, a dire «questa è una finestra». Copertura dichiarata: 27 facciate su 58, 257 rettangoli; le altre restano spente apposta, perché un palazzo spento è invisibile mentre un cornicione acceso è un KO — sono state scartate configurazioni più generose che salivano a 29 ma accendevano i decori al neon di nightlife_w144 e i fregi di commercial_w96. La domanda che il ticket lasciava aperta (luci vere o rettangoli ridipinti?) è stata decisa misurando: nell'array delle sorgenti ci sono 24 slot a schermo su 32, la lanterna ne prende uno e le insegne hanno la precedenza — un solo palazzo alto ha già più finestre di così, quindi metterle nell'array vorrebbe dire spegnere lampioni e insegne per accendere le finestre. Sono rettangoli ridipinti, ma con la grammatica delle altre luci: passata additiva, dithering 4×4, e disegno sulla texture vera, così la luce esce dal vetro che c'è e i montanti restano più bassi. Trappola trovata e risolta: ricopiare get_canvas_transform() a mano ogni frame non funziona, perché il _process di World gira prima che la Camera2D applichi i limiti — dove la camera è clampata le finestre si staccavano dal palazzo e restavano accese in mezzo alla strada; si vedeva solo confrontando lo stesso inquadramento di giorno. Ora è un CanvasLayer con follow_viewport_enabled. La tabella è un .gd generato e non un .json, perché i .json non entrano nell'export Android/iOS. Determinismo verificato con due generazioni a parità di seme (zero finestre diverse) e nessun tiro di RNG aggiunto, così le città esistenti non si spostano. Costo: +4 draw call, ms/frame invariati. - [fix] La nuvoletta sopra il barbone smette di essere una scatola (SU-206, KO). Era letteralmente un
draw_rect con una codina triangolare ancorata 8 px a destra della testa: ecco perché Ivan la vedeva «quadrata» e «staccata». Ora il contorno è l'unione di 8 cerchi rasterizzata su celle da 2 px — bordo nero a gradini spesso una cella, riempimento bianco, solo rettangoli interi, nessuna curva liscia e nessun antialiasing — e la codina è sostituita da due pallini di pensiero, come nel riferimento allegato. La scelta dei cerchi al posto di un PNG non è estetica ma pratica: la nuvoletta deve allargarsi quando le icone diventano due e insieme restare centrata, e una formula simmetrica attorno a x=0 risolve le due cose insieme; con un PNG servirebbero due immagini, e un nine-patch sui bordi tondi non funziona. Trovato durante la taratura, e solo perché il provino aveva un self-check invece di essere guardato a occhio: con due icone il cappuccio laterale lasciava l'icona più esterna a cavallo del bordo nero. Nessuna soglia, isteresi o gate toccati — cambia solo la forma e la posizione del disegno. - [change] L'endless smette di essere un sistema e diventa l'assenza della retata (SU-84, KO). Il KO chiedeva di togliere, non di aggiungere: «se si è sbloccato l'endless non arriva più la retata di strada e si può proseguire all'infinito, così ci togliamo il problema dell'avvocato di strada». Via le ondate CACCIA/TREGUA — segnali, costanti, tick, conteggi, banner — e
_enter_endless() che ora salta dritto alla fase ENDLESS senza emettere raid_started: niente Squadra Sgombero, niente spawn, niente cattura. Il punto meno ovvio stava nel sync verso i client: net_sync() era scritto in sequenza, quindi un client che saltava a ENDLESS attraversava comunque la fase ACTIVE e si vedeva lampeggiare il banner della retata; ora è un bivio ACTIVE-oppure-ENDLESS. Lo sbloccabile della Baracca diventa MODALITÀ ENDLESS nei testi, ma l'id persistito resta avvocato: è la chiave già scritta in user://meta.cfg e rinominarla avrebbe fatto sparire l'acquisto a chi ce l'ha già. L'HUD è stato rimesso in pari alla chiusura del lotto, perché altrimenti in endless avrebbe mostrato «TREGUA — 0 s» per sempre: gli stub deprecati is_endless_assault()/endless_phase_remaining() restituiscono per forza false e 0, e nessuno se ne sarebbe accorto prima del playtest. Ora il timer conta all'insù da quando sarebbe scattato lo sgombero; tolte le connessioni ai segnali spariti, i due gestori diventati irraggiungibili e sei chiavi morte dai CSV — ma non HUD_ONDATA_IN_ARRIVO/AL_RIPARO, che nonostante il nome sono le ondate meteo. Aggiunta necessaria e dichiarata: retate_sopravvissute viene incrementata anche all'ingresso in endless, altrimenti chi compra l'upgrade prima di sopravvivere a una retata vera non sbloccherebbe mai più NOTTEFONDA, perché raid_started non arriva più. - [fix] Su iPhone il testo non era piccolo: spariva (SU-207, KO). Il giro precedente aveva ingrandito tutto del 30% sul telefono e Ivan l'ha bocciato («diventa tutto troppo grande, ritornerei alle proporzioni: mi servirebbe solo il testo più definito perché si perde nei pixel»). Tolto il fattore 1,3 —
_auto_text_scale() vale 1.0 ovunque — e misurata la causa vera. StreetU-Pixel.ttf ha em 1000 su una griglia di disegno da 50 unità, cioè 20 pixel-di-disegno per em, quindi un pixel del disegno vale font_size × stretch / 20 pixel di schermo. Il canvas è sempre alto 768 (con canvas_items+expand comanda il lato corto) e cambia solo la larghezza: iPad 1024, Mac ~1365, iPhone in orizzontale 1663, cioè stretch 1,537. Da lì i numeri: font 11 → 0,85 px per pixel-di-disegno, font 13 → 1,00. E il font era importato con antialiasing = NESSUNO: sotto 1, la copertura di un tratto sottile arrotonda a zero e il tratto sparisce. «Si perde nei pixel» era letterale, e nessun ingrandimento globale poteva curarlo — anzi, ingrandire tutto era la cura sbagliata per la diagnosi sbagliata. Verificato riproducendo il framebuffer dell'iPhone 1:1 sul Mac (finestra 2556×1179 → viewport 1663×768, identico al telefono) invece di fidarsi di un simulatore ridimensionato. Rimedio in due parti: antialiasing in scala di grigi (provate 5 configurazioni; hinting=full e sub-pixel peggiorano) e pavimento 13 su carte e HUD, che è esattamente il punto in cui il rapporto torna 1,00. Il pannello di fine run passa da 600 px fissi — il 36% dello schermo su iPhone — a una larghezza che si adatta. Scartate e dichiarate due strade: stretch/scale_mode = "integer", che con expand arrotonda per difetto e su iPhone porterebbe 1,537 → 1,0 dimezzando tutta la UI, e MSDF, che mangerebbe gli angoli (pixel_range 8 su una griglia da 2,4 texel). Il prezzo è dichiarato: l'antialiasing dà lettere complete ma coi bordi più morbidi, e su Mac — dove il testo andava già bene — si vede; la geometria non cambia (bbox del pannello identica, misurata), la resa sì. Se non convince si rimette a 0 un valore in StreetU-Pixel.ttf.import, perché il pavimento 13 da solo copre già il caso iPhone. Sistemato per strada un difetto preesistente: col corpo online al completo il testo di fine run sfondava il pannello e finiva a scrivere sul nero e sotto i pulsanti. - [fix] La luce di notte non illuminava: ritagliava il giorno dentro la notte (SU-211 + SU-210, KO). Il KO diceva «staccano troppo dalla notte» e il provino spiega perché senza appello: dentro la pozza di luce il mondo tornava esattamente com'era alle 13:00, fuori restava blu piatto. Erano buchi, non lampade. La causa è che un overlay in
mix può soltanto interpolare verso il proprio colore — cioè schiarire lavando via il contrasto — ed è letteralmente l'adesivo appiccicato sul buio che il ticket originale metteva in guardia di non fare. Rifatto con una sola lista di sorgenti (lanterna, lampioni, insegne) e due passate: il tint del mondo dove la tinta blu cede e vira sul caldo della sorgente, più un overlay nuovo in blend_add che aggiunge energia invece di sostituirla, così la texture di ciò che è illuminato sopravvive. Il bordo è quantizzato a gradini con dithering ordinato 4×4 ancorato al mondo (non allo schermo, altrimenti la grana strisciarebbe con la camera): è il cono stipplato delle immagini di riferimento, né alone morbido né taglio secco. La lanterna del barbone smette di essere un caso speciale e passa dalla stessa matematica dei lampioni, da 400 a 190 px di raggio — molte sorgenti piccole invece di una grande, come nei riferimenti, ma è un cambio di quanto si vede di notte e va giudicato giocando (Bagliore▸ a 0 nel DebugPanel rimette la resa vecchia per il confronto). Le insegne diventano sorgenti proprie: restano accese anche dove non arriva nessun'altra luce e hanno l'alone del colore dominante ricavato dalla loro texture, non un colore fisso. Scartate e dichiarate tre strade: PointLight2D+CanvasModulate (avrebbe buttato via vignette, lampo, calura e gelo di SU-16), la lettura di SCREEN_TEXTURE con moltiplicazione (resa un filo migliore ma un backbuffer copy per frame, caro su Android), e lo Sprite2D additivo sull'insegna (è la macchia del KO). Costo misurato: <0,2 ms/frame su M2 Pro, ma sono due fullscreen invece di uno e su GPU mobile va misurato. Lo sprite della lanterna non esiste: è disegnato a pixel nel codice, 7×9, ed è un segnaposto dichiarato — aggancio, cinque direzioni, z-order e luce sono già a posto, serve solo l'arte vera col flusso a due ticket. - [change] L'ombra dei palazzi ora segue il sole, e lo stacco dalla base aveva una causa misurabile (SU-198, KO — resta un provino dietro flag, spento di default). Il sole va da nord-ovest la mattina a sud-ovest la sera, con mezzogiorno senza ombre, e l'ora arriva dall'orologio di
GameState. Il difetto che Ivan aveva notato su dir0_nordovest_1 non era capriccioso: parecchie texture hanno righe di contorno sotto il filo del muro (vasi, gradini) — building_downtown_v1 ne ha cinque, coperte al 34-65% contro il 94% del muro — e l'ombra si ancorava al bordo del rettangolo invece che al muro vero. Ora la base si misura texture per texture. Aggiunte anche le altre due richieste: la facciata si scurisce quando l'ombra le cade davanti, e i bordi sono pixellosi con lo stesso dithering delle luci. Nota per il futuro: la scuritura sovrascrive il modulate del palazzo, quindi se un giorno un altro sistema modulerà i palazzi va coordinato. - [fix] Il tutorial prometteva quattro cose che non funzionavano, e insegnava comandi sbagliati (SU-199, KO provato su device). I quattro bloccanti avevano cause distinte, tutte trovate leggendo il codice invece che tentando fix. Bagni:
_build_toilet() piazzava tre sprite e due comparse e basta — nessun Interactable, e il commento stesso lo diceva («puramente didattica»): non c'era niente da premere, quindi ci arrivavi e non succedeva nulla. Superpotere: _process/hold_begin/try_activate/_refresh_hud escono *tutti* su not run_active, che nel tutorial è false apposta — il battesimo funzionava, il lancio no; risolto con SuperPower.tutorial_mode, gemello del già esistente WeatherSystem.tutorial_wave_mode, invece di accendere run_active, che avrebbe portato dentro decay, retata e wanted, tutti vietati dal ticket. Tasti: InputManager.KEYBOARD_LABELS era una tabella fissa mentre i comandi sono rimappabili su user://keybinds.cfg — mappa su R, tutorial che diceva T; ora get_label() legge l'InputMap vivo con as_text_physical_keycode(), la stessa etichetta della schermata Opzioni, e siccome Ivan ha chiesto «sempre e ovunque» si sistemano anche i prompt [E] del mondo. Trovato per strada: l'azione "super" non stava in nessuna tabella e mostrava [SUPER] su tastiera e un tag vuoto su pad. Gatto+polizia: stun_by_cat() delega a world.net_scatter_coins() e world.pick_police_respawn_point() e si arrende in silenzio se mancano — TutorialWorld non le aveva. Il resto del KO: stazioni riorganizzate a blocchi con una sola spiegazione ciascuno invece di una modale per stazione, jolly tolto, cinque bidoni uno per tipologia (mosche comprese) con loot deterministico via Interactable.forced_bin_loot invece di reimplementare il bidone nel tutorial, energy drink sfuso rimosso, bagni ravvicinati e senza NPC attorno, fila di cinque passanti sopra la vineria per vedere la trasformazione, sbronza a 5 s, temporale a 5 s che parte davvero, testi spostati nella fascia nera sotto i cartelli delle zone. Le tre aggiunte fuori dal tutorial (InputManager, SuperPower, Interactable) sono inerti in città: flag a default spento. - [feat] Una build esportata parte solo sui dispositivi in lista, e le build vecchie si spengono da sole (SU-219, SU-221). Nuovo autoload
BetaGate, ultimo in project.godot, che si istanzia da sé la schermata di blocco: MainMenu.gd non è stato toccato di una riga, come imponeva il ticket. Il device_code è SHA-256(sale + OS.get_unique_id()) troncato a 12 hex e mostrato SU-XXXX-XXXX-XXXX — il sale non è cerimonia: i seriali Mac sono corti e da alfabeto ristretto, e senza sale un hash pubblicato si inverte a forza bruta. Attenzione al nome, perché è una trappola vera: nel progetto get_unique_id() compare nove volte ed è sempre multiplayer.get_unique_id(), cioè l'ID del peer di rete; OS.get_unique_id() è tutt'altro, quindi la funzione si chiama device_code() e mai unique_id. È il primo HTTPRequest del progetto: finora il gioco non faceva nessuna chiamata HTTP. Timeout 5 s a ogni avvio, con ricaduta sul via libera in user://beta_gate.cfg purché più fresco di 7 giorni, così un tester autorizzato può stare offline una settimana senza accorgersene. Fail-closed come deciso, senza valvole di sfogo: repo irraggiungibile, HTTP diverso da 200, JSON malformato o versione inattesa, e nessuna cache valida, il gioco non parte. SU-221 ci si innesta sopra come secondo controllo distinto: confronto fra config/version e ultima_versione fatto per componenti numeriche e non fra stringhe, perché "0.9" come stringa risulta maggiore di "0.28" ed è il modo classico di sbagliare questa cosa; prima di obbligatoria_dal il gioco parte con un avviso discreto, da quel giorno in poi non parte e la schermata dice dove scaricare invece di limitarsi a dire che è finita. Niente di tutto questo si attiva dall'editor (OS.has_feature("editor") è il primissimo controllo), quindi sviluppo, TestBot e compile-check non lo incontrano mai. Resta a Ivan riempire IVAN_DEVICES e popolare il repo della lista: finché sono vuoti, fail-closed chiude fuori anche lui. - [fix] Il gioco non spegneva mai EOS, e su Mac se ne accorgeva il tester all'uscita (SU-218). Il crash report arrivava tutto da
libEOSSDK: il processo usciva con la piattaforma ancora viva e connessa, l'SDK si smontava da solo negli atexit mentre i suoi thread di rete lavoravano ancora, e il main thread moriva in pthread_join aspettando un thread che nel frattempo dereferenziava un puntatore nullo. In tutto il progetto non c'era una sola chiamata a Platform.release()/shutdown(): l'addon le espone, non le invocava nessuno. Aggiunti quit_game() e shutdown_ordered() in EOSBridge — salvataggio prima di tutto, uscita lobby con timeout 2,5 s, due frame di respiro, poi release() e shutdown() per riflessione, coerente col «zero riferimenti statici» già in cima al file. Agganciato NOTIFICATION_WM_CLOSE_REQUEST con set_auto_accept_quit(false), così vale anche per ⌘Q e chiusura finestra e non solo per il bottone: era metà del difetto, perché il tester chiude col comando di sistema. Se EOS non è mai partito si esce subito, e se lo spegnimento non risponde entro il timeout si esce comunque — un'uscita appesa sarebbe peggio del crash. Lo spegnimento al ritorno al menu, che il ticket chiedeva solo di *valutare*, non è stato fatto: rischio concreto che l'SDK non consenta un re-init pulito nello stesso processo, e rompere rematch e nuovo host per curare un crash all'uscita sarebbe stato un pessimo scambio. - [fix] Il tutorial sbloccava contenuto vero, perché era una palestra che scriveva sul salvataggio (SU-216). Il cancello
run_active esisteva già in MetaProgress._tick() — col commento che diceva testualmente «il tutorial lo tiene a false, quindi non sporca la statistica» — ma copriva solo il conteggio delle run. Esteso a note_and_check(), note_shop_purchase(), ai quattro handler arresto/panchina/rapina/battesimo e a Interactable._use_gattile(), che incrementava gatti_consegnati scavalcando note_and_check() ed era l'ultimo buco rimasto. La scoperta non ovvia: il tutorial *battezza davvero* un superpotere, stessa EventBus.super_baptized del gioco vero, quindi senza cancello quel battesimo finiva a vita nel salvataggio. Il gate sta sui punti d'ingresso di gameplay e non su add_counter(), che è di basso livello e lo usa anche _tick(): gatarlo lì avrebbe spento run_totali. Verificato caso per caso che nessun punto gated venga sommato a fine run, quando run_active è già tornato false — è il modo in cui questo fix, fatto male, avrebbe spento in silenzio la progressione di tutti. - [fix] Tre «Lambda capture was freed» a ogni cerimonia del super chiusa in fretta (SU-217). I timer che ripuliscono i coriandoli erano
get_tree().create_timer(), cioè timer che vivono per conto loro tenendo in una lambda un riferimento alle particelle: se _close() le liberava entro i ~2,4 s, allo scadere la cattura non esisteva più — uno per sparata, da cui i tre errori. Ora il timer è un Timer figlio della particella, quindi muore con lei e non scatta nel vuoto. Rumore in console, non un crash, ma era rumore che sporcava ogni log di collaudo e nascondeva gli errori veri. - [fix] Il birillo non era disegnato male: era stato schiacciato (SU-205, KO). L'arte approvata in SU-202 è 13×30, rapporto 1:2,31. In gioco era 9×14, rapporto 1:1,55, perché il giro precedente l'aveva ridotta con un fattore non uniforme — 0,69× in larghezza contro 0,47× in altezza — e tutto il «completamente sproporzionato» del KO sta in quel divario. Ri-derivato
pinfly.png dal raw approvato con un resize uniforme NEAREST: 6×14, rapporto 1:2,33, cioè l'originale intatto solo più piccolo, senza ridisegnare un pixel. La cosa da ricordare è che ribalta una scelta dichiarata: 6×14 era stato valutato e scartato apposta perché «troppo sottile per leggersi mentre ruota in volo», e il KO dice il contrario — vince il KO, ma il compromesso resta vero ed è segnalato sul ticket. HIT_RANGE invariato: è un raggio di contatto centro-centro condiviso con gatto e bottiglia e dominato dall'ingombro del barbone, toccarlo sarebbe stato bilanciamento. - [docs] Il manuale cartaceo documentava una Street University di mesi fa (SU-212). Aggiunte tutte e nove le voci mancanti — carte e XP, superpoteri, giullare e carta oro, jolly, energy drink dai bidoni, bagno pubblico, lattine e La Baracca, multiplayer, tutorial — dentro l'impaginazione esistente, stesse 2 facciate A4 e stessi 2 libretti A3. Ogni voce è stata verificata nel codice prima di scriverla, non ricordata: super tenuto premuto 0,45 s, giullare al livello 9 con tre colpi, energy drink +15 energia, bagno +25 igiene a $5. Pagina 1 resta identica come layout; pagina 2 no, e va detto: per far entrare tre pannelli nuovi sono stati accorciati quattro pannelli vecchi e fusi SBRONZA e METEO, col corpo a ~6,7-7,4 pt contro i 7,5-9,5 di prima. Il criterio diceva «nessun pannello spostato», ma aggiungerne tre senza aggiungere pagine lo rendeva impossibile: il linguaggio grafico è lo stesso, la disposizione no. Escluse e dichiarate tre cose che il gioco non fa davvero (l'upgrade DOPPIA IDENTITÀ, i costi in lattine ancora «da confermare», la raccolta lattine in partita). I 3 PDF stanno solo in
TMP/manuale_provino/: quelli nella root non sono stati toccati e si sostituiscono solo dopo l'ok di Ivan. - [docs] Come si annuncia la beta chiusa ai tester (
/avvisa-tester). La prima stesura era un messaggio a sé con la guida per sistema operativo a recuperare il MAC address delle schede di rete, ed è stata buttata: il device_code di SU-219 è un hash salato di OS.get_unique_id() calcolato dentro il gioco, quindi non esiste da nessuna parte nelle impostazioni di Windows, macOS o Android e nessuna guida può farlo trovare. Lo mostra il Buttafuori al primo avvio, col tasto COPIA già lì: resta un blocco di quattro righe da appendere una volta sola all'annuncio della versione col cancello, uguale per tutti. La conseguenza da non nascondere ai tester è che la registrazione avviene dopo l'installazione, non prima — al primo avvio si viene bloccati per forza, e il messaggio lo dice apertamente perché nessuno lo scambi per un guasto. Unica variante prevista, per i tester segnati iPhone/iPad: su iOS il cancello non c'è (lo fa Apple) e non devono fare nulla. Registrato anche il seguito: codici nelle colonne T/U/V dell'Excel, poi genera_whitelist.py (SU-220).
La città si accende di notte, il barbone dice quando sta male, e il tutorial impara le novità2026-07-31
- [fix] L'ombra dei palazzi era «staccata e spostata» perché lo skew di Godot ruota attorno all'origine del nodo, non attorno ai piedi (SU-198, KO). Il KO di Ivan diceva «la allineerei sempre al palazzo, ora dagli screenshot vedo che è staccata e spostata»: il primo giro posizionava l'ombra come se lo skew non esistesse, ma
skew inclina l'asse Y attorno all'origine — e con centered = true l'origine è il centro dello sprite, non la base. L'asse diventa (−sin k, cos k), quindi il bordo-piede scivolava di mezza·sin k e si alzava di mezza·(1−cos k): l'errore era proporzionale all'altezza della texture, ~27 px di lato e 8 in alto su una torre, quasi nullo su un palazzo basso. Ecco perché sembrava un difetto capriccioso invece che sistematico. Ora il perno si ricava al contrario — si impone che il bordo-piede cada su (spr.position.x, base_y) e si risolve per il centro — e vale identico su tutte le 110 texture, da 68 a 231 px di altezza. Aggiunta la seconda richiesta del KO: ombra_direzione con il sole a sud-ovest (texture non ribaltata, schiacciata all'insù, skew specchiato), bottone «Sole▸» nel DebugPanel. Il problema interessante lì era lo z-order, risolto senza inventare un ordinamento nuovo: z_index = int(base_y) − 1 mette l'ombra sopra le facciate della fila dietro e sotto la propria. Limite misurato e dichiarato: fra due file di palazzi ci sono 192 px, quindi con le tarature di default l'ombra muore sull'asfalto e sale sulle facciate dietro solo con Lungh. 0,80 e Angolo −14°, consegnati come provino a parte. Il flag ombre_palazzi resta false di default: il ticket chiedeva provini, non una feature accesa. - [fix] Le insegne al neon coprivano mezza facciata: rimpicciolite a monte, non con lo scale (SU-210, KO). «Le insegne sono troppo grosse e sproporzionate rispetto al palazzo». La causa era doppia: gli sheet 128×24 danno frame da 64×24 su facciate larghe 96, e il cap «80% dello slot» non mordeva mai perché 96×0,80/64 fa 1,2 — cioè il limite era sempre sopra la dimensione reale e non limitava nulla. I frame sono ora rimpiccioliti una volta sola a monte a 48×18 con
Image.resize NEAREST e tenuti in cache, col nodo a scale 1.0. La divisione esatta per 2 — che è la regola generale su questo progetto — è stata provata e scartata con motivo: a 32×12 la scritta diventa una barra rossa illeggibile, e un'insegna che non si legge non è un'insegna. La seconda richiesta del KO non ha richiesto codice, ed è la parte che vale la pena raccontare: Ivan chiedeva che «al buio i palazzi risultino blu e passando con la luce tornino al colore originale nella parte illuminata», e quel comportamento c'era già — il WorldTint sta su CanvasLayer 5, quindi copre anche i palazzi, e il cerchio del barbone e le pozze dei lampioni ci bucano dentro con lo stacco netto e pixelato di SU-16. Misurato sul marciapiede: notte piena (33,38,65) → pozza lampione (95,87,72) → cerchio barbone (123,118,104). Consegnato uno screenshot che lo dimostra invece di scrivere codice che avrebbe duplicato un effetto esistente. - [change] Il giullare non è più l'intruso della città (SU-204, con la generazione approvata in SU-201). Era fuori scala e fuori stile perché era arrivato per una strada diversa dagli altri 96 archetipi e non aveva nemmeno un raw nel formato standard. Il difetto vero non era l'altezza ma la larghezza: 167 px contro i 104-127 di tutti gli altri, rapporto 0,77 contro 0,50-0,59. Il raw approvato (121×216, rapporto 0,56) è stato importato con la pipeline esistente e promosso in
sprites_raw/PEOPLE/, così il giullare smette di essere l'unico archetipo senza sorgente canonica. Dal manifest sono sparite due righe di commento che dicevano che l'importer «non è più nel repo»: era falso, e ora la voce è generata come tutte le altre. - [change] Il birillo del giullare è finalmente un birillo (SU-205 residuo, con la generazione approvata in SU-202). Lo sprite precedente a 18×28 era un rapporto 1:1,55 — un dado, non un attrezzo da giocoliere. Il disegno nuovo ha collo stretto, pancia e base, e la fascia al collo è viola invece del bianco/oro di prima: deviazione segnalata all'approvazione e accettata da Ivan. Portato in gioco a 9×14, la misura che SU-205 aveva già stabilito, con un rimpicciolimento non proporzionale scelto e dichiarato: a rapporto costante sarebbe uscito 6×14, troppo sottile per leggersi mentre ruota in volo.
HIT_RANGE, traiettoria, velocità e danno invariati — qui si è cambiato solo il disegno. - [asset] Generata la lanterna che il barbone impugna di notte (SU-214, in attesa di approvazione). Foglio 160×32 con una posa per ciascuna delle 5 direzioni, lanterne da 3×10 a 7×12 px — dentro la fascia 10-14 stabilita da SU-205 per gli oggetti che si tengono in mano. È un overlay condiviso da tutte le skin, quindi l'ancoraggio non poteva andare a occhio: i punti-mano sono stati misurati su
player_sheet.png (E 18,21 · SE 20,22 · S 22,21 · NE 11,22 · N 9,22) e passati a Codex come immagine-righello, perché su questo progetto i vincoli di dimensione scritti a parole vengono regolarmente ignorati. Intoppo utile da ricordare: il post-processing interno di Codex ha sovrascritto il PNG buono con un rimpicciolimento illeggibile, e il grezzo corretto è stato recuperato da ~/.codex/generated_images/ — è il motivo per cui la ricetta dice di verificare col mtime e mai con l'exit code. - [chore] Installato il plugin
superpowers e innestato nel nostro ciclo a pezzi scelti, non come metodologia. Richiesta di Ivan: «usiamo il meglio dei due mondi». Il plugin di Jesse Vincent porta 14 skill e un hook SessionStart che inietta ~700 token in ogni sessione dicendo al modello che «se c'è l'1% di probabilità che una skill si applichi, DEVI invocarla» — cioè esattamente il genere di spinta che durante /sprint litigherebbe con la nostra dieta token. Due cose lo rendono innocuo e l'hanno fatto tenere: le sue skill dichiarano loro stesse che CLAUDE.md e le richieste dirette di Ivan hanno la precedenza, e nei subagenti si autodisattivano (<SUBAGENT-STOP>), quindi i builder dei lotti non lo vedono nemmeno. Preso quello che copre buchi veri, rifiutato quello che duplica o contraddice. Dentro: systematic-debugging in /ko e nel fixer post-collaudo — vale soprattutto per la regola 3 fix falliti = ci si ferma e si mette in discussione l'architettura, che è la contromisura mancante ai 4 e 5 giri di KO sullo stesso ticket del 23 luglio; verification-before-completion e l'idea del reviewer-subagente di requesting-code-review, entrambe assorbite nella nuova skill di progetto chiusura-ticket; brainstorming riscritto in progetta-feature. Fuori, con motivo scritto perché non si riapra la discussione: test-driven-development (GDScript senza suite di unit test: qui il RED-GREEN è check_files.sh + TestBot + device), using-git-worktrees (git 2.13.1 su questo Mac vuole il worktree creato a mano), dispatching-parallel-agents e subagent-driven-development (le nostre hanno tiering di modello e tetto ai builder, le loro no — e il loro «agente fresco per task con doppia review» è il pattern che il post-mortem da 2,4M token ci ha fatto abbandonare), e finishing-a-development-branch vietata, perché propone merge e PR mentre da noi main si tocca solo dentro /release con l'ok esplicito di Ivan. - [chore] Due skill di progetto nuove in
.claude/skills/, che sono la parte davvero nostra dell'innesto. chiusura-ticket è il cancello prima di ogni «In revisione»: le nostre chiusure non falliscono per codice rotto — quello lo prende il compile-check — ma perché il ticket chiedeva un'altra cosa, quindi la skill impone di guardare il diff vero prima di credere al report dell'agente (un report è un'affermazione, non una prova: l'agente può essere morto a metà o aver scritto altrove), di spuntare riga per riga il testo del ticket e l'ultimo commento KO, e di allegare uno screenshot quando è UI. Sopra i 3 file la spunta si delega a tuttofare-haiku: è il trucco buono di requesting-code-review — il diff vive nel contesto del subagente e all'orchestratore tornano solo gli scostamenti — pagato però a tariffa haiku invece che con un general-purpose. progetta-feature copre il punto opposto del ciclo, le idee non ancora ticket: dialogo a domande singole, 2-3 strade con la raccomandata per prima, e stato terminale diverso dall'originale — non un piano di implementazione ma ticket Jira secondo SU-9, con il design lungo in OPUS_BRIEFS/ (dove sta già il contesto di progetto) invece che in docs/superpowers/specs/. La mappa completa di cosa si usa e dove sta in ORCHESTRAZIONE.md § «Skill esterne»; i punti d'innesto sono scritti nei comandi /sprint e /ko e nei file di fase 01, 02 e 03, così valgono anche quando la sessione non ha letto la mappa. - [change] Il protocollo dei ticket bocciati non dipende più da quale comando lanci:
/sprint li riconosce da solo. Ivan ha fatto notare che /ko non lo usa mai, e la verifica gli ha dato ragione su un punto e torto sull'altro. Ragione: /sprint leggeva già i commenti e sapeva già che l'ultimo KO ridefinisce il requisito, ma tutta la *disciplina* anti-KO — lotti piccoli invece dell'aggregazione per file, diagnosi prima del rimedio, controllo se il fix è già su disco, screenshot obbligatorio — viveva solo dentro /ko, cioè in un comando mai invocato: i suoi KO venivano letti bene e poi lavorati con la grana grossa dello sprint, che è esattamente il punto in cui nascono il secondo e il terzo giro. La correzione è stata separare cosa si lavora da come si lavora: il protocollo sta ora nella skill di progetto rilavora-ko e si applica per ticket, non per comando. Il riconoscimento non costa nulla — la JQL dello sprint porta già comment, quindi «ha un KO di Ivan, oppure ha un nostro commento-report precedente (era andato In revisione ed è tornato indietro)» è deducibile dai dati in mano, senza una query in più; nel dubbio si tratta come bocciato, perché il protocollo KO è più prudente e non più costoso. Dentro ci sono le cinque regole che facevano la differenza, compresa quella che nasce dal consuntivo del 23 luglio: al terzo fix fallito ci si ferma e si portano a Ivan la diagnosi e due strade, invece di tentare il quarto. /ko non è stato cancellato ma svuotato: resta come filtro di scope per il giro corto sui soli bocciati, e ora rimanda a sprint.md invece di duplicarne il protocollo — così le due strade non possono più divergere in silenzio, che è il modo in cui era nato il problema. - [feat] Di notte la città si accende: lampioni e insegne al neon (SU-210). È il seguito diretto di SU-16, il cui playtest diceva testualmente «diventa buio, non si vede niente, mancano le luci»: il buio era stato fatto, le luci no. Il problema vero non era disegnare la luce ma farla bucare la tinta notturna a schermo intero — una macchia chiara disegnata *sopra* il buio non è una lampada che illumina, è un adesivo. Lo shader del
WorldTint ha ora un array uniform lampioni[32] (centro in FRAGCOORD + raggio) testato sul pixel già quantizzato, così lo stacco resta netto e pixelato come il cerchio del barbone — la convenzione che Ivan aveva imposto col KO di SU-16, dove aveva bocciato l'alone morbido. Le sovrapposizioni si risolvono per massimo e non per somma, altrimenti due luci vicine si sbiadiscono a vicenda. In città finiscono ~70-100 lampioni agli angoli degli isolati, col palo disegnato proceduralmente (Image/set_pixelv, come già si fa per i cordoli) invece che generato: ognuno ha la propria soglia di accensione fra le 18:00 e le 19:30, quindi la città si accende a spizzichi e non con un interruttore. Le insegne al neon non sono costate un asset nuovo — c'erano già sette texture animate e AnimatedBuilding.set_off_state() — e ora sono grigie di giorno e accese di notte. Zero RPC: RNG separato seedato da seed_used più l'orologio già condiviso, quindi host e client vedono le stesse luci. Il ciclo per-pixel gira solo di notte e solo sulle luci inquadrate (bucket spaziali da 192 px). Scartato PointLight2D+CanvasModulate: avrebbe voluto dire buttare la tinta a schermo di SU-16 e con lei vignette, lampo, calura e gelo. - [feat] Il cerchio di luce del barbone respira come una candela (SU-211, parte). Tre seni incommensurabili — una fiamma non è periodica, e una sinusoide pulita si sente subito — più un guizzo saltuario, ampiezza 5,5%, raggio bloccato fra 0,86 e 1,14 del valore base, centro che si piega di ~3 px. Il bordo resta a stacco netto: l'effetto candela non è diventato una scusa per sfumare. In multiplayer il tremolio è locale e non sincronizzato, perché nessuno si accorge che la fiamma altrui balla diversa e sincronizzarla costerebbe rete per niente. Tutto tarabile a caldo dalla nuova sezione DebugPanel «LUCI NOTTURNE (SU-210/211)». La lanterna in mano non è stata fatta: serve uno sprite che non esiste, e l'arte nuova passa dal flusso a due ticket con l'approvazione di Ivan (SU-214).
- [feat] Una nuvoletta sopra il barbone quando fame o energia scendono sotto il 25% (SU-206). Le barre stanno in un angolo dell'HUD e il giocatore guarda il barbone, non l'angolo: si arrivava a fine run senza accorgersi di stare morendo di fame. Non nasce dal nulla, e questo è il punto: c'erano già
HungerLabel (la scritta «FAME», a soglia 20) e SleepZ (le Z, a una soglia 30 hardcoded e scollegata da tutto il resto) — cioè proprio le cose che Ivan giudicava poco evidenti. Sostituite da una nuvoletta unica: hot dog sotto 25 di fame, icona del sonno sotto 25 di energia, e con entrambe le stat sotto soglia una sola nuvoletta che si allarga con le due icone affiancate — mai due nuvolette che si contendono lo spazio sopra la testa proprio nel momento in cui devono essere leggibili. La soglia non è nuova: è WARN_LEVEL_ALARM con l'isteresi WARN_HYSTERESIS già esistente, quindi compare a 25 e sparisce a 30 senza sfarfallare. Spenta sui puppet MP (le stat degli altri non vanno in rete: zero RPC), da fantasma, nel tutorial e a game over. SleepZ resta in vita ma solo per il superpotere PISOLINO LAMPO. - [feat] Il tutorial impara le novità: bagno pubblico, carte, superpotere, jolly ed energy drink (SU-199). Era fermo a com'era il gioco mesi fa — chi lo finiva arrivava in città senza sapere che esistono le carte e il superpotere. Da 15 a 20 stazioni, con le novità avanzate in coda e la lattina di energy drink aggiunta alla stazione BIDONE esistente invece che in una nuova (è lo stesso bidone). Il vincolo che ha guidato tutto: il tutorial gira con
run_active = false, e i sistemi delle stazioni nuove sono gated proprio su quello. Accendere run_active sarebbe stato il modo più rapido di far partire timer, retata e wanted dentro il tutorial, quindi non è stato fatto: le carte si provano davvero (il motore grezzo XPSystem.add_xp() non è gated, si dà l'XP esatta di un level-up e si apre il ventaglio autentico) e il superpotere pure (una SuperCeremony dedicata che pesca 3 super veri e chiama baptize()). La stazione «usa il super» resta invece raccontata e non provata, perché l'attivazione richiede anch'essa run_active: meglio una stazione onesta che una run accesa di nascosto. 13 chiavi nuove tradotte in tutte e cinque le lingue. - [fix] Giocare il tutorial sporcava il salvataggio vero, e poteva sbloccare contenuto (coda di SU-199).
PowerUpSystem marcava le carte come già viste nel Quaderno e, via _note_meta_level, alimentava la ragnatela degli sblocchi: il tutorial arrivava a sbloccare carte vere. Ora non scrive più su MetaProgress quando run_active è false — che è la convenzione con cui il progetto distingue già il tutorial dalla partita, e che MetaProgress._tick() usava già così. Il rischio opposto era molto peggiore del difetto (un cancello sbagliato lì spegne in silenzio la progressione di tutti), quindi è stato verificato sul salvataggio vero e non a lettura: una run SP con 6 level-up scrive cards_seen/cardmax_*, lo stesso identico percorso dentro il tutorial non scrive nulla. Restano scoperti i contatori non-carte (negozi, bidoni, gatti): il tutorial può ancora sbloccare un quartiere — difetto preesistente, ripro in SU-216. - [fix] Il joypad virtuale non arrivava alle schermate, e la diagnosi di partenza era mezza sbagliata (SU-208, SU-209). Verificarla ha cambiato la soluzione:
LevelUpScreen e NameEntry ascoltavano già le azioni giuste, e _do_press_action usava già Input.parse_input_event, quindi il tasto A confermava già. Il buco era solo il joystick, che premeva move_* con Input.action_press — quello cambia lo *stato* dell'azione ma non genera un InputEvent, quindi event.is_action_pressed() dentro _input non scattava mai. È anche il motivo per cui ogni schermata si era dovuta fare i propri bottoni touch su misura. Ora il joystick emette un InputEventAction one-shot ui_left/right/up/down a ogni colpo di levetta — mai move_*, e il perché conta: un InputEventAction scrive lo stesso stato di action_press, quindi emettere move_* avrebbe schiacciato la forza analogica a 1.0 e il release avrebbe spento la camminata. LevelUpScreen.gd e SuperCeremony.gd: zero righe toccate. - [feat] Nuova voce OPZIONI «DIMENSIONE TESTO»: il testo si adatta a quanto è grande lo schermo davvero (SU-207). Su iPhone il testo era piccolo per fisica, non per un errore di layout: con viewport 1024×768 e stretch
canvas_items/expand, in proporzione allo schermo è grande come su iPad — è in centimetri veri che è 2-2,5 volte più piccolo. Scartata la scala dei font del tema, e con un dato: ci sono ~90 add_theme_font_size_override in GDScript e ~20 override nelle scene che bypassano il tema, quindi scalarlo non avrebbe ingrandito quasi nulla di ciò che si legge e avrebbe fatto traboccare proprio le poche etichette non-override. Usato Window.content_scale_factor, che non cambia nessuna proporzione: «niente testo tagliato», in cinque lingue, per costruzione. AUTO punta a una dimensione fisica come fa già VirtualControls._auto_scale() per il pad: 1.0 su Mac e iPad, ~1,3 su telefono. Da guardare su device: a 1,3 si ingrandisce tutto, mondo compreso, quindi si vede una fetta di città più piccola — è scritto anche nel testo d'aiuto in-game, ed è una scelta di gusto che decide Ivan. - [fix] La bottiglia del tossico e il birillo del giullare non sono più alti quanto un uomo (SU-205). La misura diceva tutto:
bottlefly.png era 8×28 e pinfly.png 18×28, mentre un personaggio è un frame 32×32 e la figura lo riempie tutto — cioè oggetti da lancio alti quanto 7/8 di un essere umano. Ridimensionati a monte con nearest-neighbor a 4×14 e 9×14: divisione esatta per 2, l'unica che non impasta la pixel art, e non a runtime (uno scale frazionario sfoca — a 0,4× una bottiglia da 8 px di larghezza diventa 3 px sporchi). HIT_RANGE lasciato a 18 di proposito: è un raggio di contatto centro-centro condiviso col gatto, dominato dall'ingombro del barbone e non da quello del proiettile, e ritoccarlo cambierebbe quanto spesso il tossico ti prende — cioè bilanciamento, fuori dal perimetro del ticket. - [test] Tre generazioni Codex in temp, in attesa dell'approvazione di Ivan (SU-202, SU-201, SU-200; il «Fatto» è l'approvazione). Il giullare è stato il caso interessante: il ticket diceva «fuori scala» e il raw «non esiste», e tutte e due le cose erano imprecise. Il raw c'era (in
temp/, 1254×1254 magenta, formato standard) ed è stato usato come riferimento identità; e misurando la cella idle SUD di sei archetipi è venuto fuori che l'altezza del giullare era giusta (217 px, identica a turista e poliziotto) mentre la larghezza era 167 px contro i 104-127 di tutti gli altri, buttafuori compreso — rapporto 0,77 contro 0,50-0,59. Non era alto: era tozzo. Col numero nel prompt il risultato è 121×216, rapporto 0,56, camminata 5/5 e deriva scesa da 96 px a 2. Nota di metodo che vale per le prossime generazioni: il vincolo di larghezza scritto a parole è stato ignorato, ha funzionato solo allegando un'immagine-righello. Sul barbone (SU-200) la scoperta è stata un'altra: arch_player passava già entrambi i controlli, e il tentativo bocciato di SU-188 era in realtà *peggiore* dell'originale — quindi la rigenerazione è stata consegnata come «passo più leggibile», non come riparazione di un bug. - [chore] Android diventa una piattaforma vera: APK firmato, con EOS a bordo, dentro la pipeline di release. Ivan ha aggiunto il preset e l'editor ha risposto con cinque errori su JDK e Android SDK — ma quelli erano la punta. Il Mac non aveva nessun Android SDK, e i due JDK installati erano entrambi inutilizzabili: JDK 26 e JDK 7, mentre il build template di Godot 4.6 dichiara
javaVersion : JavaVersion.VERSION_17 con Gradle 8.11.1. Sistemare i due percorsi avrebbe portato dritti al muro successivo. Installati senza sudo JDK 17 Temurin e SDK completo (platform-tools, build-tools 35.0.0, platform android-35) — il primo giro di sdkmanager è morto con lo zip di Platform 35 corrotto a metà download, ripetuto pacchetto per pacchetto. Il vincolo che ha deciso l'architettura è EOS: l'SDK Epic per Android arriva come .aar, e un .aar non entra in un APK preconfezionato — quindi use_gradle_build è obbligatorio, e con lui il build template Gradle in files/homeless_city/android/, con dipendenze androidx, schema di redirect del login Epic costruito dal client_id, e System.loadLibrary("EOSSDK") + EOSSDK.init() dentro GodotApp.java. La patch che nessun documento menziona l'ha trovata solo l'export vero: l'AAR di Epic pretende il *core library desugaring*, il README dell'addon è fermo a una AGP precedente e non ne parla, e senza le due righe (coreLibraryDesugaringEnabled + desugar_jdk_libs) Gradle muore su checkStandardReleaseAarMetadata. Risultato verificato sull'APK prodotto e non sul messaggio di successo: 148 MB, firmato col certificato di release, com.divano.streetuniversity, versionCode 26 / versionName 0.26 presi dalla versione di progetto, targetSdk 35, solo arm64-v8a, i tre permessi di rete richiesti da EOS nel manifest, libEOSSDK.so e libeosg.android…so a bordo, icona adattiva con background e foreground. - [chore] Il template Android pesa 200 MB rigenerabili, quindi le personalizzazioni non potevano vivere lì dentro.
files/homeless_city/android/ è finita in .gitignore (il grosso sono i .aar di Godot in libs/), ma le patch EOS non sono rigenerabili: stanno in tools/android/installa_template_android.sh, che reinstalla il template da android_source.zip e ci riapplica sopra le cinque modifiche, è idempotente e è stato collaudato davvero da template vergine, non a lettura — cancellazione completa, reinstallazione, cinque patch applicate. Il pre-volo di export_all.sh ora controlla che il template ci sia e che abbia le patch, perché un template senza patch produce un APK che si installa e poi non fa multiplayer: fallire in silenzio era lo scenario peggiore. Stesso ragionamento su version/code, il contatore intero con cui Android decide se una build è un aggiornamento: vive in export_presets.cfg (gitignored, quindi invisibile nelle diff) e nessuno si ricorderebbe di toccarlo al bump di versione — ora il pre-volo lo ricalcola da config/version a ogni export, entrambe le strade provate (già allineato → conferma; disallineato → corretto). Il keystore di release sta fuori dal repo, e le sue credenziali arrivano dalle variabili d'ambiente che Godot legge già di suo (GODOT_ANDROID_KEYSTORE_RELEASE_*), lette da un file a permessi 600 — così non finiscono né in git né in export_presets.cfg. Nuovo publish_apk.sh: nessun impacchettamento (l'APK è il pacchetto), ma verifica con apksigner che sia firmato prima di pubblicarlo in ANDROID/ sui due cloud, perché un APK non firmato non si installa e il tester vedrebbe solo un errore generico. Android è un canale come gli altri, non un extra: cartella ANDROID/ accanto a MAC/ e WIN/, target di default di export_all.sh passato da «mac win» a «mac win android», e la parte Android scritta in /release, /esporta, /avvisa-tester e nel cheat sheet di WORKFLOW/README.md. Con una differenza voluta rispetto agli altri due: l'APK va SOLO su Google Drive. Apple non pubblica un'app iCloud per Android, quindi da lì un tester potrebbe arrivarci solo via iCloud.com nel browser — un giro scomodo che su 148 MB spesso non completa; tenerne una copia su iCloud avrebbe solo aggiunto un file che nessuno può usare e che confonde chi cerca quello giusto. su_pubblica accetta ora un terzo argomento per i cloud di destinazione (entrambi di default, così dmg e zip non cambiano di una virgola) e, quando Drive è l'unico canale e manca, lo dice con un messaggio dedicato invece di lasciarlo fra i ⚠ generici: per Android non c'è un secondo cloud a fare da rete. Nell'avviso ai tester sono finiti anche i due passaggi che sembrano errori e non lo sono — «origini sconosciute», che dall'Android 8 è un permesso *per app* e va dato a quella che apre il file, e Play Protect con «Altri dettagli → Installa comunque». Il prezzo del default è dichiarato invece che scoperto: Android passa da Gradle, quindi il giro di default dura minuti e non secondi, e siccome il pre-volo gira su tutti i target prima di esportarne uno, un problema di firma o di template ferma anche mac e win — è voluto, meglio accorgersene prima di spedire mezza release. Due cose trovate per strada e sistemate: project.godot dichiarava config/icon="res://icon.svg" e quel file non esiste (l'icona vera è res://iOS/icon.png, da cui sono state generate le icone Android — legacy 192 e adattiva 432 con l'arte dentro la zona sicura del 66% e fondo campionato dal cielo dell'illustrazione); e la regola git per android/ è stata ancorata con la barra iniziale, perché senza avrebbe ingoiato qualsiasi cartella android/ a ogni livello del progetto — comprese quelle degli asset. - [chore] L'anagrafica tester si prepara alla beta chiusa: «Nome» e «Nickname» separati, tre colonne «Device», e i messaggi che chiamano la gente come la chiami tu (parte di SU-220, ticket aperto per il generatore). La colonna B era «Nome / Nick» e mescolava due cose: ora B è Nome (nome e cognome veri) e la nuova C è Nickname.
genera_link_messaggi.py saluta col soprannome quando c'è — «Ciao Ale!» invece di «Ciao Alessandro!» — e altrimenti torna alla prima parola del nome; un «—» nella cella vale come vuoto. Il soprannome si usa intero mentre dal nome si prende solo la prima parola, e non è una svista: il soprannome è già la forma breve, il nome in colonna è nome e cognome e «Ciao Marco Bianco!» suonerebbe come una raccomandata. In coda le tre colonne T/U/V «Device 1-3», fino a tre dispositivi a testa, che alimenteranno la lista del cancello beta (SU-219). Le lettere di tutte le colonne dopo la B sono slittate di uno, ma niente si è rotto per una ragione strutturale: lo script cerca le colonne per intestazione, non per lettera. - [fix] Spostare una colonna in un .xlsx con openpyxl perde pezzi in silenzio, e uno l'ha trovato solo il confronto degli stili.
insert_cols() sposta i valori ma non sposta collegamenti ipertestuali, celle unite, convalide dati, larghezze di colonna e blocco riquadri: tutti da staccare e riattaccare a mano. Il primo giro è finito con i 14 link (email e link WhatsApp/Telegram) rimasti alle vecchie coordinate e i mailto: finiti dentro la colonna Nickname — ripreso dal backup e rifatto staccando i link prima e riattaccandoli spostati dopo, con verifica che tutti e 14 puntassero ancora al bersaglio giusto. Il difetto insidioso però era un altro: le celle dentro un raggruppamento vecchio sono di sola lettura, quindi le due intestazioni unite di riga 1 — «PIATTAFORME» e «CANALI BUILD» — erano sparite senza un errore. Il confronto riga per riga dei valori non le vedeva, perché guardava dalla riga 3 in giù; sono saltate fuori solo confrontando anche gli stili fra il file prima e dopo, ed è la ragione per cui quel controllo è stato fatto invece di fidarsi del salvataggio riuscito. Esito finale verificato: zero differenze su 198 righe di dati, stili identici, menu a tendina e blocco riquadri alle coordinate nuove. - [chore]
BETATESTING/ non era davvero in .gitignore, pur dicendolo in due posti. Sia il README.md della cartella sia il foglio Legenda dell'Excel dichiaravano «è già inserita in .gitignore nella root»: non era vero, la cartella risultava solo *untracked*, cioè a un git add distratto di distanza dal finire su GitHub con dentro email, telefoni e — da oggi — i codici dispositivo dei tester. Regola aggiunta davvero; nessun file era mai stato committato, quindi non serve riscrivere la storia. - [chore] La lista dei dispositivi della beta chiusa ora si genera dall'Excel, e non si può generare rotta (SU-220). Nuovo
tools/release/genera_whitelist.py e skill /lista-beta. Scrive due file diversi, ed è la distinzione che regge tutto: devices.json — pubblico, sul repo street-university-beta-devices, con i soli codici — e BETATESTING/devices_mappa.csv, la corrispondenza codice-persona che resta sul Mac dentro la cartella in .gitignore. Niente nomi nel file pubblico: gli hash da soli dicono solo *quanti* dispositivi sono autorizzati, un elenco di nomi di battesimo direbbe chi è nella beta. La validazione blocca la scrittura invece di segnalare e proseguire, perché il cancello in arrivo (SU-219) è fail-closed e un file sbagliato fermerebbe tutti i tester insieme: meglio non pubblicare niente. Collaudata su una copia sporcata apposta, intercetta tutti e tre i casi con riga e colonna — formato non valido, stesso codice su due persone diverse, codice su una riga senza nome — e non scrive nulla. Lo script timbra anche ultima_versione e obbligatoria_dal (generazione + 7 giorni) per SU-221. - [fix] Il codice nell'Excel era giusto, ed è stato "corretto" con uno sbagliato: la lezione vale più dell'errore. Nella riga di Ivan c'era
SU-b8ce-97ca-2b08, letto dalla schermata del gioco. È stato scambiato per un MAC address travestito — la forma è quella, dodici cifre esadecimali a gruppi di quattro — e sostituito con un codice ricalcolato a mano. Due passaggi mancanti hanno trasformato un sospetto in un guasto: nessuno ha verificato che quel valore fosse davvero fra i MAC della macchina (non lo era: ifconfig non lo elenca), e nessuno ha guardato se BetaGate.gd esistesse già — SU-219 era stato implementato e usa il sale StreetUniversity::BetaGate::SU219::2026, mentre il ricalcolo ne usava un altro. Risultato: la lista pubblicata conteneva un codice che nessun dispositivo al mondo produce, e il Buttafuori ha fatto il suo dovere respingendo il Mac di Ivan. La regola che ne esce, ed è quella scritta ora in /lista-beta e in WORKFLOW/04_RELEASE.md: il device_code lo produce SOLO il gioco. Non si ricalcola, non si deduce, non si corregge — nemmeno quando "si vede" che è sbagliato. Se un codice sembra strano, si chiede al tester di rileggerlo dalla schermata, e basta. Il controllo del lock si è invece guadagnato la paga il primo giorno: l'Excel era aperto, lo script si è fermato, e salvando da Excel una scrittura fatta da script è stata puntualmente persa — esattamente il difetto da cui protegge. - [test] Collaudo unico dello sprint: 15/15 al compile-check, tutti i ticket passano. Le 20 stazioni del tutorial attraversate una per una con un driver dedicato — il TestBot normale lì non serve, perché la sua autopartenza cambia scena verso
Main.tscn ogni volta che run_active è false, cioè sempre nel tutorial. Run notturna dedicata (22:00→06:00) per le luci: lampioni generati, zero errori nel ciclo per-pixel dello shader. Trovati due difetti preesistenti e non bloccanti, entrambi resi più visibili dal tutorial allungato: i contatori non-carte di MetaProgress senza cancello (SU-216) e tre Lambda capture was freed se la cerimonia del super si chiude entro ~2,4 s (SU-217).
Un kit per generare sprite da soli in ChatGPT, e la riga che il manuale insegnava al contrario2026-07-29
- [docs] Nuovo
KIT_CHATGPT/: sette documenti che permettono a Ivan di rigenerare gli sprite da solo nel browser, senza consumare quota Claude. Richiesta esplicita («vorrei gestire in autonomia per oggi la generazione tramite ChatGPT»), con l'obiettivo di attaccare le sheet che hanno la camminata rotta. Il kit non rimanda al file master ma lo sostituisce per questo compito: NPC_AI_PROMPTS.md pesa 133 KB e caricarlo intero fa perdere al modello le regole che stanno nel mezzo, quindi 01_MESSAGGIO_SETUP_NPC.md (setup del thread) e 02_PROMPT_RIGENERA_CAMMINATA.md (un messaggio per personaggio) sono autosufficienti e si incollano così come sono. Completano il kit la lista misurata delle sheet da rifare, le frasi pronte di troubleshooting, l'equivalente per gli edifici e il giro post-generazione (dove salvare, come verificare, come importare). I due file master sono confermati e scritti nero su bianco perché la domanda tornava: NPC_AI_PROMPTS.md per i personaggi, BUILDINGS_AI_PROMPTS.md per i palazzi. - [docs]
KIT_CHATGPT/07_ISTRUZIONI_CHATGPT_WORK.md: le stesse regole, ma per ChatGPT che lavora sulla cartella invece che sulla chat. Ivan ha chiesto di sfruttare la modalità Work — dare a ChatGPT il progetto e istruirla su cosa leggere, cosa guardare e quali comandi accettare, senza più incollare i prompt. Il file è pensato per essere l'unica cosa che le dici («leggi 07_ e seguile»), e da lì lei si prende il resto. La sezione che sta in cima non è il compito ma il perimetro di scrittura, e sta lì per un motivo documentato: un agente con accesso alla cartella tocca file che nessuno gli ha chiesto — può scrivere solo in KIT_CHATGPT/generati/, il resto del progetto non lo vede nemmeno, i manuali si segnalano ma non si correggono, e i comandi git sono vietati (i raw stanno in LFS). Comandi accettati ridotti a sette (stato, genera <nome>, genera prossimo, rifai riga N di <nome>, di nuovo, controlla, genera edificio). Due modi di fallire sono scritti dentro invece che scoperti sul campo: se non riesce a salvare il PNG deve dirlo e indicare percorso e nome esatti invece di arrangiarsi (un nome sbagliato manda all'aria l'import), e se non può eseguire comandi deve dichiararlo subito e affidarsi alla checklist visiva. Aggiunto un STATO.md di sessione, così una chat nuova sa dove eravamo rimasti — con il vincolo che lo stato massimo che può scrivere è «in attesa di approvazione»: il «Fatto» lo mette solo Ivan. Il punto d'ingresso è AGENTS.md, che ora dirotta le richieste di generazione immagini su questo file invece che sulla lettura dei brief di sviluppo. - [test] Il kit è stato collaudato con una generazione vera, e il collaudo ha trovato un difetto che nessuno aveva visto. Codex avviato con
cwd sulla sola cartella del kit, messaggio di avvio identico a quello documentato, comando genera arch_bouncer_v1 — scelta apposta la peggiore delle 31, con 4 righe rotte su 5. Il protocollo ha tenuto senza aggiustamenti: al messaggio di avvio ha letto le istruzioni, riassunto lo stato e non ha generato; poi ha salvato col nome esatto, creato STATO.md nel formato previsto e scritto «in attesa di approvazione» invece di «Fatto». Il perimetro pure, verificato per differenza: dall'avvio in poi ha scritto esattamente due file, il PNG e lo stato. Sulla camminata: da 4 righe rotte a 0, misurato in proprio e non riportato da Codex. Il difetto l'hanno trovato i margini di un test, non l'occhio: la metrica del baricentro dell'incarnato di SU-47 qui non discrimina — il buttafuori è calvo, la testa è tutta pelle e il baricentro non si sposta col verso — quindi è stata sostituita da un confronto che risponde davvero, «questa cella somiglia di più all'originale o al suo specchio?». Verdetto: nessuna riga specchiata, ma su SUD-OVEST e NORD-EST il vantaggio sullo specchio è quasi nullo (60,5 contro 62,6; 73,4 contro 73,8), e c'è una sola lettura possibile — quelle celle sono quasi simmetriche, cioè le viste 3/4 sono girate troppo poco e leggono quasi frontali. Da qui due aggiunte al kit: un blocco BODY ROTATION nel prompt (copiare la *quantità* di rotazione, non solo il verso) e la frase pronta 4bis nel troubleshooting. Lo sprite resta in generati/: nessun import, il giudizio è di Ivan col VisoreNPC. - [test] PixelLab riprovato e ristaccato in giornata: copia benissimo, ma non esegue i cambi di posa — e il perché vale anche per Codex. Ivan ha riaperto la strada dopo aver capito che l'errore storico era la dimensione richiesta (68×68 sui personaggi vecchi, contro i 256×256 che lo strumento produce chiedendone 128×128), quindi il divieto del 2026-07-26 è stato revocato, il server riagganciato e — dopo le prove — ristaccato su sua richiesta. Due esperimenti sulla stessa cella, quella che a Codex era costata più giri. Il primo,
edit_image con «ridisegnalo identico ma scambia le gambe»: fedelta' altissima e misurata (stessa altezza al pixel, massa +1,3%, palette indistinguibile) e gambe non scambiate, cioe' lo stesso modo di fallire già visto su Codex e Nano Banana. Il secondo è un'idea di Ivan per aggirare la copia: amputare le gambe dalla vita in giu' e obbligarlo a inventarle. Le ha disegnate bene — scarpe viste da dietro, falcata ampia, nessun artefatto — ma nella stessa disposizione di prima, e allungando la figura del 12% in massa. La conclusione è più utile del risultato: il problema non è che il modello ricopi il riferimento, è che non sa mappare «gamba sinistra/destra» su una figura di spalle — con le gambe cancellate, e nessuna posa da copiare, ha ricostruito comunque quella. L'unica formulazione che ha funzionato in tutta la giornata è geometrica: «il piede più in alto deve essere quello della metà sinistra della cella». Chiamate fatte via HTTP diretto, perché un MCP aggiunto a caldo non si attiva nella sessione in corso. - [test] Le due celle bocciate da Ivan sono state corrette, e la correzione chirurgica ha funzionato davvero: 24 celle su 25 bit-identiche. Sul primo sheet Ivan ha bocciato riga 1 (SUD) colonna 4 e riga 5 (NORD-EST) colonna 4, entrambe con la stessa gamba avanti della colonna 2. Il primo tentativo ha corretto solo SUD, e il motivo è istruttivo: a NORD-EST avevo scritto «va avanti verso l'angolo in alto a destra», ma in una vista di spalle il piede avanti sta più in alto, non più avanti — l'istruzione era ambigua proprio nella vista che il progetto aveva già segnato come la più difficile. Riformulata in termini di altezza ha funzionato al primo colpo. La misura che conta: fra il secondo e il terzo giro è cambiata una sola cella (differenza 4,66) e le altre ventiquattro sono a 0,00, cioè identiche al bit. La storia del progetto diceva che sistemare una riga ne rompe altre; con la striscia identità e un'istruzione descritta da zero, non è successo. Perimetro rispettato in tutti e tre i giri.
- [test] Uno strumento nuovo tentato, sembrato buono e RITIRATO: la validazione che convinceva era sbagliata.
verifica_camminata.py aveva dato NORD-EST «OK al 44%» su una cella che Ivan ha bocciato — differivano braccia e busto, non i piedi: è la trappola nota del «quanto differiscono» invece del «cosa fanno le gambe». Ho costruito il controllo che sembrava rispondere alla domanda giusta (la fascia gambe della colonna 4 somiglia di più alla colonna 2 o alla colonna 2 specchiata?) e sui primi confronti classificava correttamente tutti i casi noti. Poi si è scoperto perché: quella taratura confrontava la colonna 4 di un file con la colonna 2 di un *altro* file, e i verdetti giusti venivano da lì. Rifatto sullo stesso file, sull'unico sheet con verità nota azzecca SUD e sbaglia proprio NORD-EST, il caso che l'aveva motivato. Cancellato invece che consegnato: un controllo inaffidabile dentro il kit avrebbe prodotto la stessa falsa sicurezza del difetto che voleva curare. Il punto che resta, e che il progetto aveva già concluso: sulla singola cella nessuna metrica provata finora decide, il giudice è l'occhio nel VisoreNPC. - [fix] Le correzioni di una cella lasciavano un alone, e la cura provata per prima era peggiore del male. Ivan ha bocciato le due celle corrette non per la posa ma per il metodo: il generatore non ridisegna la cella, ci incolla una toppa, e il bordo della maschera resta come un anello di magenta di tonalità diversa; nella riga 1 era rimasto anche un pezzo di piede della versione precedente. La misura che avevo usato per escludere l'alone era sbagliata: mediavo la distanza del fondo da magenta puro su tutta la cella, e un anello tenue su un'area piccola sparisce in quella media — misurando invece lo scarto dal colore dominante della cella, le due celle rattoppate davano 2,8% e 5,3% contro l'1,5-1,7% di quelle intatte. La prima cura — generare le celle a parte e rimontarle — è stata scartata dopo averla costruita e misurata, e per tre motivi che nessuno aveva previsto: nel foglio il personaggio scivola orizzontalmente di colonna in colonna (testa a x=164, 146, 124, 101, 84), quindi ancorare alla colonna 1 sposta la cella a destra; la figura generata fuori dal foglio ha proporzioni più snelle (a parità di altezza, massa -13%) e nessuna scala lo corregge; e le figure sbordano oltre il bordo nominale della cella (nella riga 1 arrivano a y=259 dove il quinto finisce a 251, e lo fanno tutte le colonne), quindi sostituire il solo rettangolo nominale lasciava fuori i residui. Quello che regge è la correzione dentro il foglio seguita da una pulizia deterministica: nuovo
KIT_CHATGPT/strumenti/pulisci_cella.py riempie lo sfondo della cella col colore di fondo dominante del foglio — alone da 2,8% e 5,3% a 0,0%, nessun altro pixel toccato — mentre il residuo *saldato* alla figura, che la pulizia per costruzione non tocca, si risolve solo facendolo ridisegnare. Entrambe le cose sono ora nel kit: passo di pulizia obbligatorio in 02_ e 07_, frase pronta 4ter in 04_, limiti scritti nel docstring dello script. - [chore]
KIT_CHATGPT/ è diventata una cartella chiusa: si dà a ChatGPT così com'è, senza esporle il resto del progetto. Richiesta di Ivan («così sono sicuro che le do solo quel perimetro»), ed è il modo giusto di leggerla: il perimetro fisico vale più di quello scritto. Le regole in cima a 07_ restano, ma ora il danno peggiore possibile è un PNG brutto in generati/, perché dentro la cartella il gioco non c'è. Ci sono finiti gli esempi di stile (esempi_stile/), le 31 strisce identità (identita_idle/), la destinazione dei nuovi PNG (generati/), il manifest e gli esempi degli edifici (edifici/) e gli strumenti di controllo; tutti i percorsi dei documenti sono stati riscritti relativi alla cartella. Gli strumenti sono copie dichiarate, non collegamenti: verifica_camminata.py misura le pose passando dalla stessa pipeline di ritaglio dell'importer vero, quindi si è portato dietro anche import_chatgpt_sprites.py — spezzare quell'ancoraggio avrebbe fatto smettere al verdetto di dire la verità sul gioco. Con una differenza voluta e una sola: nella copia dell'importer l'avvio diretto è disattivato e spiega dove si fa l'import vero, perché lanciata per sbaglio da lì scriverebbe sheet inutili dentro il perimetro; le funzioni restano identiche. Data e istruzioni di riallineamento in strumenti/LEGGIMI.md, così la copia non invecchia in silenzio. Le immagini della cartella (24 MB) sono fuori da git: sono copie di roba che sta già in LFS o file rigenerabili con estrai_colonna_idle.py, e LFS copre solo sprites_raw/ — versionarle significherebbe portarsi dietro duplicati come blob normali. - [fix] Il manuale chiedeva la riga 4 verso SUD-EST, la pipeline la vuole verso SUD-OVEST: chi lo seguiva alla lettera produceva quella riga girata. Lo STYLE ANCHOR di
NPC_AI_PROMPTS.md — il blocco che per protocollo va copiato in testa a ogni prompt — descriveva la riga 4 come SE con «naso in basso a destra», diagramma a frecce compreso. L'importer però dichiara SOURCE_ROW_DIRS = ["s","n","e","sw","ne"] con MIRROR_ROWS = {"sw"}: si aspetta il SUD-OVEST e la specchia lui per ricavarne la SE. Le due cose non possono essere entrambe vere, ed è coerente con i KO «est e ovest invertiti» di SU-47, che su quella famiglia di righe sono tornati più volte. La nota del 2026-07-23 in cima alla sezione 0 diceva già «4 SUD-OVEST», ma il corpo dell'anchor la contraddiceva dieci righe più sotto — e il corpo è quello che finisce nel prompt. Corretti riga di layout, cheat sheet orientamento, diagramma delle frecce, blocco di verifica finale e tutti e nove i blocchi AVOID sparsi negli archetipi che vietavano esplicitamente le viste southwest, cioè proprio quella richiesta. Aggiunta anche, dentro la sezione COLUMNS, la nota che il ciclo ridefinito da Ivan ([fermo, passo, fermo, passo opposto, fermo]) supera le cinque pose diverse descritte lì sotto: prima quell'informazione esisteva solo nel cappello della sezione, quindi non seguiva l'anchor quando lo si copiava. - [chore] Nuovo
tools/estrai_colonna_idle.py, che nasce da un errore già pagato: allegare la sheet rotta come riferimento fa ricopiare anche il difetto. È misurato dal giro Nano Banana — il modello ricalca il riferimento entro il 4,4%, ciclo di camminata compreso — e la contromisura trovata allora era passargli solo la colonna idle, che porta l'identità del personaggio senza nessuno dei frame difettosi da imitare. Lo script estrae quella colonna da uno sheet raw 5×5 e la impagina come striscia verticale su magenta, riusando il rilevamento delle isole dell'importer vero (con ripiego a griglia fissa) invece di tagliare a quinti la tela. Generate le 31 strisce dei personaggi da rifare in KIT_CHATGPT/identita_idle/. - [test] Misura di tutte e 96 le sheet raw: 31 hanno almeno una riga con la camminata rotta, 50 sono solo sospette, 15 sono pulite. Passata completa di
tools/verifica_camminata.py su sprites_raw/PEOPLE/, 480 righe esaminate, CSV grezzo conservato nel kit. Le righe che si rompono non sono distribuite a caso: NORD e NORD-EST dominano la lista, cioè le viste dove le gambe si vedono meno e il modello risparmia. Il conto serve a rispondere alla domanda che SU-188 aveva lasciato aperta — «quanti sono quelli da rifare?» — con un numero invece che a naso, e a dare un ordine di lavorazione: i due bouncer con 4 righe rotte prima dei poliziotti con una sola. Le 50 sospette restano sospette e non sono state promosse a «da fare»: la soglia le mette lì per costruzione (tutti e cinque gli hipster e tutti e tre i punk hanno solo la riga NORD sospetta, che somiglia più a un limite della misura in vista di spalle che a un difetto vero), e il giudice resta il VisoreNPC.
L'export dei pacchetti diventa una skill, e si porta dietro il controllo che nessuno script fa2026-07-28
- [feat] Nuova skill
/esporta: i passi 6-8 della release (export, dmg/zip, pubblicazione ai tester) hanno il loro comando. Richiesta di Ivan subito dopo la release v0.26, dove quei passi erano rimasti fuori dal perimetro di /release. La skill non riscrive niente: tools/release/export_all.sh faceva già pre-volo, export headless e post-volo sul .pck, e --pacchetto incatenava già dmg e zip — quello che mancava era il protocollo attorno, cioè le cose che uno script non può sapere. Il controllo che aggiunge, e che nessuno script faceva: prima di esportare, verificare che config/version, la sezione in cima alle note in-game e il commit taggato concordino fra loro. È l'unico modo di intercettare l'errore che spedisce ai tester la build di ieri, e non è automatizzabile perché solo Ivan sa quale versione vuole spedire — se i tre non concordano la skill si ferma e chiede, invece di esportare. Seconda regola, sulla pubblicazione: --pacchetto sovrascrive i pacchetti nelle cartelle iCloud e Google Drive condivise coi tester, cioè manda roba fuori dal Mac di Ivan — la skill impone di chiedere l'ok esplicito prima, dicendo cosa si sta per pubblicare e dove, e offre il giro senza --pacchetto per chi vuole prima guardarsi la build. Trappole scritte dentro, invece che imparate sul campo: export_presets.cfg è gitignored e l'editor Godot lo riscrive (è così che le build Windows della v0.24 e v0.25 sono uscite senza note di rilascio e con l'online morto, quindi il pre-volo --controlla va rifatto dopo ogni giro con l'editor aperto); make_dmg.sh elimina la .app a fine giro e non è ripetibile; iOS esporta ma si finisce a mano in Xcode; cloud spento è un ⚠ e non un fallimento. Collaudata lanciando il pre-volo vero, non per lettura: export_all.sh --controlla dà include_filter a posto su entrambi i preset della v0.26 — e ha per giunta segnalato l'editor Godot aperto, cioè proprio il caso che la skill copre. Agganciata al workflow come /avvisa-tester: WORKFLOW/04_RELEASE.md (passo 6) e la riga «Build» del cheat sheet in WORKFLOW/README.md, che rimandava ancora all'export a mano da Godot. .claude/commands/esporta.md (nuovo).