← Novità

v0.25-dev

2026-07-26 · 8 voce/i di changelog

Bonifica PixelLab: sparisce da documentazione, permessi e configurazione MCP2026-07-26

  • [docs] PixelLab rimosso ovunque come strumento possibile: la generazione immagini è SOLO Codex, anche dove il lavoro era già impostato su PixelLab. Richiesta esplicita di Ivan, che estende la regola già recepita in convenzione 7: non basta non *proporre* PixelLab — qualsiasi cosa da fare o da leggere che lo prescriva (ticket in corso compresi) va generata solo ed esclusivamente con Codex. Bonifica in quattro mosse. Documentazione operativa: WORKFLOW/README.md e 05_SETUP_CLI.md non spiegano più come configurare l'MCP PixelLab — il passo 4-bis ora dice il contrario, cioè come *rimuoverlo* — UI_AI_PROMPTS.md perde l'«alternativa PixelLab» (le specifiche restano, vanno nel prompt Codex), QUARTIERI_DESIGN.md e PIANO_SVILUPPO_V2.md reindirizzano a Codex il pilota SU-60 e la traccia P1, 01_TICKET.md marca l'epic SU-6 come «solo Codex». Il brief P1_RESTYLE_PIXELLAB.md apre con un avviso in grassetto: nome file storico, ovunque dica PixelLab si legge Codex; soggetti, palette e GATE restano validi. Convenzione 7 estesa con la clausola sui ticket già impostati su PixelLab. Configurazione: server MCP pixellab rimosso da ~/.claude.json (entry col token salvata in ~/.claude/pixellab-mcp-entry-removed-2026-07-26.json, ripristinabile), permessi mcp__pixellab__* tolti da .claude/settings.local.json. Cosa NON si è toccato, apposta: le voci storiche di questo changelog, gli script tools/import_from_pixellab.py e simili (restano come cronaca della vecchia pipeline, con nota che li dichiara tali) e gli asset in sprites_raw/PIXELLAB/ — cancellare la storia renderebbe illeggibili i changelog che la citano.

Sprint 12: chiesa e banca scendono in strada, dieci palazzi nuovi rompono la monotonia, e in città si trova un bagno chimico2026-07-26

  • [feat] SU-185 (KO) — i landmark non sono più soprammobili da piazza: sono edifici del quartiere. Terzo giro del ticket, e il KO di Ivan lo ha ridefinito di nuovo («i landmark tipo chiesa e banca diventano edifici da inserire nel resto del quartiere, o in un isolato con singolo edificio o in mezzo ai grattacieli»). I due giri precedenti li avevano lasciati dov'erano — al centro delle piazze grandi — ed è proprio la posizione che il KO contesta: un landmark in mezzo a una piazza è arredo, non architettura. Ora chiesa, banca e fabbrica entrano in città in due modi, e nessuno dei due inventa logica di collisione nuova: isolato dedicato (nuovo BlockKind.LANDMARK, prenotato con lo stesso _reserve_footprint() che già garantisce negozi e gattile, con rete di sicurezza «almeno un edificio civico per mappa») e innestato nella prima fila dei super-isolati, cioè in mezzo ai grattacieli, dove la texture entra nella rotazione del packing esistente — _riempi_intervallo() non è stato riscritto, come il ticket imponeva. Sei chiavi nuove in Quartieri.LAYOUT_DEFAULTS (landmark_isolato_chance, landmark_infila_chance, landmark_max, landmark_veto, landmark_pref, landmark_pref_chance): la fabbrica è vietata su COLLINA, GATTOPOLI e SOLLEONE e preferita su FUMAROLA, la banca sta su CENTRO/COLLINA/NOTTEFONDA, la chiesa su MERCATO/BRINA/SOLLEONE/GATTOPOLI — decisioni per quartiere in tabella, non if sparsi nel generatore. La scoperta che avrebbe fatto un KO se non intercettata: i 3 PNG landmark non sono ritagliati al contenuto (disegno di 92-119px dentro una tela 128×128, con 12-14px di trasparenza sotto) e in fila coi palazzi — che sono ritagliati — sarebbero fluttuati sopra il marciapiede. Risolto a runtime con un AtlasTexture sul get_used_rect(), quindi nessun PNG toccato né rigenerato; taglie effettive 95×104 banca, 94×110 chiesa, 121×115 fabbrica. Scartati il downscale a 0,75 per far stare la fabbrica nel lotto 96px (sfrangia il pixel art) e la riscrittura del packing (vietata dal ticket). Anti-gemelli esteso a entrambi i modi: mai due landmark uguali di seguito. Le piazze restano, senza landmark ma con la fontana monumentale. Determinismo dal seme — vincolo MP, host e client devono generare la stessa città — verificato col provino esistente: 32/32 firme sha256 identiche, 8 quartieri × 4 semi. Seme 424242: 12 landmark, 7 in isolato dedicato e 5 in fila. Limite dichiarato: la fabbrica ritagliata è 121px su un lotto da 96 e sborda ~12px per lato sul marciapiede (largo 34) — mai sulla strada. WorldGenerator.gd, Quartieri.gd. Commit df45940.
  • [feat] SU-184 — le dieci varianti Codex entrano in rotazione e la monotonia dei quartieri finisce. Seconda metà del lavoro aperto da SU-175: SU-183 aveva generato gli sprite, qui si montano. Importate solo le 10 approvate — quelle generate con Codex, non le omonime PixelLab bocciate che restano sul disco: la regex di import matcha _codex e ignora le altre, così un rilancio dello script non può ripescare l'arte sbagliata. Entrano come building_<distretto>_w48b/c.png per commercial, industrial, nightlife, residential e slums; buildings_manifest.gd rigenerato con lo script Python, mai a mano, con diff puramente additivo (10 entry nuove, nessuna esistente toccata) e le dimensioni reali lette da PIL in BUILDING_SIZES — il dettaglio che decide il ticket, perché il packing filtra per larghezza e una taglia dichiarata male significa una variante che non verrà mai pescata. Catalogo da 6 a 8 varianti per ciascuno dei 5 distretti. Misura sul seme 424242, che è il vero criterio di riuscita: MERCATO 47,8→23,5%, FUMAROLA 36,2→20,2%, BRINA 44,6→25,3%, SOLLEONE 47,3→22,6%, NOTTEFONDA 40,6→20,3% — tutti sotto la soglia 25-30% chiesta. GATTOPOLI e CENTRO, che erano già equilibrati, non peggiorati. Nessun rischio MP: il manifest è un dato statico identico su host e client e l'ordine resta deterministico per resource_path; l'aggiunta di varianti sposta le pescate (roll % pool.size()) ma le sposta allo stesso modo dalle due parti. buildings_manifest.gd, import_buildings_sprites.py, 10 PNG in assets/sprites/world/buildings/. Commit fe4c092.
  • [feat] SU-181 — BAGNO PUBBLICO: cinque dollari, una lavata, un quarto di igiene. Sbloccato dopo SU-170. La cabina non è un edificio-isolato: un isolato è 96×96 e la cabina 21×45, piazzarla come blocco l'avrebbe fatta leggere come un cesso in mezzo a un parcheggio. È arredo interattivo sparso in piazze e parchi, con la stessa meccanica dei cestini, frequenza e tetto da due chiavi per quartiere (bagno_chance, bagno_max: FUMAROLA la più fornita, «zona cantieri», COLLINA quasi senza, il decoro non ammette cabine) e garanzia di almeno una per mappa costruita sulla rete di sicurezza già esistente per la fontana — perché sotto i $20 dei vestiti è l'unico modo di ricomprarsi igiene. «1/4 di vita» del ticket = +25 di igiene via GameState.wash(25): nel progetto non esiste una stat health, e l'azione è lavarsi. Prezzo base 5$ con gli stessi rincari di tier e quartiere degli altri negozi; senza soldi nessuna transazione e notifica comica, identico al ramo già collaudato di _use_clothes_store(). Punto d'uso davanti alla porta, non al centro: al bagno ci si entra dal davanti. Sprite a scala nativa, senza scale, perché la proporzione col barbone è esattamente ciò che Ivan ha chiesto di sistemare. Un difetto intercettato durante il lavoro: le cabine atterravano sui cestini, perché _blocked_spots raccoglie solo gli oggetti ingombranti — aggiunto un registro _prop_spots con la posizione di ogni interattivo creato. Seme 424242: 17 cabine, nessun quartiere scoperto. Determinismo 32/32 firme identiche. Debug F1: «Bagno pubblico» piazza una cabina accanto al barbone. Follow-up dichiarati: manca l'icona sulla minimappa (servirebbe un map_bagno.png, cioè un ticket di generazione), le cabine finiscono più spesso nei parchi che nelle piazze 96×96 perché lì la clearance da 44px degli arredi lascia poco spazio, e _note_shop("bagno") rende un filo più facile l'impresa «3 negozi diversi» del MERCATO. Interactable.gd, WorldGenerator.gd, Quartieri.gd, DebugPanel.gd, nuovi sprite in assets/sprites/world/. Commit 310bcd9.
  • [change] SU-180 (KO) — niente paracadute prima della retata: la super la danno solo la progressione e il giullare. Il giro precedente aveva lasciato in piedi una scorciatoia — negli ultimi 5 minuti prima della GRANDE RETATA la carta dorata entrava d'ufficio nel ventaglio del level-up, così chi non incontrava mai il giullare non finiva la run senza super. Ivan l'ha bocciata: «solo la progressione e il giullare danno la super». Rimossi alla radice RAID_PARACHUTE_SEC, should_offer_golden() e golden_card() in SuperPower, più l'iniezione nel ventaglio e il codice morto che ne dipendeva in LevelUpScreen (ramo golden al pick e riapertura del ventaglio). Resta una sola via, quella voluta: carta a livello 9 → compare il giullare → lo abbatti → la carta dorata cade a terra → la raccogli → cerimonia. Conseguenza accettata esplicitamente: chi non incontra o non batte il giullare finisce la run senza super, e non è un buco da compensare. Lasciata solo l'icona dorata nel render delle carte, perché scripts/tools/shot_cards.gd la usa ancora per fotografarla: il ramo di gioco era morto, quello di QA no. SuperPower.gd, LevelUpScreen.gd, DebugPanel.gd (commento che citava ancora riparo e dormita). Commit e6d25f1.
  • [test] Collaudo di fine sprint: PASS su tutti e quattro i ticket, nessuna regressione. Compile-check globale 86 script / 35 scene, 0 falliti. 2 run SP col bot su quartieri e modalità diverse (wander/FUMAROLA 120s, survive/BRINA 150s con calamità forzata): 0 SCRIPT ERROR, orphans=0 sempre, conteggi nodi/NPC/gatti in range storico. Il wave_kind atteso=cold osservato=heat FAIL comparso in una run non è una regressione: è il comportamento documentato di --bot-wave-after, che bypassa apposta il filtro cold/heat del quartiere. Determinismo dal seme verificato per la prima volta coi quattro lotti insieme — ciascun agente l'aveva verificato solo sul proprio — 18/18 firme sha256 identiche su 2 semi × 9 quartieri. Le tre verifiche a rischio sono passate: il caso dubbio della fabbrica (121px di sprite su un lotto da 96) è stato riprodotto su 24 semi e lo sbordo è sempre 12,5px per lato, zero overlap con altri edifici e mai oltre il marciapiede da 34px, quindi si passa; le percentuali di SU-184 rimisurate sul codice integrato — non su quello parziale su cui aveva misurato il suo autore — restano tutte sotto soglia, massimo 24,2% a SOLLEONE, con GATTOPOLI al 26,9% dentro fascia e CENTRO invariato; la catena della super percorsa end-to-end dal vivo (giullare → carta dorata → cerimonia) con grep a conferma che di RAID_PARACHUTE_SEC, should_offer_golden e golden_card( non resta traccia. Cabine del bagno: 17 su 9 quartieri, 0 scoperti, transazione verificata dal vivo con e senza soldi e cabina solida al giro attorno. Limite dichiarato: il multiplayer non è collaudabile in questa sandbox (hang noto di host_game()), quindi la replica MP dei landmark e delle cabine resta da provare su device. Screenshot in TMP/screenshots_claude/su_sprint12/, dettagli in TESTLOG.md.
  • [feat] SU-170 (KO, in attesa di approvazione) — la cabina scende alla misura di un bagno chimico vero. Ivan ha approvato la forma della v5 («proviamola così») ma non la scala: «è poco più alto del barbone il bagno chimico, fai che la porta arrivi poco sopra la testa del barbone e poi c'è ancora il tetto». La v5 stava a 84px contro i 32 del barbone, cioè 2,6× — l'altezza di un palazzetto. La v6 sta a 45px (1,41×): la porta finisce a 36px, 4px sopra la testa, e sopra restano 9px di tetto. Non è un ridimensionamento: il downscale a 0,54× sfaldava contorni, maniglia e feritoia (provato e scartato, lo script resta in TMP/ come traccia), quindi a 21×45 la cabina è stata ridisegnata pixel per pixel con PIL conservando forma, impostazione, la falda del tetto a gradini — che era il punto dei due KO precedenti sull'angolo di camera — e gli stessi 4 colori della v5. Non è arte nuova: è la v5 approvata alla scala giusta, coerente con la regola che le correzioni geometriche su arte approvata si fanno con PIL e non rigenerando. 21×45px, alpha binaria. sprites_raw/WORLD/temp/objects/su170_bagno_pubblico_v6_{sud,nord}.png, anteprima TMP/su170_bagno_preview_v6.png. Scelta di metodo dichiarata: SU-181 è partito con questa v6 invece di aspettare un terzo giro di approvazione — se la scala non convince si sostituisce il PNG senza rifare il codice dell'interazione.

