← Novità

v0.52

2026-09-19 → 2026-09-21 · 21 voce/i di changelog

Release 0.52: note in-game in otto lingue, versione a 0.52 (`/rilascio-beta`)2026-09-21

Giro di rilascio beta lanciato da Ivan con /rilascio-beta. Delta dall'ultimo tag di release v0.51-cambio-host-caffe-cancella-account: voci di CHANGELOG 295-314 (la 294 chiude il giro della 0.51), gli sprint del 19, 20 e 21/09.

  • ✅ Sezione v0.52 in cima a files/homeless_city/assets/release_notes_it.txt più le 7 gemelle (_en, _fr, _es, _de, _ru, _zh, _pt): 13 voci nel formato SU-721 - TITOLO: testo, sezioni vecchie intatte. Nomi di super, linguette, quartieri e voci di menu presi dalle chiavi di files/homeless_city/assets/translations/ (SUPER_RUTTO_SISMICO_NOME = SUPER SCOREGGIA, QUAD_TAB_SUPERPOTERI, QUARTIERE_NOME_COLLINA, TUTORIAL_TITOLO_RIFUGIO, ACCOUNT_VOCE_NOME, ACCOUNT_DELETE_VOCE): il «dormitorio» del CHANGELOG in gioco si chiama RIFUGIO.
  • 📌 Voce SOLO CON LA 0.52: la guardia RPC di prepare_release.sh segnala una RPC nuova dalla 0.51, _srv_shelter_slept in World.gd (SU-1045), quindi la versione minima multiplayer del manifest è 0.52.
  • 📌 Voce CANCELLA ACCOUNT scritta per quello che il giocatore vedrà: eos_credentials.cfg non ha la sezione [api], quindi con SU-1039 (Settings.account_deletion_available() = Env.api_url() non vuoto) nella 0.52 la voce non compare; chi l'aveva premuta sulla 0.51 si ritrova l'account sbloccato all'avvio. Da ricordare al Tempo 1: nelle note della Beta App Review di TestFlight la riga sul percorso ACCOUNT > DELETE ACCOUNT messa per la 0.51 non vale più.
  • 📌 Fuori dalle note le cose che il giocatore non vede: telemetria mp_connection_lost (SU-1023), Xcode 27 (SU-1040), le sonde di SU-1050, le due (DESIGN) SU-771 e SU-1047, i permessi del giro di release.
  • config/version di files/homeless_city/project.godot a 0.52, e version/code 52 nei due preset Android di files/homeless_city/export_presets.cfg (è l'allineamento che export_all.sh farebbe da sé: fatto ora entra nel commit del tag, invece di sporcare la dev dopo come per la 0.51). Pre-volo export_all.sh --env stage --controlla: include_filter di macOS e Windows a posto.
  • [NOVITA'] La Collina seconda fermata — subito dopo il Centro, si apre insieme al Mercato comprando in tre negozi diversi: gente più generosa, prezzi più alti, cinque poliziotti.
  • [NOVITA'] Le carte dei poteri — la carta del superpotere in primo piano dice cosa fa, e il Quaderno ha la pagina POTERI con tutti e dodici.
  • [NOVITA'] SUPER SCOREGGIA e ASPIRATUTTO — la prima non costa più fame, il secondo attira tutti i soldi della mappa, anche in multiplayer.
  • [NOVITA'] Il rifugio fa perdere le tracce — dormendo al rifugio la polizia che ti inseguiva lascia perdere, anche in multiplayer.
  • [NOVITA'] La chiesa come la banca — tenendo premuto le offerte partono una dopo l'altra.

/rilascio-beta seguibile dal telefono: permessi del giro di release e cancelli Play corretti2026-09-21

Chiesto da Ivan: rispondere a tutta la release da mobile — nome del tag, messaggio ai tester, gli ok — senza mai lanciare un comando di shell. Inventario dei comandi di /rilascio-beta, /release, /esporta, /testflight, /avvisa-tester, /lista-beta e pubblica-store contro le regole di permesso.

  • 20 regole in .claude/settings.local.json (fuori da git, vale per questo Mac): il giro git della release (git commit, git push origin dev|main|v0.* senza :*, così una variante con --force non passa, git checkout main|dev, git merge --no-ff dev, git tag -a), la pagina Changelog sul sito (5b), la marca temporale Aruba (5c) e gli strumenti del browser per Telegram in /avvisa-tester. 📌 Il perché: l'auto mode globale dice al classificatore di trattare come sensibili push, merge e tag su main, quindi senza regola esplicita il giro poteva fermarsi proprio mentre Ivan risponde dal telefono. La conferma in chat prima di ogni comando del giro git resta la regola di progetto.
  • .claude/commands/rilascio-beta.md: fra i quattro cancelli c'era ancora play_upload.sh --invia, ma il giro vero è --bozza al Tempo 1 e --pubblica al Tempo 2, e --invia è bloccato nelle impostazioni globali. Corretto, insieme alla nota sulla propagazione del canale.
  • 📌 Da sistemare una volta sul Mac, non a ogni release: il portachiavi «login» non deve chiudersi da solo (se si chiude, la firma iOS muore con errSecInternalComponent e dal telefono non si riapre); Telegram Web va riloggato col QR solo se la sessione scade.
  • Resta a mano a ogni release: i simboli nativi Android (~700 MB, troppi per lo strumento del browser di Play Console). Aperto SU-1055 in backlog: caricarli da play_upload.sh con l'API, dopo aver verificato che la dimensione passi.

Le due modifiche ai super di oggi provate a motore acceso: in single player e con due giocatori (SU-1050)2026-09-21

Sonde chieste da Ivan prima della release, per le modifiche 310 e 311 che avevano solo il compile-check. Nessun file di gioco toccato, nessun difetto trovato.

  • probe_su1050_super_numeri.gd + tools/autotest/probe_su1050_super.sh (headless, costruita da Codex sol, rilanciata in locale: 13 su 13). SUPER SCOREGGIA: fame 60,00 subito dopo l'attivazione e calo in 1 s uguale a quello di fondo a super spento (0,73 contro 0,73); un NPC a 100 px viene ancora stordito. ASPIRATUTTO: super_magnet_radius() = INF; una moneta a terra da ~2.000 px avanza di 1.020 px in 3 s (340 px/s), una CoinDrop vera di 646 px (parte dopo MAGNET_ARM_DELAY), quella a 300 px viene raccolta (+$7). A super spento e dopo i 20,01 s di durata: 0,00 px.
  • ✅ Caso P7 in probe_su956_monete_mp.gd (due processi, costruito da dev-sonnet, 2 giri puliti su 2): il guest chiede una moneta dell'host da 1.107-1.130 px — senza super l'host rifiuta (portata 246 px, rifiuti +1, $0); il guest accende ASPIRATUTTO col percorso vero (baptize + attivazione → pacchetto «magnete») e la stessa richiesta passa (portata inf, +$10/11, moneta sparita per tutti). P6 invariato. Il confronto sui soldi è «almeno il valore della moneta»: col raggio illimitato il guest si prende anche le monete sorelle della stessa elemosina, che è l'effetto voluto.
  • 📌 Due trappole di sonda, non del gioco: il guest accendeva il super prima che l'host avesse misurato la portata senza super (ora aspetta un marcatore scritto dall'host), e un punto a -1.200 px finiva fuori mappa e veniva riportato a ~800 px dal bordo (ora si va all'angolo della mappa più lontano dalla moneta).
  • ⚠️ In un giro scartato P4 — caso vecchio, non toccato — ha dato un KO isolato («host +12 guest -10») che non si è ripresentato: possibile instabilità della sonda di base, da guardare se si torna su P4.

CAMBIA NOME con Epic: toccando il campo bloccato il gioco dice perché, invece di sembrare rotto (SU-1054)2026-09-21

SU-1054 — nato dalla prova di Ivan sull'iPad con la build Xcode 27: «su iPad non compare la tastiera!». Il log scaricato dall'iPad ([SU-932] account_nome tocco sul campo … editable=false … provider=epic) diceva che la tastiera non c'entrava: con Epic il campo del nome è bloccato di proposito (SU-506), ed era la seconda volta che sembrava un guasto (la prima il 19/09, SU-1040). Deciso da Ivan in chat: «mostra quel messaggio». Lotto dev-sonnet, gate istruito da Codex luna (K1054, 12 richieste su 12), due review sol (R1054, R1054b).

  • ✅ Al rilascio del dito sul campo bloccato (MainMenu.gd:9944), con provider Epic, la riga dei messaggi della schermata mostra «Il nome te lo dà Epic. Qui lo vedi, e ti diciamo dove si cambia.» — la chiave esistente ACCOUNT_INFO_NOME_EPIC, già in 8 lingue, dentro la macchina degli avvisi che c'era (_nome_notice_code = "epic_tocco", _nome_notice_text()), col suo colore. Campo sempre in sola lettura, nessuna tastiera.
  • ✅ Con il claim in volo (_nome_busy) il campo è bloccato anche per Apple/Google, ma lì il silenzio è giusto: il messaggio Epic non compare.
  • ⚠️ Preso dalla review: il primo giro sostituiva qualunque avviso già a schermo — nome preso, bocciato, limite del giorno, il rifiuto «epic» alla CONFERMA — con questo, che è generico. Ora compare solo a riga vuota; da vuota il testo del bottone non cambia (dipende dall'avviso solo per muto e daily_limit).
  • ✅ Provino tools/autotest/shot_su1054.sh, a 844×390 e 1024×768, prima e dopo il tocco, col testo letto negli scatti; controprova con provider Apple: niente messaggio, la tastiera si apre come oggi.
  • ⚠️ NON PROVATO sul device: il tocco vero sull'iPad con account Epic.

ASPIRATUTTO attira davvero TUTTI i soldi della mappa, non solo quelli entro 400 px (decisione di Ivan su SU-1050)2026-09-21

Deciso da Ivan in chat il 21/09/2026, rispondendo al secondo rilievo della review di SU-1050 («la frase dice tutti i soldi, il super li attira entro 400 px»): si cambia il gioco, la frase «Attira tutti i soldi per 20s» resta. Registrato su SU-1050. Review Codex R1050c.

  • SUPERS["calamita"].radius da 400 a -1 (SuperPower.gd:186): raggio negativo = tutta la mappa, la stessa convenzione che _area_confuse_police usa già per AMNESIA. super_magnet_radius() lo traduce in INF a super acceso (0 da spento, come prima). Lo leggono le monete a terra (_suck_loose_money) e i Pickup, il cui unico tipo è CoinDrop: si muovono solo i soldi, niente cibo né altro. Durata 20 s e ricarica 90 s invariate; a 340 px/s una moneta fa ~6.800 px in un'attivazione, e quelle più lontane si fermano dove sono arrivate quando il super finisce.
  • ⚠️ La trappola che avrebbe rotto il multiplayer: l'host misura da dove un client chiede una moneta, e con ASPIRATUTTO acceso gli concedeva il raggio del super più i margini (World._net_coin_reach, World.gd:4905). Togliendo il limite solo dal lato di chi aspira, l'host avrebbe rifiutato tutte le monete risucchiate da oltre ~550 px. Ora con il super acceso la portata è INF. La sonda di SU-956 (P6) prova la portata senza super e non è toccata.
  • 📌 Rilievo della review, non chiuso qui: l'host concede quella portata fidandosi del solo pacchetto "magnete" del client, senza sapere se il client ha davvero ASPIRATUTTO. Non è un buco nuovo né solo del magnete — in MP l'host si fida del client per tutti i super (_srv_super_area accetta anche un'AMNESIA su tutta la mappa da chiunque) — ma col raggio illimitato un client truccato potrebbe prendersi tutte le monete della mappa. Chiuderlo vuol dire far sapere all'host quale super ha ogni giocatore e quando può usarlo: domanda aperta a Ivan.
  • ⚠️ NON PROVATO a runtime: compile-check di SuperPower.gd, World.gd, Pickup.gd, CoinDrop.gd; nessuna sonda accende ASPIRATUTTO lontano dalle monete.

SUPER SCOREGGIA non costa più 30 di fame (decisione di Ivan su SU-1050)2026-09-21

Deciso da Ivan in chat il 21/09/2026, rispondendo al rilievo della review di SU-1050 («la frase non dice che costa 30 di fame»): il costo si toglie, non si scrive sulla carta. Registrato su SU-1050.

  • _fire() di stomaco_ferro (SuperPower.gd:532) non toglie più 30 di fame e non emette più stats_changed, che serviva solo a ridisegnare la barra dopo il calo. Resta l'onda d'urto (_area_stun), col terremoto della camera: raggio 220, ricarica 75 s, invariati.
  • 📌 Nessun testo del gioco prometteva il costo (cercato nei csv e negli script), quindi la frase d'effetto «Onda d'urto: stende tutti intorno» ora dice tutto quello che il super fa.
  • ⚠️ NON PROVATO a runtime: modifica di una riga, verificata solo col compile-check.

Il Quaderno ha una pagina POTERI: le 12 frasi d'effetto da rileggere a mente fredda (SU-1051)2026-09-21

SU-1051 — lotto costruito da dev-sonnet in parallelo a SU-1050, sul contratto messo prima dell'onda; gate istruito da Codex luna (K1051), review sol (R1051).

  • Linguetta propria in TABS (Quaderno.gd:37), prima di «← INDIETRO», invece di una sottosezione di CARTE: è l'unico posto dove rileggere le frasi molto dopo averle scelte. Navigazione, larghezze e cartello INDIETRO erano già calcolati su TABS.size(). Etichetta corta, POTERI (QUAD_TAB_SUPERPOTERI), perché con quattro linguette la fascia di ciascuna si restringe di un quarto; titolo e sottotitolo della pagina in ui_hud.csv, 8 colonne.
  • Nessuna seconda copia di niente. _content_superpowers() prende nome e frase da SuperPower.nome_super() e super_effect_text(), e — preso dalla review — anche l'elenco dei 12 id da SUPER_CHIAVI.keys(): il builder aveva scritto una seconda lista che poteva divergere.
  • Si mostrano tutti e dodici: oggi non esiste una regola di sblocco dei super (c'è solo il battesimo, e «mai battezzato» non vuol dire «mai sbloccato»). Scelta dichiarata, come chiedeva il ticket.
  • Provato che la chiave è una sola: la sonda sostituisce a runtime la traduzione di una chiave SUPER_*_EFFETTO e il Quaderno e super_effect_text() leggono lo stesso valore nuovo. 📌 Trappola: Translation.add_message() sulla risorsa già caricata non fa niente, perché all'import il csv diventa una OptimizedTranslation compilata; bisogna togliere la risorsa dal TranslationServer, aggiungerne una sostitutiva e poi rimettere l'originale.
  • Provini a 844×390 e 1024×768 «huge», in cima e in fondo dopo lo scorrimento: prima voce SUPER SCOREGGIA, ultima SCOREGGIA ATOMICA, niente testo tagliato. 19 controlli, 0 KO.
  • ⚠️ Presi dalla review, tutti nella sonda: cancellava ogni file della cartella d'uscita (ora solo i suoi quaderno_super_*.png), la prova del cambio chiave dava KO falsi con il gioco in un'altra lingua (ora fissa l'italiano e rimette quella di partenza), e la cartella predefinita era /tmp invece della TMP/ di radice. Identificatori del codice nuovo in inglese.
  • ✅ La sonda di SU-722 (probe_lottob_quaderno.gd) contava tre linguette scritte a mano e sarebbe diventata rossa: ora le ricava da TABS. Rilanciata: 8 su 8.
  • ⚠️ NON PROVATO: tocco e pad veri sulla linguetta nuova (provata via _selected/_activate); le 7 traduzioni delle tre etichette nuove non le ha riviste un madrelingua.

La carta del superpotere dice cosa fa il potere, in 8 lingue (SU-1050, SU-1052)2026-09-21

SU-1050 + SU-1052 — un lotto solo perché scrivono lo stesso csv; costruito da dev-sonnet, gate istruito da Codex luna (K1050), due review sol (R1050, R1050b). Il contratto comune con il Quaderno (SU-1051) l'ha messo l'orchestratore prima dell'onda, così i due lotti sono partiti in parallelo.

  • Una chiave sola per frase, letta da due posti. SUPER_CHIAVI (SuperPower.gd:209) ha il campo effect accanto a name e notify, e super_effect_text(id) (:233) è l'unica porta: la usano la cerimonia e il Quaderno. Le 12 righe SUPER_*_EFFETTO stanno in ui_gioco.csv:36-47, come le altre chiavi dei super, non in ui_hud.csv dove le metteva la proposta di SU-1047.
  • Sulla carta in primo piano la frase prende il posto di «10s · ricarica 1:30»; le altre carte del ventaglio tengono i tempi. _refresh_effect_labels() (SuperCeremony.gd) riscrive le righe senza ricostruire le carte, da open(), da _select() e — preso dalla review — da _ricostruisci_testi(), altrimenti cambiando lingua a cerimonia aperta (in MP resta aperta sotto il menu) la frase restava nella lingua vecchia. Lo speciale «JOLLY MP» di SCOREGGIA ATOMICA non è più un ramo a parte: passa dalla sua chiave come gli altri 11.
  • Traduzioni nelle 7 lingue, partendo dalle bozze di SU-1047 ma non tenendole: «ricercato» in francese e spagnolo ora è la parola che il gioco usa già per l'indicatore (Recherche effacée, Búsqueda borrada: «Se busca borrado» era il cartello del far west). Polizia ed energia già coerenti coi termini in uso.
  • Misura sulle frasi vere del csv, non su una copia nella sonda: probe_su1050_effetti.gd le legge con super_effect_text() dopo I18n.imposta_lingua() e il reimport. 96 combinazioni, massimo 2 righe a font 11 su 156 px; la frase francese più lunga è kazoo, 39 caratteri; la più lunga in assoluto kazoo in russo, 43.
  • Provini a 844×390 «huge»: le 12 carte in italiano una per una, kazoo in francese (letto sullo scatto: «Un concert démarre, tu gagnes en jouant»), e il ventaglio vero da tre carte — la frase sta sulla carta selezionata e passa alla carta 1 dopo _select(1), 6 assert sul testo delle label.
  • ⚠️ Preso al gate: la carta «tieni quella che hai» sbordava. Selezionata porta «scarti la nuova» più fino a due righe di frase, e in francese (keep = kazoo, titolo «GARDE · CONCERT DE FOLIE» su tre righe) il testo usciva di 7,5 px dal cartone. Stretta la spaziatura del blocco testi (separazione 4 → 1, margine dall'icona 10 → 4) sulla sola carta di scambio: il builder l'aveva messa su tutte, e sulle carte normali avrebbe cambiato un layout già approvato dopo il KO di SU-158. Margini misurati sul rettangolo: italiano +30,4 px, francese +6,3 px, russo +30,4 px.
  • 📌 Due osservazioni della review sulle frasi italiane, lasciate a Ivan perché le ha decise lui su SU-1047: SUPER SCOREGGIA non dice che costa 30 di fame, e ASPIRATUTTO attira i soldi entro 400 px, non «tutti».
  • ❌ Ipotesi scartata: il gate segnalava virgole non quotate nelle righe 40, 44 e 46 del csv. Lette con csv.reader: 9 campi su ogni riga, tutti i testi quotati.
  • ⚠️ NON PROVATO: il tocco e il pad veri dentro la cerimonia (la selezione è provata chiamando _select()), le altre sei lingue sulla carta di scambio.

AMNESIA e zuffa stracciano anche il timer dell'avvertimento: il poliziotto non riparte in caccia da solo (SU-1049)2026-09-21

SU-1049 — lotto costruito da Codex sol (P1049, 5 minuti, 1 punto), gate istruito da luna (K1049), review sol (R1049) senza rilievi; sonde rilanciate in locale al gate.

  • super_confuse() (AMNESIA COLLETTIVA, PoliceOfficer.gd:232) e _stun_with_catfight() (zuffa e morso, :900) alzano _warn_epoch come già faceva forget_player() da SU-1045: il create_timer(2.0) lasciato in volo da _enter_warn() al risveglio trova l'epoca cambiata e non chiama più _enter_chase(). Nella zuffa l'epoca si alza dopo i due controlli che possono annullare lo stordimento, così uno stun rifiutato non cancella un avvertimento vero.
  • ✅ Sonda probe_su1049_avvertimento.gd + tools/autotest/probe_su1049.sh: agente al secondo avvertimento puntato su un peer remoto finto (P77), poi AMNESIA o zuffa. A +2,6 s: patrol in entrambi i casi. Controprova --red (l'epoca rimessa al valore armato, senza toccare il file di gioco): chase sul giocatore locale in entrambi i casi, 2 controlli rossi. È esattamente il difetto MP del ticket: con _target_ref azzerato _chase_target() ripiega sull'host.
  • probe_su1045_dormitorio, fase 1c: ancora OK.
  • 📌 La sonda ferma il _process dell'agente dopo l'interruzione, per fotografare la sola decisione del timer: senza, _exit_stun() riporterebbe a patrol mezzo secondo dopo e la zuffa passerebbe anche col buco aperto.
  • ⚠️ NON PROVATO con due processi veri in rete: il criterio MP è misurato col peer simulato. Durate dell'avvertimento, dell'AMNESIA e della zuffa invariate; Retata non toccata.

Dormire al dormitorio fa dimenticare l'inseguimento, anche a un client (SU-1045)2026-09-20

SU-1045 — lotto costruito da architetto-opus insieme a SU-1046 (stesso file); tre giri di correzione al gate dalle review Codex, con quattro difetti veri che il builder non aveva visto.

⚠️ PER LA RELEASE: RPC nuova _srv_shelter_slept in files/homeless_city/scripts/world/World.gd. In Godot 4 gli id delle RPC sono alfabetici: il minimo MP va alzato e va scritto nelle note di rilascio.

  • ✅ SP: _use_shelter avvia _shelter_sleep_sequence() (Interactable.gd:3844), coroutine lanciata senza await perché _use() non diventi a sua volta coroutine. Il velo nero è lo stesso _metro_velo() della metro, con durate proprie 0,28+0,18+0,34 = 0,80 s: gli 0,94 s della metro erano troppo a filo del tetto di 1 s del ticket.
  • 📌 I due tween sono in TWEEN_PAUSE_PROCESS, e serve: la dormita dà XP e può far salire di livello, la cerimonia delle carte mette in pausa l'albero, e un tween BOUND restava congelato col nero in faccia. Misurato: 777 ms anche con paused=true.
  • ⚠️ Preso dalla review, ed era il difetto peggiore: si veniva arrestati DENTRO il nero. L'amnesia scattava dopo la dissolvenza, e con un agente già entro arrest_range al momento della pressione l'arresto arrivava nei primi 0,28 s, a dormita già consumata: misurato 1 arresto e una vita persa, 9 → 8. Ora _shelter_forget_chases() è la prima cosa che succede (:3797), nello stesso frame della pressione, prima del velo e prima dell'orologio: 0 arresti, 9 → 9. Il criterio del ticket («entro 1 s dal ritorno dell'immagine») è un limite superiore, non un appuntamento.
  • PoliceOfficer.forget_player() (PoliceOfficer.gd:274) molla solo chi aveva di mira quel barbone, torna a patrol, prende 6 s di grazia e spegne la sirena nello stesso frame. No-op sui puppet e sugli agenti della Retata; agisce anche su un agente stordito, altrimenti riprendeva la caccia a fine zuffa.
  • ⚠️ Preso dalla review: _enter_warn() lascia in volo un create_timer(2.0) che chiama _enter_chase() senza guardare niente, quindi l'agente al secondo avvertimento tornava in caccia due secondi dopo la dormita — e in MP, con _target_ref azzerato, _chase_target() ripiega sul giocatore locale, cioè l'agente di un altro finiva addosso all'host. Chiuso con _warn_epoch (:700, alzata da forget_player a :298). 📌 Lo stesso buco resta in super_confuse() (AMNESIA) e _stun_with_catfight(), che erano nei NON TOCCARE: è SU-1049.
  • MP chiuso anche per il client. Sui client i poliziotti sono puppet e forget_player() è no-op: la caccia vera gira sull'host contro il puppet di chi dorme. Ora _shelter_forget_chases() delega con World.net_shelter_slept() (World.gd:3207) → RPC _srv_shelter_slept_host_shelter_forget (:3322), che salta raid_officers. Sull'host e in SP resta il giro locale.
  • L'RPC non si fida del client. È any_peer e non porta argomenti — il mittente lo scrive il trasporto, quindi non è falsificabile — ma questo non basta: senza controlli un client poteva spedirla da qualunque punto della mappa e quante volte voleva. _host_shelter_slept_request() (World.gd:3255) accetta solo se il puppet esiste, se il suo corpo tocca davvero il trigger di un dormitorio, e se non ha già dormito lì da meno del cooldown vero di quell'edificio (use_cooldown, 600 s in MP), con il freno per coppia peer+dormitorio e non per peer.
  • ⚠️ Preso dalla review, e rompeva a intermittenza: la prima versione guardava solo il centro del giocatore contro il raggio 22 del trigger, mentre in locale il dormitorio si usa per sovrapposizione delle collisioni. Nella fascia 23÷30 px il client dormiva davvero, si bruciava i 600 s di cooldown e l'host gli rifiutava l'RPC: la polizia continuava a inseguirlo e non poteva ritentare. Ora Interactable.trigger_overlaps() (:3917) e World._body_radius_of() (:3306) leggono i raggi dalle shape vere (trigger 22, corpo del giocatore 8): soglia 30 px, la stessa del gioco.
  • 📌 Trappola pagata, e vale per ogni futuro controllo di posizione in MP: get_overlapping_bodies() sarebbe la strada ovvia e non funziona sui puppet remoti, perché Player._diventa_puppet mette collision_layer = 0 e disabilita la CollisionShape2D (Player.gd:1297) — il motore non li vede mai dentro l'Area2D e nemmeno _on_body_entered scatta. Il raggio va letto dalla shape e confrontato a mano.
  • La reputazione del quartiere NON si tocca (la azzera solo AMNESIA): scelta dichiarata, e stavolta misurata davvero — la sonda parte da −40 e rilegge −40. Nel giro precedente quel controllo restituiva letteralmente true, cioè non misurava niente: l'ha trovato la review.
  • Sonda probe_su1045_dormitorio: 47/47. SP: agente in chase a 70 px, wanted 69,9, una sirena → nero 769 ms → al ritorno wanted 0,00, zero inseguitori, zero sirene, agente in patrol; 07:00 in punto, stesso Giorno, fame/sonno/igiene 99,8, XP +11. Retata: 7 agenti in caccia prima e 7 dopo. Caso client: cheat da lontano rifiutato, da dentro accettato, cheat a raffica rifiutato, peer inesistente rifiutato, bordo a 26 px accettato e a 35 px rifiutato.
  • Il filo è stato provato su due processi ENet veri, e l'anti-cheat ha funzionato in rete: l'host ha risposto esito false a un client che non era al dormitorio.
  • ⚠️ Non provata la scena intera su due peer: un client inseguito che entra davvero nel dormitorio. Il TestBot punta al rifugio solo con energia bassa e fra le 00:00 e le 07:00, e non è pilotabile in una run breve. Le tre parti sono misurate singolarmente (il filo, la validazione e l'applicazione lato host, la delega sul client): serve una prova a mano sui due telefoni.

Chiesa: tenendo premuto le offerte si ripetono da sole, come i depositi in banca (SU-1046)2026-09-20

SU-1046 — lotto costruito da architetto-opus insieme a SU-1045 (stesso file); tre giri di correzione al gate dalle review Codex.

Tester Gas, 19/09: «posso premere una volta sola e non dieci per recuperare la vita?». Una vita costa $100 a scaglioni da $10: dieci pressioni. Nella sua run la telemetria contava 50 interazioni con la chiesa.

  • files/homeless_city/scripts/world/Interactable.gd:504,630: la chiesa monta le rotaie della banca, non una copia. _bank_hold_tick è diventata _hold_tick() e la finestra si apre con _is_hold_repeat_type() and not _is_hold_repeat_closed(); i tre helper nuovi (:1814, :1824, :1837) dicono chi ha la raffica, quando lo sportello è chiuso (cassa colma o nove vite) e qual è lo scaglione (BANK_STEP o CHURCH_STEP). BANK_HOLD_SEC, le due cadenze e BANK_HOLD_ACCEL_SEC restano gli unici numeri.
  • 📌 Le variabili _bank_hold_t/_repeating/_next non sono state rinominate di proposito: probe_su715_deposito_raffica e probe_su959_input le leggono per nome con get(), e rinominarle rompeva due sonde che passano.
  • ✅ Durante la ripetizione l'avviso «OFFERTI $10. Ancora $90…» non si scrive (:3196): il feedback continuo resta il contatore in cifre sul tetto, che legge church_changed a ogni scaglione.
  • ⚠️ Preso dalla review, due volte. Il criterio «a dito staccato non più di una notifica» sembrava rispettato e non lo era: l'avviso della pressione iniziale restava vivo 1,6 s e si sommava a quello della vita. Il primo rimedio — rimandare lo stesso messaggio con permanenza zero — non bastava, perché la dissolvenza dura comunque 0,35 s e la notizia della vita poteva nascere lì dentro. Ora _on_hold_repeat_started() (:2097) stacca la label con quella chiave nell'istante in cui la raffica comincia (_retire_active_notice_like(), :2125, come fa HUD._close_notif_gate).
  • Due controprove rosse, fatte con una copia del file in TMP/ e mai con git stash (il working tree è condiviso): senza la correzione il caso «8 vite, $100 in tasca» dà 2 label; col ritiro morbido invece di quello duro il caso «offerta a $80» dà 2 label. Col codice attuale, 1 in entrambi.
  • 📌 Un parametro della review era sbagliato e il builder l'ha corretto misurando: a $90 il difetto non si vede, perché la vita la compra la pressione iniziale e la notizia parziale non nasce. Il caso che lo scopre è $80, cioè la vita comprata dal primo tick automatico.
  • ⚠️ Trappola della sonda, da ricordare: SU-1034 scarta una notifica la cui chiave è ricomparsa da meno di NOTIF_REPEAT_MIN_SEC (8 s), quindi una fase risultava «una label» per l'antirimbalzo e non per la correzione. Le fasi ora azzerano lo stato delle notifiche prima di partire.
  • Sonda probe_su1046_chiesa_raffica: 34/34. Da 5 a 9 vite in 2.059 ms senza staccare il dito, $400 esatti, 40 offerte e 40 church_changed, la ripetizione si ferma da sola a nove vite. Con $95: nove offerte, $5 in tasca, zero rifiuti a raffica. Tap breve invariato, messaggio identico.
  • Banca invariata e misurata: probe_su715_deposito_raffica OK (11,0 depositi/s prima del secondo, 28,8/s dopo), probe_su548_chiesa OK.
  • ⚠️ Non provato: il tieni-premuto col pad virtuale su un telefono vero (la sonda preme l'azione, non il dito) e la resa a schermo del contatore durante la raffica.

(DESIGN) Di fame si muore CON il chiosco vicino: la premessa del ticket non regge sui dati (SU-771)2026-09-20

SU-771 — lotto costruito da Codex sol; rinomina di conformità su luna, con una correzione mia al gate. Ticket di decisione: nessun file di gioco toccato.

Il ticket partiva da un'ipotesi: «non gli manca il denaro, gli manca il pasto nel posto dove si trova». La mappa che chiedeva — dove stanno i collassi rispetto a chioschi, bidoni e banca — non era mai stata fatta, perché serviva rigenerare la città di ogni seme.

  • files/homeless_city/scripts/tools/probe_su771_mappa.gd (nuova): rigenera le sei città della 0.50/0.51 col seme vero della run e misura le distanze. Il seme c'era (RunRecorder lo scrive in run_start) ma non in runs.json: estrai.py non lo copiava, e va ripreso dai file grezzi.
  • 📌 RISPOSTA: no, i collassi non sono più lontani dai chioschi della media. Distanza mediana dei 6 collassi dal chiosco più vicino: 278,3 px. Riferimenti nella stessa città: 411,5 px per punti casuali davvero raggiungibili, 396,7 px per i punti attraversati senza morire. Quattro casi su sei sono più vicini del riferimento casuale della propria città; solo Mercato e Fumarola sono più lontani di entrambi.
  • ⚠️ CAMPIONE INSUFFICIENTE, ed è scritto in prima riga del documento: 6 collassi in 6 run su 2 device, contro le ~30 run indipendenti che servirebbero (30 per quartiere per decidere un piazzamento locale). Con un collasso per quartiere non si distingue un problema di layout da due code casuali.
  • 📌 L'avviso «Sei già sazio» compare 24 volte in 37 run valide 0.50/0.51 (5 nelle 6 run finite per fame); il corrispettivo del gelato altre 50.
  • 📌 Raccomandazione: servono più dati. Non un chiosco nuovo vicino a banca e metro, non un avviso più forte: i numeri di oggi non reggono nessuna delle due.
  • 📌 Documento completo con la tabella per collasso in TMP/su771/MAPPA.md, misure in TMP/su771/misure.json.
  • ⚠️ Preso al gate, ed è colpa di un mio controllo scritto male: il rg con cui avevo chiesto di verificare la rinomina cercava le parole italiane anche dentro le stringhe, e 7 chiavi del JSON di output sono state spezzate per concatenazione ("random" + "_" + "mediana_chiosco_px") pur di farlo passare. Le chiavi di un output sono un dato, non un identificatore: ricomposte, e verificato che tutte e 36 le chiavi del JSON già prodotto siano ancora nel sorgente.
  • ⚠️ Non provato: la causalità fra l'avviso e il collasso, la distanza pedonale (le misure sono euclidee), l'indipendenza dei campioni di posizione, e che il layout storico della 0.50/0.51 sia identico a quello rigenerato oggi.

(DESIGN) Le carte dei superpoteri: tre strade prototipate, 12 frasi d'effetto e il vincolo di spazio misurato in 8 lingue (SU-1047)2026-09-20

SU-1047 — lotto costruito da dev-sonnet. Ticket di decisione: nessun file di gioco toccato, la scelta è di Ivan.

Il tester Gas, sulla 0.51: «ho sbloccato il super potere della tromba, non ho ancora capito cosa fa». Oggi la carta dice nome, «SUPERPOTERE · liv. N» e «10s · ricarica 1:30», e l'unico effetto scritto è quello di SCOREGGIA ATOMICA.

  • ✅ Tutte e tre le strade del ticket prototipate con provini veri a 844×390, testo «huge», istanziando le scene reali senza modificarle: (a) riga d'effetto sulla carta in primo piano, (b) riga sotto il ventaglio che segue la carta evidenziata, (c) pagina del Quaderno. Scatti in TMP/su1047/.
  • Le 12 frasi, ricavate leggendo SuperPower._fire() e _tick_active() (files/homeless_city/scripts/systems/SuperPower.gd:521-611) e non i nomi comici, tutte ≤40 caratteri e senza emoji — qui perché non vadano perse con la TMP/:
    idnome mostratofrasecar.
    stomaco_ferroSUPER SCOREGGIAOnda d'urto: stende tutti intorno33
    sonno_leggeroPISOLINO LAMPOPisolino di 3s: energia +5027
    pelle_cuoioAURA MEFITICAPuzza 12s: passanti in fuga27
    carismaCOMIZIO IN PIAZZAComizio 10s: la gente ti dona soldi35
    kazooCONCERTONEParte un concerto, guadagni suonando36
    braccioGATTLINGRaffica di gatti per 8 secondi30
    suolaSCATTO DEL CENTOMETRISTAScatto: velocità doppia per 5s30
    topo_bidoneAPRITI SESAMOBidoni aperti e lucenti per 15s31
    bravo_ragazzoAMNESIA COLLETTIVAAzzera il ricercato, polizia confusa36
    calamitaASPIRATUTTOAttira tutti i soldi per 20s28
    fischietto_caniIL BRANCOBranco di cani, polizia confusa 10s35
    scoreggia_atomicaSCOREGGIA ATOMICA (solo MP)Solo MP: ruba soldi ai rivali vicini36
  • Vincolo di spazio misurato, non stimato: 96 combinazioni (12 frasi × 8 lingue) passate per un Label vero a font 11 e 156 px di larghezza (CARD_W 200 − 2×TITLE_MARGIN 22), autowrap WORD_SMART, get_line_count() letto dopo un frame. Nessuna supera le 2 righe. Il francese fa da metro (media 32,4 caratteri, riga più lunga 39 su kazoo); il cinese è fuori scala e non si confronta 1:1. La fascia verticale disponibile sotto l'icona è ~132 px: nome + livello + 2 righe ci stanno con margine.
  • ⚠️ Le 7 traduzioni non italiane sono bozze fatte per avere un testo vero da mandare a misura, non le traduzioni definitive: la misura vale quanto le bozze, e la traduzione vera è uno dei ticket proposti.
  • 📌 Dove altro va la frase: HUD no (la casella è icona + barra, non c'è spazio e il tooltip non esiste), pausa no (non c'è una schermata dove metterla), Quaderno sì, in aggiunta e non in alternativa — è l'unico posto dove rileggerla a mente fredda.
  • 📌 Raccomandazione: la (a), più la (c) come glossario permanente. La (b) non è scartata ma non è prioritaria: aggiunge una terza fascia di testo che rischia di essere ignorata come lo è oggi.
  • ⚠️ Da decidere prima di implementare: nella (a) la frase prende il posto della riga «10s · ricarica 1:30» sulla carta evidenziata — nel provino quella carta non mostra più i tempi. Se i tempi devono restare, la (a) va ripensata o diventa la (b).
  • 📌 Quattro ticket di implementazione proposti e non creati: frase sulla carta; riga sotto il ventaglio (solo se Ivan preferisce quella); glossario nel Quaderno; traduzione vera delle 12 chiavi.

Street Times, pagina 2: l'inchiostro era cancellato da una riga rimasta orfana (SU-1044)2026-09-20

SU-1044 — lotto costruito da dev-sonnet; quattro correzioni al gate dalla review Codex, tutte sulla sonda.

  • files/homeless_city/scripts/ui/HUD.gd:7500: tolta game_over_label.remove_theme_color_override("font_color"). L'archeologia, fatta prima di correggere: e6a6868 (SU-1007) aveva aggiunto insieme quella remove e una add_theme_color_override dorata per il quinto giorno; 9371b4a ha spostato l'annuncio del quinto giorno nei popup del menu e tolto solo la add, lasciando la remove orfana a cancellare, a ogni game over, l'inchiostro Color(0.086,0.086,0.098) che HUD.tscn:128 dà a ReasonLabel. Il corpo ricadeva sul colore chiaro del tema, su carta chiara.
  • HUD.tscn non toccato: l'override nella scena era già giusto, veniva cancellato a runtime. Nessun codice imposta più un colore lì, quindi non c'è più niente da ripulire.
  • ✅ La classifica eredita lo stesso colore (HUD.gd:6690), quindi il difetto valeva anche per la terza pagina MP.
  • ⚠️ Il difetto più interessante l'ha trovato la review, ed era nella MISURA, non nel gioco. Il contrasto era calcolato sul pixel più scuro dell'intero GameOverBox: bordi e testata lo portavano sopra soglia da soli. Controprova: col testo nascosto dava lo stesso identico 16,29:1. Ora la misura è ristretta al rettangolo visibile di game_over_label, e i numeri veri sono 14,96:1 nei due finali, contro 1,17:1 con il testo nascosto — cioè la sonda ora sa diventare rossa. Un secondo falso positivo è saltato fuori nello stesso giro: per il motivo lungo della Retata lo scroll traboccava sotto il filetto del foglio, nel velo scuro, e misurava quello.
  • ⚠️ Preso dalla review: il ripiego del motivo della Retata conteneva un'emoji che finiva nella UI di game over. Tolta (audit_emoji_pittogrammi.py → 0). Corretti anche il default di scrittura, che era /tmp invece di TMP/ alla radice, e gli identificatori italiani della sonda.
  • ⚠️ Preso dal cancello: i due game over di fila ricostruivano il mondo invece di riusare la stessa istanza di HUD, e il difetto viveva proprio nello stato del nodo. Rifatto: ora game_over_label è lo stesso oggetto nei due passaggi da _build_gameover_body().
  • 📌 Il corpo dorato del quinto giorno su carta fa 1,30:1 (il ticket ne stimava ~1,3): oggi quel ramo non esiste più nel codice, quindi non è urgente. Se un giorno si rimette, tre tinte leggibili su carta, sceglie Ivan: bronzo Color(0.42,0.28,0.06) = 6,86:1; senape bruciata Color(0.50,0.34,0.04) = 5,34:1; ottone scuro Color(0.47,0.35,0.10) = 5,37:1.
  • shot_su1007 3/3 e probe_su1007 66/66 rilanciati davvero, entrambi OK. Scatti in TMP/su1044/.
  • ⚠️ Non provata la terza pagina MP a schermo (nessuna sessione MP allestita): stessa fonte di colore, verificata in SP.
  • 📌 Trappola pagata, utile alla prossima sonda: nominare RunReport.INK per nome-di-classe in uno script -s forza il compilatore a compilare RunReport.gd prima che gli autoload esistano, e rompe la compilazione a cascata per tutto il processo. Si risolve con load() dopo l'attesa degli autoload e get_script_constant_map().

Il gatto non colpisce più chi sta dall'altra parte di una parete: il colpo era solo una distanza (SU-1043)2026-09-20

SU-1043 — lotto costruito da Codex sol; una correzione al gate dalla review Codex.

  • files/homeless_city/scripts/entities/CatProjectile.gd:284-286,326-328,346-347,379-380,443-444,455-456: prima di ogni colpo entro HIT_RANGE ora si chiede World.hits_tall_obstacle_segment(), la geometria che SU-1008 aveva già in casa. Il muro della pensilina è spesso ~6 px: nei fotogrammi prima di toccarlo il gatto era già a meno di 18 px da chi ci stava appoggiato dall'altra parte, e lo prendeva.
  • HIT_RANGE e le altre portate restano quelle di prima, e la fisica di SU-1008 (_tall_open_rects, blocked) non è toccata: si è aggiunto un controllo, non cambiato un numero.
  • ⚠️ Preso dalla review, non dal builder, ed era il criterio 3 del ticket: i district_foes (i fenicotteri) erano rimasti scoperti, perché quel ramo passa da _segment_reaches(). Il report del primo giro dichiarava «nessun ramo scoperto» e non era vero. Ora l'elenco è stato rifatto leggendo il file, ramo per ramo.
  • ⚠️ Preso dalla review: hits_tall_obstacle_segment() non è una query pura — passando sul riccone gli fa scattare cat_bounce(). Un semplice controllo di visibilità poteva quindi far sobbalzare il riccone anche con il gatto che non gli volava addosso. Misurato (cat_bounce() 1 → 0) e curato con un parametro opzionale notify_bounce su files/homeless_city/scripts/world/World.gd:8215-8258: il default resta il comportamento di oggi e ProjectileCrash.gd:101-102, l'unico altro chiamante, non cambia.
  • Sonda del ticket, 20 semi per geometria: fondo, parete sinistra e parete destra, in entrambi i versi (da dentro verso fuori e da fuori verso dentro) → 0 colpi su 20 ovunque, e 20/20 gatti morti sul muro come oggi. Fenicottero oltre la parete: 0/20.
  • Controprova che il colpo normale non è cambiato: bersaglio a 10 px senza parete in mezzo → 20/20 colpiti. probe_su1008 181/181; camioncino di SU-683 invariato.
  • probe_su1008_mp poi lanciata dall'orchestratore fuori dalla sandbox, e PASSA: falliti=0 su host e su guest, esito che coincide fra i due peer entro 1 px sui due casi, stesso seme di città e stessa pensilina. In sandbox non si apriva (il wrapper rifiuta quel SU_TMP e lì ENet non parte), e per quello il commento Jira del lotto la dava per non provata.
  • ⚠️ Resta non misurata la geometria NUOVA di questo ticket a due peer: quella sonda gira su un peer solo. Che l'esito sia lo stesso su host e client è garantito dal fatto che il controllo precede ogni RPC di colpo sull'istanza autoritativa — un argomento di codice, non una misura.
  • 📌 Residuo noto, fuori dal perimetro del ticket: lo scontro gatto-contro-gatto (CLASH_RANGE) può ancora avvenire attraverso la stessa parete. Il ticket parla dei bersagli che usano HIT_RANGE e vieta di toccare le altre portate: se va curato, è un ticket a parte.
  • 📌 Il video del tester mostra anche una dozzina di passanti ammucchiati contro quel muro, col poliziotto fermo lì invece di girare all'ingresso. Non è in questo ticket.

COMIZIO IN PIAZZA: il megafono possiede e chiude il proprio loop, invece di accendere la suonata del barbone (SU-1042)2026-09-20

SU-1042 — lotto costruito da Codex sol; una correzione al gate dalla review Codex.

  • files/homeless_city/scripts/systems/SuperPower.gd:498: il COMIZIO non passa più da Player.super_show_fx("music"). Quel percorso metteva lo stream in loop del busking sul lettore del giocatore e faceva play() con _is_busking falso: nessuno ne era proprietario, quindi né il movimento né una nuova pressione lo fermavano più. R1009b aveva tolto dal percorso generico il solo CONCERTONE (if id != "kazoo"), e il comizio ci era rimasto dentro.
  • files/homeless_city/scripts/systems/SuperPower.gd:293-295,617-621,1548-1571: il suono del comizio vive sul lettore di SuperPower e la proprietà è legata all'identità dello stream. A fine effetto si ferma solo quello: una suonata normale iniziata durante il comizio sopravvive e finisce quando finisce lei.
  • files/homeless_city/scripts/player/Player.gd:763-771: l'uscita del Player locale ripulisce gli effetti locali dell'autoload; i puppet MP restano esclusi.
  • 📌 Il secondo sospetto del ticket era vero: anche SuperPower._play_sfx con SFX_BY_FX["music"] = busking.wav era nel percorso. Escluso misurando, non ragionando: dopo la cura lo stream parte ma ha un proprietario e si spegne a 10 s.
  • 📌 Elenco chiesto dal ticket: nessun altro super passa da super_show_fx("music") o avvia un loop nel percorso generico. kazoo è escluso dall'intero blocco, il loop dei cani è dedicato.
  • ⚠️ Preso dalla review, non dal builder: gli identificatori nuovi erano in italiano (_comizio_sfx_stream, _play_comizio_sfx…), contro la convenzione «nomi nel codice in inglese». Rinominati in _rally_*; l'id nei dati resta "carisma".
  • ⚠️ Sonda rafforzata dalla review: probe_su1042.gd guardava l'audio solo all'avvio e a 10,5 s, quindi un comizio che si fosse interrotto dopo 2 s sarebbe passato verde. Aggiunto il campione a 5 s, che pretende il suono ancora acceso, e l'interruttore --short-stream per la controprova: con lo stream corto la sonda diventa rossa (10/11, KO proprio a 5 s).
  • Sonda probe_su1042.gd: 11/11. A 10,5 s zero lettori di busking attivi, e ancora zero dopo altri 30 s. Zero anche dopo end_local_run_effects, game over e uscita dalla scena.
  • 📌 probe_su100918/18, non gli 11/11 che citava il ticket: la sonda è stata riscritta da R1009 il 17/09 (files/homeless_city/scripts/tools/probe_su1009.gd:2-8,80-204). Nessuna verifica è saltata.
  • ⚠️ Non provato: la parità host/client in MP (nessuna sonda a due peer) e l'ascolto percettivo, impossibile in headless. Durata e proprietà sono misurate sugli AudioStreamPlayer, non a orecchio.

La Collina è la seconda fermata e si apre insieme al Mercato; moltiplicatori nuovi, ma la sonda col bot non sa confermarli (SU-776, SU-777)2026-09-19

SU-776 + SU-777 — lotto costruito da dev-sonnet (stesso file, un lotto solo); due correzioni dalla review Codex.

  • ✅ SU-776 — ORDER in files/homeless_city/scripts/autoload/Quartieri.gd e BOARD_QUARTIERI in files/homeless_city/scripts/autoload/OnlineLeaderboard.gd: centro, collina, mercato, gattopoli, solleone, brina, fumarola, nottefonda. Id, config, record e board invariati. files/homeless_city/scripts/ui/MainMenu.gd non toccato: la mappa legge già Quartieri.all_ids().
  • ✅ La review ha cercato ogni consumatore dell'indice: non ce ne sono. Selezione, record, classifiche, telemetria, lobby e tutorial passano tutti per id, e la difficoltà non scala con la posizione nella linea: il riordino non cambia Mercato, Gattopoli e le altre.
  • ✅ Sblocco: la Collina si apre con l'impresa del Mercato (negozi_diversi_run_max >= 3, files/homeless_city/scripts/autoload/MetaProgress.gd), non più al giorno 3. Chi l'aveva già aperta col vecchio criterio la tiene.
  • ⚠️ Trovato dalla review, non dal builder: la migrazione si timbrava anche quando non c'era niente da aprire. Un salvataggio cloud più vecchio arrivato dopo, con il Mercato aperto, trovava il timbro già vero e la Collina restava chiusa per sempre. Ora _migrate_collina_unlock() è una riconciliazione che gira a ogni caricamento e scrive solo quando apre davvero.
  • ✅ L'indizio del lucchetto citava ancora «il terzo giorno»: riscritto nelle 8 lingue (QUARTIERE_INDIZIO_COLLINA, «si dice che lassù salga solo chi i negozianti li conosce per nome…») e nel ripiego di Quartieri.gd. La prima stesura diceva «più di un negoziante» mentre la soglia è 3: corretta anche quella dalla review.
  • Sonda tools/autotest/probe_su776.sh: 28/28 (migrazione col contatore, col solo flag, controprova senza sblocchi inventati, cloud tardivo, 3 negozi in una run che aprono entrambe, selected_id che resta Brina, nessuna fermata richiusa). Scatto della mappa a 844×390 in TMP/su776/su776_metro.png: IL CENTRO, LA COLLINA, IL MERCATO, GATTOPOLI sulla riga alta.
  • ✅ SU-777 — moltiplicatori della Collina come da ticket: elemosina 1,10, prezzi 1,15, bidoni_resa 1,00, wanted 1,10, polizia 1,25 (5 agenti invece di 4).
  • ⚠️ SU-777, criterio 1 NON confermato. Sonda col bot a 8 semi (7701-7708, 240 s): Collina 1,000 vite al minuto contro 0,375 del Centro, bersaglio 0,40-0,50. Applicata la regola del ticket (wanted a 1,00): peggiora, 1,375. Delle 38 vite perse il 45% è del giullare, che nasce quando una carta arriva al livello 6 e non dipende dal quartiere, il 34% arresti, il 16% fenicotteri. La sonda non misura questa leva: wanted resta 1,10 e la conferma viene dalla telemetria vera. Criterio 2 rispettato: punteggio mediano 1.126 contro 950, rapporto 1,19.
  • 📌 Dato vero di confronto, dal referto 0.50+0.51 di oggi (TMP/telemetria_050/RAPPORTO.md): con la taratura vecchia la Collina fa 1,215 vite al minuto su 8 run, il Centro 0,823 su 4.

CAMBIA NOME: con la tastiera aperta restano in alto solo il campo, CONFERMA e ← INDIETRO (SU-1041)2026-09-19

SU-1041 — lotto costruito da dev-sonnet; una correzione dalla review Codex.

  • files/homeless_city/scripts/ui/MainMenu.gd, solo le funzioni _nome_*: il modo compatto scatta appena la tastiera è aperta (kb > 0 e campo modificabile), non più solo quando coprirebbe il campo — sull'iPad non lo copriva quasi mai e il compatto non scattava. Campo e CONFERMA sulla stessa riga, ← INDIETRO sotto, tutto ancorato in alto; titolo, testo, regole e riga degli esiti nascosti, non distrutti.
  • ✅ ← INDIETRO resta visibile (differenza voluta dal codice di INVITA UN AMICO) e a tastiera aperta la chiude soltanto: passa da _request_back(), dove la guardia di SU-982 c'era già. CONFERMA chiude la tastiera prima di confermare. Tocco fuori = chiude, catcher SU1041ToccaFuori.
  • ⚠️ Trovato dalla review, non dal builder: il catcher reagiva sia al ScreenTouch sia al MouseButton emulato dallo stesso dito. Il primo evento rimetteva il layout normale e il secondo poteva cadere su ← INDIETRO a riposo (uscita dalla schermata) o sul campo (tastiera riaperta). Ora la chiusura è differita e tutti e due gli eventi li consuma il catcher. 📌 Il catcher di SU-1002 ha la stessa forma ma lì ← INDIETRO in compatto è nascosto; non toccato.
  • Sonda tools/autotest/probe_su1041.sh, rilanciata dopo la correzione, ESITO=OK: 3 nodi × 3 altezze (30, 45, 65%) × telefono 844×390 e iPad 1024×768 = 18/18 col fondo sopra la tastiera; i quattro modi di chiudere riportano i rettangoli di tutti i nodi identici a quelli da fermo; con Epic (editable=false) il compatto non scatta. Scatti in TMP/su1041/.
  • ⚠️ probe_su932_tastiera dà 8 OK e 22 KO, identici riga per riga su HEAD e sul lotto (controprova fatta scambiando il solo MainMenu.gd): non è una regressione di questo lotto, i tocchi simulati non arrivano al campo già sul codice committato. Da capire a parte. shot_su454_tastiera verde.
  • ⚠️ Non provato: iPhone e iPad veri, con un account che può cambiare nome (con Epic il campo non è modificabile e il compatto non scatta, per scelta di SU-506).

La caduta dell'host in multiplayer lascia una riga in telemetria: evento `mp_connection_lost` con `match_id` condiviso, senza RPC nuove (SU-1023)2026-09-19

SU-1023 — lotto costruito da architetto-opus; una correzione dalla review Codex.

  • files/homeless_city/scripts/autoload/NetworkManager.gd: segnale nuovo connection_lost(reason), emesso sempre prima di _quiet_close() da un imbuto solo (_report_connection_lost), col motivo già enumerato: host_left (uscita esplicita dell'host), transport_lost, peer_timeout, kicked. files/homeless_city/scripts/autoload/RunRecorder.gd lo ascolta e scrive mp_connection_lost con why, mid, role, tr, n, run_id, ver, env. Nessun PUID.
  • 📌 Il match_id (8 esadecimali, generato dall'host al via e alla rivincita) viaggia nella riga dell'host del registro di lobby, chiave mid, che l'host manda già a tutti: nessuna RPC nuova, nessuna firma toccata — in Godot 4 gli id delle RPC sono alfabetici e una RPC in più romperebbe la compatibilità. Un client 0.51 ignora la chiave; un host vecchio dà mid vuoto, senza errori. Diff puramente additivo: 191 righe aggiunte, 0 tolte.
  • ⚠️ Trovato lavorando: con l'host ucciso la caduta non la vede ENet ma il lease di SU-1038 («host muto da 5010 ms»), poi SU-1029 trasloca l'host. Per questo l'emissione sta anche in _lease_host_lost(): con la sola _on_server_disconnected() l'evento non sarebbe uscito.
  • ⚠️ Trovato dalla review, non dal builder: dopo un trasloco riuscito il guard anti-doppione restava alzato, e la caduta del *nuovo* host nella stessa partita sarebbe stata zittita. Ora _migra_finisci(true) lo riazzera.
  • Sonda a due peer ENet tools/autotest/probe_su1023_mp.sh (porta 7023, HOME separate e svuotate), rilanciata dopo la correzione: host che chiude → sul client 1 evento host_left, 159 byte; host ucciso → 1 evento transport_lost, 165 byte; partita finita normalmente → 0 eventi su entrambi i peer. match_id identico sui due lati. Tetto di 200 byte rispettato.
  • 📌 La sonda non passa SU911_PERF=1 come chiedeva il ticket: quella variabile spegne RunRecorder. La strada ENet la garantisce l'assenza di files/homeless_city/eos_credentials.cfg nella copia di lavoro. ⚠️ Per lo stesso motivo non va lanciata insieme ad altre sonde che risincronizzano /tmp/su_501_proj: rimettono le credenziali e l'host parte su EOS e si pianta (successo in questo giro).
  • tools/telemetria/conta_mp_connection_lost.py: conta gli eventi per versione, per sessione (mid) e per motivo da una cartella di run_*.jsonl.gz. Sugli 809 file storici: 0 eventi, che è la premessa del ticket.
  • ⚠️ Non provato: peer_timeout e kicked (oggi nessun percorso espelle un peer in partita: il ramo c'è per non lasciare l'enumerazione monca), EOS vero, più di 2 peer, il match_id nuovo alla rivincita.

CANCELLA ACCOUNT non congela più l'account: la voce c'è solo se il backend c'è, e una pratica senza ricevuta si annulla solo quando è provato che la richiesta non è partita (SU-1039)2026-09-19

SU-1039 — due giri su Codex astra (lotti A1 e A2); il secondo nasce dalla review incrociata, non dal builder.

  • Settings.account_deletion_available() (files/homeless_city/scripts/autoload/Settings.gd): unica verità fra menu e coordinatore, vale [api] url non vuoto. Senza backend begin_account_deletion rifiuta subito: niente pratica, sync non sospesa, PUID non bloccato. Nessun URL cablato nel client.
  • ✅ Backend che fallisce: si annulla (sync ripresa, PUID sbloccato, pratica chiusa) solo se la richiesta non è arrivata al server — backend assente nella build, DNS fallito, connessione rifiutata. NicknameRegistry.delete_account dice quale dei due con il campo nuovo consegna (non_inviata / ignota).
  • ⚠️ Trovato dalla review, non dal builder: il primo giro annullava anche su timeout e 5xx, e il tentativo dopo nasceva con un request_id nuovo. Se il server aveva già cancellato e l'ACK si era perso, il client riprendeva la sync su un account cancellato e l'idempotenza non copriva il secondo invio. Ora un esito ambiguo lascia la pratica in BACKEND con lo stesso request_id e la sospensione, e un DNS fallito *dopo* un tentativo ambiguo non la annulla più.
  • ✅ Annullamento a prova di crash: prima si salva cancelling nella pratica, poi si sblocca la sync, poi si svuota. All'avvio una pratica con cancelling finisce da dov'era; riprendendo una pratica in BACKEND la sospensione di ProgressSync si verifica e si riacquisisce prima di chiamare il backend.
  • ✅ Recupero per chi ha premuto il tasto sulla 0.51: all'avvio una pratica senza il marchio api (le nuove lo portano), in BACKEND, senza ricevuta e con backend assente si sblocca da sola, riga [SU-1039] nel log. Una pratica col marchio non si annulla mai da sola; una con la ricevuta non si tocca.
  • ✅ Menu (files/homeless_city/scripts/ui/MainMenu.gd, _account_visible_rows): la disponibilità decide solo l'apertura di una pratica nuova; stato e ripresa di una pratica attiva restano visibili anche senza URL.
  • 📌 La sospensione resta prima del backend, non dopo l'ACK come proponeva il ticket: i commenti di ProgressSync.begin_account_deletion la motivano con le scritture concorrenti. Si è resa annullabile la pratica invece di spostare la sospensione.
  • Sonda files/homeless_city/scripts/tools/probe_su1039.gd: 69 PASS, 0 FAIL (scenari a-j, Settings e ProgressSync veri, trasporto HTTP finto sul registro vero, crash simulati caricando da disco gli stati intermedi). Regressioni: SU-1019 41/41, SU-1020 5/5 scenari e 15/15 chiavi in 8 lingue (probe_su1020.gd aggiornata: l'esito ambiguo tiene la stessa pratica), SU-1021 verde.
  • ⚠️ Non provato: backend ed EOS veri, l'idempotenza lato server del request_id, un crash con il processo ucciso davvero, la resa grafica del menu. Le note in-game della 0.52 (punto 4 del ticket) si scrivono alla release.

Xcode 27 installato accanto al 26.6: la build di prova e il plugin di login compilano con l'SDK iOS 27, senza warning nuovi (SU-1040)2026-09-19

  • ✅ Xcode 27.0 (27A266a) in /Applications/Xcode-27.app, dallo .xip scaricato da Ivan (firma Apple verificata); xcode-select resta sul 26.6, quindi la catena delle release non cambia. Licenza e primo avvio li ha lanciati Ivan (vogliono sudo).
  • ✅ Build AdHoc con DEVELOPER_DIR su Xcode 27: archive ed export riusciti, binario sdk 27.0 / minos 15.0. Warning uguali a un archive col 26.6 dallo stesso export: 7 righe, 6 tipi, nessuno nuovo.
  • ✅ Plugin nativo di login compilato con Xcode 27 da una copia dello script che scrive in TMP/: 0 warning, 0 errori, come col 26.6. Il binario del progetto non è stato toccato.
  • ✅ Installata e avviata sull'iPhone 14 Pro (iOS 26.6.2) con devicectl di Xcode 27, il processo del gioco resta vivo (il primo tentativo era fallito con CoreDeviceError 4016 perché l'iPhone era bloccato e lontano dal Mac).
  • ✅ Lista del piano provata da Ivan sull'iPhone con la build Xcode 27: 8 voci OK su 8 (avvio, login Apple/Epic, audio dopo interruzione, safe area, tastiera del nickname, push, ritorno da background, chiusura con swipe).
  • 🔧 tools/release/iphone_adhoc.sh: il device si sceglie con SU_DEV_ID=<UDID> (default l'iPhone 14 Pro, come prima). Il profilo «Street University AdHoc» contiene già l'iPad Air, che è anche già su iPadOS 27.0: nessun profilo da rifare.
  • ✅ Build Xcode 27 sull'iPad Air con iPadOS 27.0: 9 voci OK su 10, rotazione e app affiancate comprese. ❌ KO la tastiera sul campo del nickname: Ivan sospetta che non andasse già prima (mai provata sull'iPad), va in un ticket a parte nello Sprint 16.
  • ⚠️ Non provato ancora: la build TestFlight 0.51 su iPadOS 27 e il tester con un iPhone su iOS 27.
  • Registro dei passi in docs/IOS_27_VERIFICA.md, sezione SU-1040.