/avvisa-tester impara a premere invia: i messaggi partono da WhatsApp Web2026-07-27

  • [feat] La skill /avvisa-tester ora manda i messaggi da sola via WhatsApp Web, pilotando Chrome. Scelta esplicita di Ivan fra le due strade proposte: pilotaggio di WhatsApp Web con Claude-in-Chrome (che verifica l'intestazione della chat prima di ogni invio — con dieci amici veri il rischio che conta è il messaggio nella chat sbagliata) contro l'AppleScript cieco su app nativa (whatsapp:// + Invio simulato: parte anche se la chat non ha caricato — scartato). Non serve nessuna ricerca per nome o numero: il link porta già alla chat giusta, l'automazione aggiunge solo il click su invia. Lo script guadagna --tsv (nome ⇥ link wa.me ⇥ link web.whatsapp.com/send, quest'ultimo salta la pagina intermedia di wa.me; saltati su stderr) e la skill il protocollo d'invio: un tester alla volta, verifica destinatario e testo precompilato, click sulla freccia (mai Invio a tastiera cieco), popup «numero non valido» → salto dichiarato, max 2-3 tentativi poi ci si ferma e si chiede. La colonna Q dell'Excel si compila comunque prima dell'invio: è il piano B a mano se il giro browser si inceppa. Prerequisiti primo giro: permesso estensione su web.whatsapp.com, QR scansionato da Ivan se la sessione non c'è. Dichiarato in chat anche il non detto: l'invio automatizzato è formalmente fuori dai ToS di WhatsApp — per 10 amici a release il rischio pratico è vicino a zero, ma zero no. tools/release/genera_link_whatsapp.py (--tsv), .claude/commands/avvisa-tester.md (riscritta), WORKFLOW/04_RELEASE.md (passo 9). Nel frattempo Ivan ha compilato i numeri veri nell'Excel: al primo giro manca solo quello di Davide Panato.
  • [change] Numero mancante ≠ sempre problema: lo script ora distingue segnaposto e dimenticanze. Regola dettata da Ivan: una riga senza numero né email né Stato è un tester futuro, messo lì in anticipo — va citato e basta; il problema vero è il tester con Stato Invitato/Attivo (l'accesso ce l'ha già) a cui il numero manca per dimenticanza. Lo script classifica dallo Stato (⚠ MANCA IL NUMERO a tester già invitati/attivi contro ℹ Righe future, in --tsv marcatori PROBLEMA/FUTURO su stderr) e la skill riporta i due casi con pesi diversi. Sul foglio vero la regola fotografa la realtà: Marco Bianco è Attivo senza numero (dimenticanza da segnalare), Davide Panato è il segnaposto. Scoperto en passant che Ivan ha tolto una colonna dal foglio: la ricerca delle colonne per intestazione ha assorbito il cambio senza rompersi (i link ora cadono in P). tools/release/genera_link_whatsapp.py, .claude/commands/avvisa-tester.md.
  • [test] Prova d'invio dal vivo: PASS, messaggio recapitato alla chat personale di Ivan. Giro completo su WhatsApp Web pilotando Chrome: apertura chat «Ivan Bianco (tu)» (cercata a mano perché Ivan non è tra i tester dell'Excel — la chat self di WhatsApp non richiede il numero), verifica dell'intestazione, testo digitato, click sulla freccia, doppia spunta a video. Un intoppo istruttivo: WhatsApp Web era aperto in un'altra finestra di Ivan e il primo tentativo è finito sul dialogo «Usa qui» con chiusura della tab — la gestione (clicca «Usa qui», avvisa che l'altra scheda si scollega) è ora scritta nella skill. Non testato con questo giro: l'ingresso in chat via web.whatsapp.com/send?phone=... con testo precompilato, che resta da vedere al primo avviso reale. .claude/commands/avvisa-tester.md.

Un comando e l'Excel dei tester si riempie di link WhatsApp pronti da cliccare2026-07-26

  • [feat] Avviso di nuova versione ai beta tester via WhatsApp — nuovo tools/release/genera_link_whatsapp.py, passo 9 della fase release. Richiesta di Ivan, con tre scelte esplicite sue: niente email (sono amici, WhatsApp è più diretto), messaggio sintetico — una frase da massimo 20 parole, tanto le note complete se le leggono in gioco — e i dati presi dall'Excel già esistente BETATESTING/Beta_Tester_Homeless_City.xlsx, non da un json nuovo. Scartate in discussione sia l'API Business di Meta (burocrazia sproporzionata per 10 amici) sia le librerie non ufficiali tipo whatsapp-web.js (violano i ToS, rischio concreto di ban del numero personale): la via scelta è il semi-automatico coi link wa.me precompilati — lo script fa tutto tranne il tap finale su invia, che resta a Ivan. Come funziona: legge il foglio «Tester» (colonne trovate per intestazione, non per posizione), per ogni tester con un numero in «WhatsApp / Tel.» compone «Ciao <PrimoNome>! <frase>» e scrive il link cliccabile (blu sottolineato, etichetta «WhatsApp → Nome») nella colonna Q «Link WhatsApp», subito dopo «Note»; i link escono anche a terminale. Difese incluse perché il bersaglio è un file che Ivan tiene aperto e a metà compilazione: guardia sul lock ~$ di Excel (salvare mentre è aperto = link persi al prossimo salvataggio di Excel; --forza per scavalcare), normalizzazione dei numeri in tutti i formati plausibili (+39 con spazi, 0039 coi trattini, numero nudo che riceve il 39 da solo, celle numeriche, prefissi esteri conservati), righe senza numero saltate con avviso e non rotte, warning se la frase sfora le 20 parole. Collaudato su una copia con numeri finti nei vari formati: 8 link su 8 corretti, 2 salti dichiarati, colonna e hyperlink verificati a lettura — l'Excel vero non è stato toccato (la colonna numeri è ancora da compilare). tools/release/genera_link_whatsapp.py (nuovo), WORKFLOW/04_RELEASE.md (passo 9, rimando Cowork 6-8→6-9).
  • [feat] Skill /avvisa-tester — la compilazione dell'Excel si lancia con un comando. Richiesta di Ivan subito dopo lo script: il giro a mano (pensare la frase, ricordarsi il percorso, lanciare python) diventa una skill in .claude/commands/. Se la frase non viene passata come argomento, la skill la propone da sola riassumendo in ≤20 parole la sezione più recente di release_notes_it.txt — ma non parte mai senza l'ok di Ivan sul testo, perché il messaggio arriva ai suoi amici; poi lancia lo script, riporta link scritti e tester saltati per numero mancante (offrendo di aggiungere il numero e rilanciare) e ricorda il gesto finale dei click in colonna Q. Vincoli espliciti nella skill: non toccare altre colonne dell'Excel, non committare. .claude/commands/avvisa-tester.md (nuovo), WORKFLOW/04_RELEASE.md (passo 9 rimanda alla skill).

Le Azioni rapide pubblicano su tutti e due i cloud, in cartelle MAC e WIN2026-07-26

  • [change] Azioni rapide DMG/ZIP — ogni pacchetto va su iCloud E Google Drive, nelle sottocartelle MAC/ e WIN/. Richiesta di Ivan: prima il dmg finiva solo su iCloud e lo zip solo su Drive, cioè ogni gruppo di tester dipendeva da un cloud solo. La logica di pubblicazione è stata estratta in un file unico, tools/release/dist_lib.sh (basi: iCloud StreetUniversity, Drive StreetUniversity auto-rilevato in Drive for Desktop; override SU_DIST_ICLOUD/SU_DIST_DRIVE, che sostituiscono le vecchie SU_DIST_MAC/SU_DIST_WIN), così make_dmg.sh e make_winzip.sh non hanno più due copie divergenti del rito .tmp + rename atomico — un cloud spento produce un ⚠ e la build resta valida, mai un errore. Sul Mac la struttura è già pronta e popolata: creati MAC/ e WIN/ su entrambi i cloud, dmg e zip attuali copiati in tutte e quattro le destinazioni, e su Drive la cartella StreetUniversityWIN è stata RINOMINATA in StreetUniversity (rinomina locale → Drive la propaga come rinomina, la condivisione coi tester sopravvive; la cartella MAC/ di Drive e la WIN/ di iCloud sono nuove e vanno condivise la prima volta). Collaudo su basi finte in tmp: doppia pubblicazione OK, cloud mancante → solo avviso; installatore rilanciato per riallineare le copie in ~/Library/Application Support/StreetUniversity/bin, finestre di conferma aggiornate coi nuovi percorsi. tools/release/dist_lib.sh (nuovo), make_dmg.sh, make_winzip.sh, quick_action.sh, install_quick_actions.sh, WORKFLOW/04_RELEASE.md.

Sprint 11: il giullare entra in scena e si porta via la super dal rifugio, le piazze si riempiono di palazzi, PixelLab esce di scena2026-07-26

  • [feat] SU-180 — il GIULLARE, la CARTA DORATA e la super che non si prende più al riparo. Il nemico nuovo non è un sistema nuovo: è l'archetipo arch_jester di ModularNPC, esattamente come la gattara — ed è questa la decisione che regge tutto il ticket, perché spawn, replica multiplayer, abbattimento col gatto e triangolo rosso di minaccia arrivano gratis dal polling già presente in World._net_host_npc_tick, senza inventare un canale di rete per un nemico che ne avrebbe richiesto uno tutto suo. Unica condizione di spawn, come da ticket: una carta arriva al livello 9SuperPower._on_powerup_matured() chiama World.request_jester_for_super(), uno per volta; nel tutorial non parte mai perché run_active è false. Abbattuto lascia a terra la carta dorata (6 frame di luccichio, GoldenCard), e raccoglierla apre la scelta della super via offer_super_choice("carta_dorata") — il punto d'ingresso unico che SU-168 aveva lasciato pronto, quindi «nuova super o tieni l'attuale» funziona anche in questo flusso senza codice duplicato. Se non c'è nulla da battezzare la carta non si consuma. Proiettile nuovo PinProjectile (il birillo da giocoliere) sul pattern di BottleProjectile. Trappola trovata e chiusa prima del collaudo: a fine zuffa la superclasse teletrasporta lontano ogni NPC civilian (SU-34) e il giullare, che sopravvive a più colpi, sarebbe sparito a metà scontro — override _relocate_after_cat_hit() no-op per is_enemy(), col rivale barbone lasciato fuori (SU-89 intatto). Taratura dichiarata: 3 colpi con finestra di 0.6s, birillo ogni 1.2s entro 300px, velocità 0.80×, piroetta di 1.2s come finestra per colpirlo. Sprite approvati in SU-169; il foglio è stato ispezionato con PIL prima di scrivere codice (5×5 con righe S/N/E/SE/NE da rimappare, carta a 6 frame) invece di indovinare il layout. Debug F1, sezione PROGRESSIONE: «Giullare» e «Carta dorata». ModularNPC.gd, World.gd, DistrictSpawner.gd, SuperPower.gd, NPCDatabase.gd, npc_variants_manifest.gd, DebugPanel.gd, nuovi PinProjectile/GoldenCard. Commit 8294386.
  • [change] SU-180 — la super non si attiva più dal rifugio né dormendo. Rimossi il rising-edge di GameState.is_sheltered in SuperPower._process() e la connessione a EventBus.sleep_started (la dormita era il gemello del riparo: lasciarla avrebbe reso la carta dorata opzionale, cioè avrebbe svuotato il ticket). Resta in piedi il paracadute: se mancano meno di 5' alla GRANDE RETATA la carta dorata entra comunque nel ventaglio del level-up, così chi non incontra mai il giullare non finisce la run senza super. Conseguenza dichiarata: il TestBot non sa abbattere il giullare, quindi nei run automatici il super arriverà quasi solo dal paracadute. Commit 8294386.
  • [feat] SU-185 (KO) — una quota delle piazze diventa isolato di palazzi, non di panchine. Il KO di Ivan ha ridefinito il ticket precedente («alcune lasciale come piazze ma altre riempile di palazzi o di case, non di panchine»): arredare tutte le piazze era la lettura sbagliata e produceva una città di panchine. Nuova _converti_piazze_in_isolati() che cambia soltanto il kind del footprint da PLAZA a BUILDING e lascia fare al packing esistente — niente logica di costruzione riscritta, _build_building() e _riempi_intervallo() sono solo chiamate. Il punto tecnico che rende la conversione gratuita: un footprint di piazza 2×2 ha la stessa Rect2 di un super-blocco di palazzi, e strade e marciapiedi sono già disegnati a monte del match sul kind. Il passo gira dopo _promote_special_footprints(), così rifugio, camioncini e pensilina si prendono le piazze per primi e nessun blocco garantito viene sfrattato. prob_building/prob_park non toccate, come chiedeva il ticket: la forma della città non cambia, cambia cosa ci sta sopra. 3 chiavi nuove in Quartieri.LAYOUT_DEFAULTS con override per gli 8 quartieri — plaza_to_building, plaza_super_to_building e plaza_keep_min, che è la garanzia esplicita che alcune piazze vere sopravvivono qualunque cosa dicano i dadi. Misura sul seme 424242: LA COLLINA da 32,4% a 8,8% di piazze (BUILDING 13→21), entrambe le super-piazze 288×288 ora sono isolati di palazzi; fumarola 5→2, brina 7→5, mercato 7→6. Determinismo dal seme verificato (è un vincolo MP): 8/8 firme sha256 identiche su 4 semi, ottenuto consumando un rng.randf() per piazza sempre, anche a quota 0, così la sequenza dei dadi non dipende dai valori del quartiere. Nota onesta: le quote sono probabilistiche su campioni di 5-11 piazze, quindi su semi sfortunati la resa varia (BRINA ha dato 2 conversioni su 7 con quota 0,65); se serve stabilità la strada è una quota fissa round(n*ratio), che però si legge come più artificiale. WorldGenerator.gd, Quartieri.gd. Commit c7dedad.
  • [feat] SU-170 (KO, in attesa di approvazione) — la cabina del bagno guarda dall'alto come gli edifici. Il KO chiedeva la porta a sud «non 45 gradi, ma ruotato in verticale per una vista più dall'alto come gli edifici», più la controparte col retro a nord. Rigenerata con Codex allegando due riferimenti distinti — un edificio del gioco *solo* per l'angolo di camera e la v1 *solo* per la forma, dato che «per il resto andava bene» — con un blocco AVOID che elencava l'errore già bocciato due volte. La prima consegna è stata respinta in casa: era frontale, sì, ma col tetto ridotto a una linea, cioè mancava l'altra metà della richiesta (negli edifici del gioco il piano del tetto è una fascia ben visibile). Un giro in più → v5 col tetto a piano spesso il 20% dell'altezza e il cappuccio di sfiato che vi sporge, frontalità intatta. Sud e nord resi identici nel tetto con PIL, non rigenerando. 40×84px, alpha binaria, nessuna scritta e nessuna croce rossa (quella la aggiungeva PixelLab di sua iniziativa). Consegnate anche le v4 per il confronto: la scelta fra v4 e v5 è di Ivan. sprites_raw/WORLD/temp/objects/, anteprima TMP/su170_bagno_preview_v5.png.
  • [feat] SU-183 (KO, in attesa di approvazione) — le dieci varianti strette rifatte frontali, con Codex. Due difetti nel KO, entrambi chiusi: la prospettiva di tre quarti («soltanto edifici frontali visti dall'alto come gli originali» — una skyline con due angoli di camera stona) e lo strumento. Rifatte da zero con Codex CLI, allegando a ogni generazione il _w48 già approvato dello stesso distretto come legge di camera, palette e outline, con divieto esplicito di 3/4, isometria e 45°: 10 generazioni, 0 retry, 0 correzioni PIL necessarie. 38-41px di larghezza — tutte dentro la fascia 36-43 della famiglia, mentre il giro PixelLab ne aveva tre fuori a 33-35px — 150-155px di altezza, alpha binaria al 100%. Insegne in inglese (CLUB, PUB, BAR). Punto debole dichiarato: la coppia FUMAROLA v1 è la meno differenziata dall'esistente, cambia la facciata ma non il materiale; se cade solo quella si rigenera chiedendo mattone, senza rifare le altre nove. sprites_raw/WORLD/temp/objects/su183_*_codex_v{1,2}.png, anteprima TMP/su183_edifici_preview_codex.png.
  • [fix] SU-180 — l'HUD mandava ancora al riparo. Trovato guardando lo screenshot di collaudo, non dal compile-check e non dai run: lo slot del super continuava a mostrare «✨ MATURO — AL RIPARO!» e, con lo slot pieno, «✨ AL RIPARO: SCEGLI» — cioè le istruzioni della logica che questo stesso sprint ha appena rimosso. Un giocatore le avrebbe seguite alla lettera, sarebbe andato sotto una ShelterZone e avrebbe aspettato una cerimonia che non si apre più. Ora dicono «✨ MATURO — GIULLARE!» e «✨ GIULLARE: SCEGLI». È il tipo di incoerenza che nessun test automatico vede, perché il codice è corretto: è il testo a mentire. SuperPower.gd. Commit cf88ff3.
  • [test] Collaudo di fine sprint: PASS, e la catena del giullare è stata percorsa dal vivo fino alla cerimonia. Compile-check globale 80 script / 31 scene, 0 falliti; 4 run SP col bot su semi e quartieri diversi (compresa LA COLLINA): 0 SCRIPT ERROR, orphans sempre 0, NPC e gatti stabili, e nei log la conversione delle piazze visibile come 2/4 al centro e 5/8 a collina — coerente col commit, nessun overlap né crash in generazione. Le tre verifiche a rischio sono passate: l'override anti-teletrasporto del giullare regge e non tocca il rivale, il paracadute della super è intatto (should_offer_golden/golden_card invariati, e il TestBot dà priorità assoluta alla carta dorata), la città si genera correttamente con le piazze convertite. Il buco che valeva la pena colmare: al primo giro il giullare era verificato solo a lettura di codice — un nemico nuovo che non parte a runtime è un KO che il compile-check non vede. Con un provino dedicato (shot_su180.gd, stesso pattern degli scatti esistenti) la catena è stata percorsa per intero e dal vivo: spawn via World.request_jester_for_super() → giullare presente nel gruppo npcs con texture caricata (non un nodo invisibile) → abbattimento vero a 3 colpi di gatto (_jester_hp 3→2→1→0) → carta dorata a terra animata → raccolta per overlap reale dell'Area2D → offer_super_choice("carta_dorata")cerimonia aperta. Screenshot in TMP/screenshots_claude/su180/. Limiti dichiarati: MP non collaudabile su questo Mac (hang noto di host_game()), tutorial verificato a codice e non eseguito (nessun flag bot per forzarlo in headless), e su questo Mac --headless non renderizza la viewport, quindi gli scatti si fanno in finestrato. Dettagli in TESTLOG.md.
  • [docs] PixelLab esce di scena: le immagini si generano SOLO con Codex. Regola nuova di Ivan, dal KO di SU-183 («NON VANNO MAI GENERATE IMMAGINI CON PIXELLAB MA SOLO CON CODEX, PIXELLAB VA ELIMINATO», con rimozione del server MCP). Recepita dove serve per non ripetere l'errore in un altro giro: convenzione 7 in OPUS_BRIEFS/00_CONTESTO_PROGETTO.md con accanto la ricetta Codex operativa (prompt via stdin col trattino, perché -i è variadico e si mangerebbe il prompt posizionale; chiamate lunghe lanciate in parallelo in background con log su file; vincolo «scrivi solo quel PNG, nessun altro file, nessun comando git» perché codex exec ha accesso in scrittura al repo; PNG verificati col mtime e non con l'exit code, con recupero da ~/.codex/generated_images/ quando il mv finale fallisce). La memoria di progetto che raccomandava PixelLab è stata ritirata. Il divieto valeva dal 2026-07-21 per i soli personaggi: ora vale per tutto, props e tileset compresi.

Sprint 10: nessuna piazza resta nuda, il magenta chiuso è chiuso alla fonte, il giullare guarda dalla parte giusta2026-07-26

  • [feat] SU-185 — le piazze non sono più parcheggi vuoti, e la garanzia è per piazza. Correttivo nato da SU-175: _build_plaza() metteva solo il pavimento, e alle piazze arrivavano soltanto cestini mentre i parchi avevano fontana e panchine — con LA COLLINA a 11 isolati su 34 di piazze, comprese due super-piazze 288×288 completamente deserte. Ora _arreda_piazza() riusa il vocabolario già esistente dei parchi (panchine, fontana, ciuffi d'erba, cestini) più i landmark per le piazze grandi, con densità proporzionale all'area: una super-piazza ha 9× l'area di un blocco 96×96 e non poteva ricevere lo stesso numero di oggetti. Le differenze fra quartieri stanno in 9 chiavi nuove di Quartieri.LAYOUT_DEFAULTS (plaza_landmark_chance, plaza_fountain_chance, plaza_bench_area/max, plaza_grass_area/max, plaza_min_props, plaza_fill, plaza_landmark_veto) — collina la più ricca, brina e fumarola spoglie e sporche — e non in if quartiere == ... sparsi nel generatore. Garanzia anti-vuoto per piazza sotto la soglia plaza_min_props, con ripiego sui 4 angoli quando la ricerca casuale fallisce: è la rete di sicurezza della fontana, ma per piazza invece che per mappa. Risolta di conseguenza anche la fontana solitaria di FUMAROLA notata in ricognizione: quella rete "almeno una per mappa" ora gira prima delle piazze, così la fontana forzata entra in _blocked_spots e l'arredo le si dispone attorno invece di ignorarla. Determinismo dal seme preservato (è un vincolo MP, non un dettaglio: host e client generano la stessa città dallo stesso seed) — solo rng seedato, lista landmark ordinata per path, mai BuildingDatabase.pick_landmark() che tira randi() globale; verificato con un provino dedicato che confronta le firme sha256 di tutti i nodi, 8/8 quartieri identici a doppia generazione. WorldGenerator.gd, Quartieri.gd, nuovo provino scenes/tools/su175c_det.tscn. Commit 97dc5b1.
  • [fix] SU-185 — due super-piazze adiacenti mostravano la stessa identica banca. Intercettato in verifica visiva prima della consegna, non dal collaudo: è la stessa ripetizione che SU-183/SU-184 stanno combattendo sugli edifici, e due landmark gemelli a un isolato di distanza si leggono peggio di una piazza vuota. I landmark disponibili sono solo 3, e sul quartiere rich il veto sulla fabbrica ne lascia 2. Ora _pick_landmark_tex() esclude l'ultimo piazzato — stessa idea del parametro escludi già usato dagli edifici — e quando il paniere non ha alternative mette la fontana invece del doppione. Conseguenza dichiarata: con ≥3 super-piazze nella stessa mappa, dalla terza in poi si vede la fontana, perché un terzo landmark distinto non esiste come asset. WorldGenerator.gd. Commit 97dc5b1.
  • [fix] SU-182 — il magenta chiuso non sopravvive più al flood-fill, e stavolta alla fonte. Il follow-up aperto da SU-177: il flood-fill parte da fuori la sagoma, quindi ogni sacca di magenta circondata dal disegno non veniva mai raggiunta — un manico, un braccio sul fianco, qualunque buco chiuso. Nuova is_magenta_key a soglia stretta (distanza < 40 da #FF00FF, senza la clausola permissiva su r/b) applicata a tutti i pixel della cella dopo il flood-fill; is_magenta_loose resta invariata per il cleanup dei bordi, perché applicarla globalmente cancellerebbe arte legittima (la borsa del fattorino sta a distanza 71 dal magenta puro, il cappotto della gattara a 109). La trappola, trovata misurando prima di consolidare: la passata sulla frangia scura seminata da un pixel trasparente qualsiasi — com'era scritto nel ticket — erode 1000-1800px per sheet, perché il contorno esterno di ogni personaggio confina per definizione con lo sfondo; ora l'espansione parte solo dai buchi aperti da is_magenta_key, mai dal bordo della sagoma. Ripassati i 14 sheet ancora sporchi: 37 → 0 pixel residui. Non-regressione verificata a numeri sui 5 sprite a palette rosa/viola e sui 3 del netturbino. Il censimento resta nel repo come controllo ripetibile, così il difetto non può rientrare in silenzio: tools/check_magenta_residuo.py → 0 su 96 sheet. import_chatgpt_sprites.py, 14 sheet in assets/sprites/npc/bodies/. Commit d821f03.
  • [fix] SU-169 (KO) — il giullare andava a sud-est invece che a sud-ovest. Ivan aveva ragione e la conferma sta nella pipeline: il layout sorgente canonico è S/N/E/SW/NE e import_people_sprites.py fa da sé il mirror SW→SE. Lo sheet era stato generato con la 4ª riga già in SE, quindi in import sarebbe stato specchiato una seconda volta. Mirror orizzontale applicato cella per cella e non alla riga come blocco — altrimenti si invertiva anche l'ordine idle/walk1-4 — con i confini calcolati a round(i*1254/5) perché 1254 non è divisibile per 5. Verifica numerica: pixel non-magenta identici prima e dopo (72936 sulla riga), righe 1-3-5 invariate bit per bit. Correzione con PIL su arte approvata, niente rigenerazione. In attesa di approvazione: sprites_raw/PEOPLE/temp/, anteprima TMP/su169_giullare_preview.png.
  • [feat] SU-170 (KO, in attesa di approvazione) — la cabina del bagno vista come un palazzo. Il KO chiedeva «vista più dall'alto come un palazzo» e una versione orientata a nord. Rigenerata con PixelLab a inquadratura molto più inclinata (view da "low" a "high top-down"): ora si vede il piano del tetto in prospettiva, che è il criterio con cui si giudica, non un accenno di tetto sul bordo. La NORD è un retro vero — parete cieca con griglia di sfiato, niente porta né maniglia. Il primo giro è stato scartato in verifica visiva prima di arrivare al ticket: angolo ancora quasi frontale e "nord" con la porta. Tre scelte lasciate a Ivan invece di deciderle: altezza scesa a 78px per tenere la larghezza nella famiglia degli edifici stretti (l'alternativa è 86px di altezza e ~47px di larghezza, fuori fascia); una croce rossa da pronto soccorso aggiunta da PixelLab e non richiesta; sud e nord sono due facce dello stesso volume, non due rotazioni complete. sprites_raw/WORLD/temp/objects/, anteprima TMP/su170_bagno_preview.png.
  • [feat] SU-183 (nuovo, in attesa di approvazione) — dieci varianti strette per rompere la monotonia dei quartieri. Due nuove varianti per ciascuno dei 5 distretti dove uno sprite solo dominava (commercial, industrial, slums, residential, nightlife); GATTOPOLI e CENTRO non toccati perché già equilibrati. Il punto tecnico che decide il ticket: dominano proprio gli sprite _w48 perché il packing greedy _riempi_intervallo() filtra per larghezza dello slot, e negli slot stretti — la maggioranza — ci entra solo quella taglia; generare edifici larghi non avrebbe spostato di un punto il 56,7% del MERCATO. Silhouette, tetti e finestre cambiati per davvero, insegne in inglese. Segnalate apertamente per il giudizio: prospettiva di tre quarti più marcata delle varianti esistenti, tre file sotto la larghezza minima (33-35px contro 36-43), e le due meno riuscite (nightlife_v2 poco neon, slums_v1 con la palette virata al grigio). sprites_raw/WORLD/temp/objects/, anteprima TMP/su183_edifici_preview.png.
  • [test] Collaudo di fine sprint: 78 script / 29 scene, 0 falliti; nessuna regressione. Il rischio vero di questo sprint era prestazionale — il generatore ora istanzia panchine, cestini, erba e landmark in ogni piazza — quindi il collaudo l'ha misurato invece di darlo per buono: run SP fino alla GRANDE RETATA (1850s di gioco a 20×, ripetuta 2 volte) con 0 SCRIPT ERROR su ~800 dump, orphans=0 sempre, fps stabile 144-146, e nodes 1567-1845 / objects 6316-7657 in range storico, cioè nessuna esplosione del conteggio nodi. Giro quartieri 8/8 PASS. Censimento magenta 0 su 96 sheet. Il tutorial è immune per costruzione: TutorialWorld.gd dichiara di non riusare WorldGenerator. Limite dichiarato: l'attraversabilità delle piazze è verificata a lettura di codice (margini, distanza minima e filtri anti-overlap, con ripiego «meglio fermarsi che impilare») più l'indizio indiretto di un bot che ha inseguito bersagli per 1850s senza incastrarsi — non è un test di collisione tile-per-tile, quindi resta da confermare su device. MP non collaudabile su questo Mac (hang noto di host_game()). Nota operativa emersa: tee negli script di test corrompe il campo world dei dump JSON su questo Mac, si aggira lanciando Godot senza tee. Dettagli in TESTLOG.md.
  • [docs] SU-175 — la ricognizione diventa tre ticket, e si chiude da sola. Su richiesta di Ivan («inserisci te i ticket correttivi ed eseguili subito… chiudi questo task automaticamente senza portarlo in revisione»): la monotonia va in SU-183 (generazione) + SU-184 (montaggio nella rotazione, bloccato dal primo), i buchi in SU-185, lavorabile subito perché usa solo arredo esistente. La separazione non è burocratica: SU-185 non aspetta approvazioni di arte e infatti è già in gioco, mentre le varianti nuove passano dal giudizio di Ivan prima di entrare.

Sprint 9: la reputazione si spegne, i nemici smettono di essere passanti, le scritte non finiscono dietro i palazzi2026-07-26

  • [change] SU-171 — la reputazione si spegne con un interruttore, non si cancella. Requisito di Ivan: «togliere il sistema di reputazione: non serve. Per ora va DISATTIVATO, non cancellato». const REPUTATION_ENABLED := false in GameState.gd rende change_reputation() un no-op — dizionario congelato, reputation_changed mai emesso — così le decine di call site sparsi nel progetto restano dove sono, che è esattamente ciò che il ticket chiede. Scartata l'alternativa di cancellare dizionario e call site: rompeva i file di tre altri lotti in lavorazione parallela e violava il requisito. Nuovo GameState.reputation_effective() → 0 per i punti che *leggevano* la reputazione producendo effetti di gioco, non solo UI. Sganciato: componente rep fuori da ScoreSystem.compute_total() e dal fallback legacy compute_survival_score(); World._ready non connette più _on_reputation_meta (quindi il contatore impresa reputazione_amato non cresce mai, e l'handler resta inerte per la riaccensione); modificatore dell'elemosina neutralizzato in NPC.gd e nell'RPC World.net_request_beg (firma invariata, manda 0). GameState.gd, ScoreSystem.gd, Districts.gd, HUD.gd, NPC.gd, ModularNPC.gd, PoliceOfficer.gd, RaidOfficer.gd, World.gd, CityEvents.gd, TutorialManager.gd, DebugPanel.gd. Commit 0886d3b.
  • [fix] SU-171 — un bug latente scoperto spegnendo la reputazione: la barra wanted restava inchiodata. La base del wanted leggeva la reputazione del distretto, e LA COLLINA parte con reputazione negativa: chi giocava quel quartiere vedeva la barra ferma a un terzo senza alcun modo di farla scendere, perché il reato la alzava e nulla la riportava sotto quella base. Non nasce da questo ticket, ma spegnere la reputazione senza toccarlo avrebbe congelato il difetto per sempre. Ora la base è sempre 0. HUD.gd. Commit 0886d3b.
  • [fix] SU-171 — «LA COLLINA» sarebbe rimasta murata per sempre. La sua regola di sblocco in MetaProgress.UNLOCK_RULES era «reputazione Amato (≥60) in un distretto»: con il sistema spento diventava impossibile, e la settima stazione della metropolitana non si sarebbe più potuta aprire. Riagganciata a un contatore a vita già esistente (niente duplicati, come chiede SU-132): giorno3_raggiunto, soglia 1 — sopravvivere al GIORNO 3 è più difficile dei 60 bidoni di FUMAROLA e più facile della prima GRANDE RETATA di NOTTEFONDA, quindi la difficoltà cresce ancora lungo la linea. Il collaudo ha verificato che nessun'altra regola di sblocco (quartieri o carte) sia rimasta appesa a un contatore che ora non cresce mai: era il modo silenzioso in cui questo ticket poteva rompere la progressione. MetaProgress.gd, Quartieri.gd. Commit 42a5155.
  • [fix] SU-172 — tossico, gattara e spazzino non sono più passanti, e il fix è a monte. Colpirli dava il messaggio «Hai colpito un passante col gatto!» più gli effetti collaterali. La causa era un solo segnale, EventBus.cat_hit_civilian, che si portava dietro tutto: messaggio, +20 wanted (violenza pubblica), −10 reputazione e panico di massa dei civili vicini. Ora non parte più quando il bersaglio è un nemico, né in single player (CatProjectile.gd) né sul ramo host del multiplayer (World._srv_cat_hit_npc) — stessa fonte di verità nei due percorsi. La classificazione è un predicato unico e pubblico, NPC.is_enemy() (false) con override in ModularNPC sulla lista già presente ENEMY_ARCHETYPES: arch_cleaner, arch_catlady, arch_junkie, arch_bully — ricavata dal codice, non inventata; il rivale barbone resta un civile (già escluso da SU-109) e la polizia ha il suo canale stun_by_cat. Il vecchio _is_enemy() privato è stato promosso, con i suoi 3 call site. Conseguenza dichiarata: stendere un nemico non fa più scappare i passanti vicini, perché faceva parte della stessa «cornice da reato». Zuffa comica, abbattimento e punti restano. NPC.gd, ModularNPC.gd, CatProjectile.gd, World.gd. Commit 0886d3b.
  • [fix] SU-179 — i gatti non spawnano più davanti alle porte, e non era sfortuna: era sistematico. Il difetto stava nel pool, non nell'istanziamento. I gatti pescavano da _npc_spawn_points, l'anello di marciapiede a 5px da ogni isolato — e il punto del lato sud cade esattamente sull'ingresso, perché tutti gli entry point stanno a rect.end.y + 4. Ora esiste un pool dedicato _cat_spawn_points, prefiltrato UNA volta a fine generazione da _wall_rects (copia delle footprint già presente in World.gd: nessun accessor nuovo in WorldGenerator, che era in mano a un altro lotto), con due divieti: fascia porta (lato sud, −12/+44px, larga quanto l'edificio) e «a ridosso del muro» (footprint +12px). NPC, polizia e retata continuano a usare il pool completo. Ripiego deterministico a due livelli con push_warning sotto CAT_POOL_MIN = 16: mai loop, mai gatti non spawnati. Determinismo dal seme preservato — il filtro è geometria pura senza randf(), gira su ogni peer dopo la stessa generazione, quindi lo shuffle seminato produce le stesse posizioni su host e client; il ramo client _cl_cat_spawned è invariato. Vale per spawn iniziale e refill: entrambi passano ora dal punto d'ingresso unico _spawn_cat_at. Nuovo bottone F1 «Rispawna gatti», solo single player (in rete una rimozione muta desincronizzerebbe i client). World.gd, DebugPanel.gd. Commit 0886d3b.
  • [fix] SU-132 (KO, terzo giro) — la metropolitana compare sempre, e il quartiere fantasma dopo il reset è morto. Due difetti dietro un KO. Il primo: _goto_metro_or_start() aveva un gate che saltava la schermata quando c'era una sola stazione sbloccata, partendo diretto; Ivan la vuole sempre visibile, con tutte e 8 le stazioni e ??? sulle chiuse (la lista completa e i ? lungo la linea c'erano già dal giro precedente, mancava l'accesso). Il secondo, il bug che aveva segnalato: nessuna azione di reset toccava Quartieri.current_id/selected_id, quindi dopo un azzeramento restava impostato l'ultimo quartiere giocato e si ripartiva da uno non più sbloccato, senza nemmeno vedere le stazioni. Nuovo Quartieri.reset_to_centro(), agganciato al reset progressi. Il collaudo l'ha verificato a runtime e non per lettura: stato sporcato a mano, poi la stessa sequenza chiamata dal menu, esito current_id=centro con tutte le stazioni richiuse. MainMenu.gd, MetaProgress.gd, Quartieri.gd. Commit 42a5155.
  • [feat] SU-173 — il reset si sceglie: classifica o progressi. La voce unica «RESETTA STATISTICHE» delle Opzioni è sostituita dal sotto-menu RESET DATI con due azioni indipendenti e la propria doppia conferma a 3s ciascuna, tappabili e navigabili: RESET CLASSIFICA (HighScore.reset_all() + record per quartiere + nuovo Settings.reset_online_best()) e RESET PROGRESSI (MetaProgress.reset_progress() + quartiere al Centro + tutorial riattivato). MetaProgress.reset_all() ora compone i due nuovi reset_records()/reset_progress() invece di essere un blocco unico. Le impostazioni (audio, controlli) non le tocca nessuna delle due. Scelta dichiarata, da tarare: record-per-quartiere e record online stanno in «classifica», non in «progressi». MainMenu.gd, MetaProgress.gd, Settings.gd, Quartieri.gd. Commit 42a5155.
  • [fix] SU-167 — un tocco non sceglie più la carta per sbaglio, e la logica a due stadi non era il problema. C'era già, ed era corretta. Il difetto è che un tocco fisico arriva due volte nello stesso frame: con emulate_mouse_from_touch (default) Godot consegna prima un InputEventMouseButton emulato e poi l'InputEventScreenTouch, sullo stesso gui_input — il primo evento selezionava, il secondo confermava di fila. Verificato con un probe eseguito, non dedotto. Fix in due pezzi, identico in LevelUpScreen e SuperCeremony: de-duplica per frame (_press_frame) e «armamento» (_pointer_armed), cioè la carta evidenziata conferma solo se già puntata in questa apertura — così anche il primo click di mouse/trackpad proprio sulla carta 0, che è evidenziata all'apertura ma non armata, la evidenzia soltanto. L'evidenziazione passa sempre da _select()_layout_fan(true): è quella delle frecce, riusata e non ricreata, che era il criterio di accettazione. Frecce, tastiera e gamepad invariati. LevelUpScreen.gd, SuperCeremony.gd. Commit 0b1a946.
  • [fix] SU-168 — rimaturando una carta la nuova super si può rifiutare. _try_open_ceremony() usciva se lo slot era pieno, quindi la seconda maturazione restava in _pending per sempre. Ora si riapre la stessa SuperCeremony, stesso ventaglio dorato, con la carta del super attuale in coda (_keep_index, oro consumato, titolo TIENI · <NOME>): la scelta ha lo stesso linguaggio visivo delle carte e funziona col doppio tocco di SU-167. Scartate una schermata sì/no dedicata (linguaggio visivo diverso e secondo percorso d'input da mantenere) e il ri-tiro di 3 super casuali (il ticket chiede «nuova o attuale», non un reroll). Comportamento dichiarato: «tieni l'attuale» scarta le super offerte in quel giro con un messaggio comico — il power-up resta a livello 9 coi bonus passivi, si butta solo la promozione. Nessuna pausa globale in MP (riuso di SuperCeremony.open, che già distingue SP/MP/bot). Predisposto il punto d'ingresso unico che servirà al GIULLARE di SU-180: SuperPower.offer_super_choice(reason). SuperCeremony.gd, SuperPower.gd, LevelUpScreen.gd. Commit 0b1a946.
  • [feat] SU-119 (KO) — la texture del parco approvata entra in gioco, con l'angolo morbido. Il KO di Ivan sul TileMap Wang aveva ridefinito il ticket: «va bene come era prima ma ricostruirei con codex il tile del parco… poi farei anche lì l'angolo morbido come per il marciapiede». Quindi l'ibrido resta il default e il TileMap resta spento, e questo giro monta soltanto l'asset approvato in SU-166: park.png sostituita con la texture seamless e nuova WorldGenerator._build_park_border() che compone la cornice del parco dal foglio 4×4 (PARK_BORDER_CELL, TEX_PARK_BORDER) con angolo arrotondato e fascia dithered verso il marciapiede — z_index -9, zero collisioni, deterministica dal seme, footprint e spawn invariati. Il layout del foglio è stato dedotto ispezionando il PNG con PIL (frazione di erba per quadrante di ogni cella) prima di scrivere codice, perché non coincide con l'atlas WANG_CELL generato a runtime. Conflitto nel ticket, risolto e da confermare: chiedeva la cornice «sopra la piastra del parco» (-10) e insieme «sotto i ciuffi d'erba» (-14), che sono numericamente incompatibili — tenuta sopra entrambi, altrimenti l'angolo tagliato non si vedeva. Il tutorial prende la texture nuova ma non la cornice. WorldGenerator.gd, assets/sprites/world/park.png, park_border.png. Commit b7749ef.
  • [fix] SU-156 (KO) — i ciuffi d'erba a metà. «Troppo grossi, vanno rimpiccioliti almeno a metà»: GRASS_TUFT_SCALE = 0.5 in _place_grass_tuft. Scelta la scala nel codice invece di rigenerare i PNG a metà risoluzione, perché così resta un numero solo da ritarare al prossimo giro — con la verifica a schermo che a 0.5 restino leggibili come ciuffi e non macchie. Densità non toccata: il KO riguardava solo la dimensione. WorldGenerator.gd. Commit b7749ef.
  • [feat] SU-178 — il gattile ha la sua icona sulla mappa. Faccia di gatto ricavata con PIL da caticon.png già nel progetto (niente generazioni nuove, come da ticket) e registrata in MapOverlay.POI_ICONS più la voce di legenda. Aggiunta anche a ENTRANCE_TYPESnon era richiesto, ma senza quello l'icona flottava sul marciapiede invece di centrarsi sull'edificio, in contrasto con rifugio, liquor e negozio di vestiti. MapOverlay.gd, assets/sprites/world/map_gattile.png. Commit b7749ef.
  • [change] SU-150 (OK-ma) — la marmotta timbra le mutande. Ivan: «cambiare testo… OOOPPSS!!!! LA MARMOTTA TI HA TIMBRATO LE MUTANDE!!! o simile (rivedi te bene)». Tenuta la sua battuta, normalizzata la punteggiatura sullo stile delle altre notifiche: OOOPS!!! LA MARMOTTA TI HA TIMBRATO LE MUTANDE!!!. Icona, colore, durata e logica del malus invariati. Il testo passa da 40 a 49 caratteri, quindi verificato a schermo che il rimpicciolimento font e l'a-capo di SU-161 lo reggano: sta su una riga e non invade il pannello stat. Player.gd. Commit 170193e.
  • [fix] SU-174 — le scritte d'azione non finiscono più dietro i palazzi alti. Il mondo non usa y_sort: ordina a mano con z_index = int(pos.y). Le scritte avevano uno z relativo al proprio nodo (prompt 6, scritta d'elemosina 0), quindi finivano a pos.y + n e qualunque edificio più a sud le copriva — i tetti delle pensiline arrivano a pos.y + 47. Ora z_as_relative = false e quota assoluta Interactable.PROMPT_Z_INDEX = 4000: sopra il massimo raggiungibile dal mondo (mappa 8×8 celle da 192px → z max ~1700) e sotto il tetto di Godot (4096). Messo negli script e non nei punti di creazione: Interactable._ready() copre in un colpo tutti e tre i posti che creano un PromptLabelWorldGenerator, TutorialWorld e CatPickup.tscn — e Player._ready() copre interact_label. Non toccata la freccia verde sopra gli edifici, che ha lo stesso difetto: una freccia sempre in primo piano rischia di sembrare puntata sull'edificio più vicino, quindi la decisione è di Ivan. Interactable.gd, Player.gd. Commit 170193e.
  • [fix] SU-177 — il magenta del netturbino, e perché toccava proprio a lui. L'ipotesi di Ivan («un'area chiusa circondata da pezzi del personaggio») è confermata, ma la causa è strutturale: import_chatgpt_sprites.py toglie il fondo con un flood-fill che parte da fuori, quindi ogni sacca di magenta circondata dal disegno non viene mai raggiunta — e il netturbino è l'unico NPC che tiene una scopa, cioè l'unico col triangolo chiuso fra manico, braccio e corpo. Misurati tutti i 96 sheet del progetto: i suoi tre erano di gran lunga i peggiori (246/240/257 pixel viola contro 0-4 di uno sheet pulito). Ripulito tutto lo sprite come chiedeva il ticket, in due passate: la prima col criterio is_magenta_loose già nel repo (riusato invece di inventarne uno: su uno sheet pulito dà 0 falsi positivi), la seconda sulla frangia scura — i blend di magenta scuriti dal downscale, che sfuggono a quel criterio perché sotto 130 di luminanza — rimuovendo solo i pixel che confinano con un trasparente, in modo iterativo, così non si erode nulla all'interno. Esito 0 pixel residui su tutte le 25 pose dei 3 sheet, silhouette intatta. Correzione con PIL, arte non rigenerata. arch_cleaner_{male_v01,male_v02,drunk}_sheet.png. Commit 170193e.

    Follow-up da aprire: altri 11 sheet hanno 1-6 pixel magenta sparsi e la pipeline continuerà a produrli; il fix durevole è una passata sull'intera cella dopo il flood-fill, non solo sui bordi.

  • [feat] SU-170 (nuovo, in attesa di approvazione) — la cabina del bagno pubblico. Generata con PixelLab: torretta verticale singola 41×86px con la porta in evidenza, alpha binaria senza aloni, ritagliata al bounding box con PIL. Larghezza calibrata sulla taglia più stretta della famiglia SU-118 (w48, edifici reali 36-43px); altezza volutamente non allineata ai 160px di quei palazzi, che sono multipiano — la cabina sta a ~2,7× il barbone. Nessuna scritta, scelta deliberata per non rischiare testo sbavato: se serve un «TOILET» conviene disegnarlo a runtime in SU-181, così resta localizzabile invece di essere cotto nei pixel. sprites_raw/WORLD/temp/objects/, anteprima TMP/su170_bagno_preview.png con cabina, edificio e barbone alla stessa scala.
  • [docs] SU-175 / SU-176 — ricognizione della città, con i numeri invece delle impressioni. Due ticket di sola ricognizione (nessuna modifica al gioco: i correttivi li apre Ivan guardando il materiale), girati sullo stesso seme 424242 degli altri provini così il confronto è diretto. Varietà e vuoti: la monotonia peggiore è al MERCATO, dove building_commercial_w48.png copre da solo 38 edifici su 67 (56,7%) — ironico per il quartiere che dovrebbe sembrare il più vivace; i vuoti peggiori sono a LA COLLINA, 11 isolati su 34 (32%) sono piazze, comprese due super-piazze 288×288 completamente nude. Seguono FUMAROLA (44,7% di uno sprite solo; e con 0 parchi scatta la rete di sicurezza del generatore, che piazza una fontana in una piazza spoglia), BRINA (44,3%) e SOLLEONE (43,6%); GATTOPOLI e CENTRO sono i più equilibrati e non richiedono azione. Proporzioni: il metro è il barbone (32×32px) e un piano reale ≈ 54,9px. Le 6 varianti _w48 (una per distretto) sono da rifare: 36-43px di larghezza, cioè 1,1-1,3 barboni, per 160px di altezza con ~9 file di finestre — una facciata larga quanto una persona. Ed è la convergenza che conta: sono anche gli sprite più ripetuti in quasi ogni quartiere, quindi rifarle risolve insieme la monotonia di SU-175 e la sproporzione di SU-176. Le 7 torri _tall (211-252px) sono da valutare: hanno 10-11 file di finestre, cioè un piano disegnato (~23-25px) più basso del barbone stesso. Tavola delle altezze e 28 screenshot in TMP/screenshots_claude/su175_* e su176_*. Nota operativa per i prossimi provini: --headless su questo Mac non renderizza la viewport texture, gli screenshot vanno scattati in modalità finestrata.
  • [test] Collaudo di fine sprint: 74 script / 26 scene, 0 falliti; nessuna regressione. Run SP fino alla GRANDE RETATA (2200s di gioco a 10×): 0 SCRIPT ERROR, orfani 0 su tutti i 147 campionamenti, gatti fermi a 8/8, fps 141-145, pool gatti 80 liberi/215 fuori-porta/296 totali senza mai ricorrere al ripiego. Giro quartieri 8/8 PASS con pool sano ovunque (48-152 punti liberi). Verificate una per una le sei interazioni fra lotti che nessun builder poteva vedere, due a runtime con provini dedicati: reset progressi × quartiere selezionato (su_reset_diag.gd, nuovo strumento che resta nel repo) e nessuno sblocco appeso a un contatore morto. MP non collaudabile su questo Mac (host_game() si blocca): resta da provare su device. Dettagli in TESTLOG.md.
  • [docs] Sprint orchestrato in una sola onda di builder. Quattro lotti a file disgiunti in parallelo (suolo/mappa, reputazione-nemici-gatti, menu/meta, carte/super) più due generazioni asset e tre micro-task inline, poi collaudo unico. La mappa di proprietà dei file ha retto: zero conflitti di merge, e i due punti dove due ticket si toccavano (la regola di sblocco LA COLLINA fra SU-171 e SU-132, il pool gatti fra SU-179 e il bordo parco di SU-119) sono stati assegnati in anticipo a un solo proprietario invece di essere scoperti dopo.