diff --git a/assets/scenes/mini/provinciale-viale.png b/assets/scenes/mini/provinciale-viale.png new file mode 100644 index 0000000..78fe760 Binary files /dev/null and b/assets/scenes/mini/provinciale-viale.png differ diff --git a/assets/scenes/provinciale-viale.png b/assets/scenes/provinciale-viale.png new file mode 100644 index 0000000..dc59f5a Binary files /dev/null and b/assets/scenes/provinciale-viale.png differ diff --git a/assets/voices/andrea-1c43e80b87.mp3 b/assets/voices/andrea-1c43e80b87.mp3 new file mode 100644 index 0000000..6f1c09d Binary files /dev/null and b/assets/voices/andrea-1c43e80b87.mp3 differ diff --git a/assets/voices/andrea-41e25f6dd0.mp3 b/assets/voices/andrea-41e25f6dd0.mp3 new file mode 100644 index 0000000..3b0fa75 Binary files /dev/null and b/assets/voices/andrea-41e25f6dd0.mp3 differ diff --git a/assets/voices/andrea-5bd253332d.mp3 b/assets/voices/andrea-5bd253332d.mp3 new file mode 100644 index 0000000..7958c32 Binary files /dev/null and b/assets/voices/andrea-5bd253332d.mp3 differ diff --git a/assets/voices/andrea-6248354f0f.mp3 b/assets/voices/andrea-6248354f0f.mp3 new file mode 100644 index 0000000..bc50e99 Binary files /dev/null and b/assets/voices/andrea-6248354f0f.mp3 differ diff --git a/assets/voices/andrea-701e3d4e55.mp3 b/assets/voices/andrea-701e3d4e55.mp3 deleted file mode 100644 index 2b0718f..0000000 Binary files a/assets/voices/andrea-701e3d4e55.mp3 and /dev/null differ diff --git a/assets/voices/andrea-77832a5082.mp3 b/assets/voices/andrea-77832a5082.mp3 new file mode 100644 index 0000000..e05b1ef Binary files /dev/null and b/assets/voices/andrea-77832a5082.mp3 differ diff --git a/assets/voices/andrea-9618e5db93.mp3 b/assets/voices/andrea-9618e5db93.mp3 new file mode 100644 index 0000000..4d26014 Binary files /dev/null and b/assets/voices/andrea-9618e5db93.mp3 differ diff --git a/assets/voices/andrea-c0180ddbd4.mp3 b/assets/voices/andrea-c0180ddbd4.mp3 deleted file mode 100644 index 2dc4bef..0000000 Binary files a/assets/voices/andrea-c0180ddbd4.mp3 and /dev/null differ diff --git a/assets/voices/andrea-dc49e0c100.mp3 b/assets/voices/andrea-dc49e0c100.mp3 new file mode 100644 index 0000000..e45527b Binary files /dev/null and b/assets/voices/andrea-dc49e0c100.mp3 differ diff --git a/assets/voices/andrea-e3d25b718c.mp3 b/assets/voices/andrea-e3d25b718c.mp3 deleted file mode 100644 index e0bea92..0000000 Binary files a/assets/voices/andrea-e3d25b718c.mp3 and /dev/null differ diff --git a/assets/voices/andrea-e82710e398.mp3 b/assets/voices/andrea-e82710e398.mp3 new file mode 100644 index 0000000..006ceef Binary files /dev/null and b/assets/voices/andrea-e82710e398.mp3 differ diff --git a/assets/voices/index.json b/assets/voices/index.json index 558e20e..738fcfd 100644 --- a/assets/voices/index.json +++ b/assets/voices/index.json @@ -619,6 +619,22 @@ "file": "andrea-d39b7f7e61.mp3", "sec": 8.25 }, + "andrea|I negozi sotto i palazzi, al riparo degli alberi. Con la festa domani, stasera chiudono tardi.": { + "file": "andrea-77832a5082.mp3", + "sec": 6.76 + }, + "andrea|Gli alberi del viale: d'estate fanno un'ombra che in piazza se la sognano.": { + "file": "andrea-e82710e398.mp3", + "sec": 3.77 + }, + "andrea|Arrampicarmi? Ho ventisette anni, non sette.": { + "file": "andrea-1c43e80b87.mp3", + "sec": 4.92 + }, + "andrea|Case coi balconi pieni di fiori. Qui il palio si sente da lontano: il giusto.": { + "file": "andrea-5bd253332d.mp3", + "sec": 6.36 + }, "andrea|Più avanti c'è La Luna, e poi Campomigliaio, il mio rione: i blu.": { "file": "andrea-a96e07a9f3.mp3", "sec": 5.92 @@ -639,9 +655,13 @@ "file": "andrea-7883437f7d.mp3", "sec": 3.46 }, - "andrea|La Sieve. Più sassi che acqua: a farci il bagno ci si sbuccia le ginocchia.": { - "file": "andrea-c0180ddbd4.mp3", - "sec": 6.46 + "andrea|La Carza, la stessa che passa sotto il ponticino per Cardetole. Più sassi che acqua.": { + "file": "andrea-6248354f0f.mp3", + "sec": 6.38 + }, + "andrea|Da quando scavano le gallerie dell'alta velocità, d'estate sparisce. Torna viva d'inverno.": { + "file": "andrea-41e25f6dd0.mp3", + "sec": 7.07 }, "andrea|Scendere nel greto ora? Il palio non sta mica sotto un sasso.": { "file": "andrea-cf81a4a625.mp3", @@ -655,13 +675,13 @@ "file": "andrea-fd634c7a9b.mp3", "sec": 3.81 }, - "andrea|Una casa bianca sotto il bosco, coi pini sopra. Bella vista sulla Sieve, e sui camion della Provinciale.": { - "file": "andrea-701e3d4e55.mp3", - "sec": 7.65 + "andrea|Una casa bianca sotto il bosco, coi pini sopra. Bella vista sulla Carza, e sui camion della Provinciale.": { + "file": "andrea-9618e5db93.mp3", + "sec": 6.65 }, - "andrea|Il muretto lungo la Sieve, tutto licheni. È più vecchio di me, e sta in piedi meglio.": { - "file": "andrea-e3d25b718c.mp3", - "sec": 6.54 + "andrea|Il muretto lungo la Carza, tutto licheni. È più vecchio di me, e sta in piedi meglio.": { + "file": "andrea-dc49e0c100.mp3", + "sec": 6.08 }, "andrea|Il pallaio, dove si gioca a bocce. Qui un punto si discute più di un rigore.": { "file": "andrea-e94382adb6.mp3", diff --git a/docs/STATO.md b/docs/STATO.md index e356d0e..429b12a 100644 --- a/docs/STATO.md +++ b/docs/STATO.md @@ -103,6 +103,12 @@ I prompt fissi (stile, scene, foglio sprite) sono in `prompts/STILE.md`, escluso **ancora da collegare** (camminata, hotspot, uscite, posto sulla mappa). Andrea, 24 ottobre: ci si arriva dal bivio della Via Provinciale (verso stazione e forno), andando a sinistra; ma prima viene **La Luna**, che Andrea deve ancora fotografare: si collega bivio → La Luna → Campomigliaio. +- **Verso La Luna** (24 ottobre, foto di Andrea): dal bivio, a sinistra, il viale alberato della Provinciale + (`provinciale_viale`), poi la Provinciale lungo la Carza in secca con la luna (`verso_luna`; la Carza d'estate si + secca da quando hanno scavato le gallerie dell'alta velocità, d'inverno torna viva). In basso si va verso La Luna e + Campomigliaio (per ora solo battute). Da mettere: le case vecchie con la curva, il rettilineo col cartello della + rotonda, l'incrocio coi cartelli (`assets/raw/foto/luna-*.jpg`): ordine da chiedere ad Andrea. +- **Ricerca nella mappa**: casella «Cerca un posto…», filtra mentre si scrive (`matchPlace`), Invio porta al primo. - **Scelta del rione** (24 ottobre): dopo «Nuova partita», «Per chi corri quest'anno?» fra i quattro rioni (`story.rioni`: Piazza rossi, Pieve bianchi, Cardetole verdi, Campomigliaio blu); resta nella partita come flag `rione:`. Cambiano la riga dell'introduzione e le battute su Pieve, Cardetole, Campomigliaio e la colonna. diff --git a/index.html b/index.html index fff3257..a0902f3 100644 --- a/index.html +++ b/index.html @@ -52,7 +52,8 @@
diff --git a/src/engine/game.js b/src/engine/game.js index 2a37505..b8e26cc 100644 --- a/src/engine/game.js +++ b/src/engine/game.js @@ -50,6 +50,14 @@ export function shuffledCredits(credits, random = Math.random) { }); } +// La ricerca della mappa: ogni parola scritta deve stare nel nome del posto, senza badare a maiuscole, accenti e +// apostrofi («camp» trova Campomigliaio, «de medici» trova Via de' Medici). +const plain = text => text.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[^a-z0-9]+/g, ' ').trim(); +export function matchPlace(name, query) { + const words = plain(query).split(' ').filter(Boolean), hay = plain(name); + return words.every(w => hay.includes(w)); +} + export const sayTime = text => Math.max(1.6, 0.9 + text.length * 0.055); export function newState(story) { diff --git a/src/main.js b/src/main.js index c0dfd7a..54bf11b 100644 --- a/src/main.js +++ b/src/main.js @@ -1,6 +1,6 @@ // Avvio nel browser: collega motore, disegno e interfaccia HTML (verbi, inventario, dialoghi, fumetti). import { story } from './story/story.js'; -import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits } from './engine/game.js'; +import { Game, VERBS, VERB_LABELS, checkSave, introText, shuffledCredits, matchPlace } from './engine/game.js'; import { Command } from './engine/command.js'; import { render, loadAssets, hasBackground, WIDTH, HEIGHT } from './engine/render.js'; import { readSave, writeSave, clearSave, readAudio, writeAudio, saveFile, saveFileName, readSaveFile } from './engine/storage.js'; @@ -143,16 +143,41 @@ function openMap() { })); return row; })); + $('map-search').value = ''; + filterMap(); $('map').hidden = false; drawRoads(); + // Col mouse si può scrivere subito; sul telefono la tastiera si apre solo toccando la casella. + if (matchMedia('(pointer: fine)').matches) $('map-search').focus({ preventScroll: true }); // Sul telefono la mappa scorre: si parte col posto dove si è, al centro. board.querySelector('.here')?.scrollIntoView({ block: 'center', inline: 'center' }); } +// La ricerca: mentre si scrive restano solo i posti che corrispondono; Invio porta al primo. +function filterMap() { + const query = $('map-search').value; + let shown = 0; + for (const row of $('map-board').querySelectorAll('.map-band')) { + let inRow = 0; + for (const b of row.children) inRow += (b.hidden = !matchPlace(b.title, query)) ? 0 : 1; + row.hidden = !inRow; + shown += inRow; + } + $('map-none').hidden = shown > 0; + $('map-none').textContent = `Nessun posto con «${query.trim()}»`; + drawRoads(); +} +$('map-search').addEventListener('input', filterMap); +$('map-search').addEventListener('keydown', e => { + if (e.key !== 'Enter') return; + const first = [...$('map-board').querySelectorAll('button')].find(b => !b.hidden && !b.classList.contains('here')); + if (first) first.click(); +}); // Le strade fra le schede, misurate dopo l'impaginazione (e di nuovo se la finestra cambia misura). function drawRoads() { const board = $('map-board'), svg = $('map-roads'), origin = board.getBoundingClientRect(); const center = id => { - const r = board.querySelector(`[data-scene="${id}"] img`)?.getBoundingClientRect(); + const card = board.querySelector(`[data-scene="${id}"]`); + const r = card && !card.hidden && !card.parentElement.hidden && card.querySelector('img').getBoundingClientRect(); return r && [r.left + r.width / 2 - origin.left, r.top + r.height / 2 - origin.top]; }; svg.setAttribute('viewBox', `0 0 ${board.scrollWidth} ${board.scrollHeight}`); @@ -383,6 +408,11 @@ function updateChoices() { window.addEventListener('keydown', e => { if (e.target === $('volume') || e.target === $('voice-volume')) return; + // Nella ricerca della mappa le lettere si scrivono; Esc prima svuota la casella, poi chiude. + if (e.target === $('map-search')) { + if (e.key === 'Escape') { if (e.target.value) { e.target.value = ''; filterMap(); } else $('map').hidden = true; } + return; + } if (debugKey(e)) return; if (!$('credits').hidden) { if (e.key === 'Escape') $('credits').hidden = true; return; } if (!$('pause').hidden) { if (e.key === 'Escape') closePause(); return; } diff --git a/src/story/story.js b/src/story/story.js index 3556029..7462dbc 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -1032,7 +1032,7 @@ export const story = { id: 'alla_scuola', name: 'più avanti, la stradina della scuola', box: [170, 196, 80, 40], at: [208, 226], exit: { scene: 'scuola', at: [600, 320], face: 'left' }, }, { - id: 'la_luna', name: 'a sinistra, verso La Luna e Campomigliaio', box: [0, 216, 44, 56], at: [12, 248], exit: { scene: 'verso_luna', at: [150, 350], face: 'up' }, + id: 'la_luna', name: 'a sinistra, verso La Luna e Campomigliaio', box: [0, 216, 44, 56], at: [12, 248], exit: { scene: 'provinciale_viale', at: [300, 350], face: 'up' }, }, { id: 'al_forno', name: 'laggiù, verso il forno', box: [280, 196, 80, 40], at: [318, 224], exit: { scene: 'forno', at: [12, 300], face: 'right' }, @@ -1042,17 +1042,53 @@ export const story = { }, ], }, - // La Provinciale lungo la Sieve, uscendo dal paese verso La Luna (foto di Andrea, 24 ottobre). + // La Provinciale dopo il bivio della stazione, fra gli alberi, prima della Carza (foto di Andrea, 24 ottobre). + provinciale_viale: { + name: 'Via Provinciale, il viale alberato', + map: [72, 68], + background: 'assets/scenes/provinciale-viale.png', + walk: [[0, 360], [0, 330], [400, 232], [462, 232], [620, 360]], + depth: [[232, 0.4], [360, 1.7]], + entry: [300, 350], + hotspots: [ + { + id: 'verso_la_sieve', name: 'avanti, verso La Luna', box: [392, 220, 80, 22], at: [435, 236], exit: { scene: 'verso_luna', at: [150, 206], face: 'down' }, + }, + { + id: 'negozi', name: 'negozi', box: [20, 150, 200, 95], at: [150, 330], face: 'left', + actions: { + look: [{ say: 'I negozi sotto i palazzi, al riparo degli alberi. Con la festa domani, stasera chiudono tardi.' }], + }, + }, + { + id: 'alberi', name: 'alberi del viale', box: [230, 20, 160, 200], at: [330, 290], face: 'up', + actions: { + look: [{ say: 'Gli alberi del viale: d\'estate fanno un\'ombra che in piazza se la sognano.' }], + use: [{ say: 'Arrampicarmi? Ho ventisette anni, non sette.' }], + }, + }, + { + id: 'villette', name: 'case col balcone', box: [500, 0, 140, 230], at: [540, 330], face: 'right', + actions: { + look: [{ say: 'Case coi balconi pieni di fiori. Qui il palio si sente da lontano: il giusto.' }], + }, + }, + { + id: 'al_bivio', name: 'indietro, verso il bivio', box: [0, 334, 620, 26], at: [300, 354], exit: { scene: 'bivio_provinciale', at: [12, 248], face: 'right' }, + }, + ], + }, + // La Provinciale lungo la Carza, uscendo dal paese verso La Luna (foto di Andrea, 24 ottobre). verso_luna: { - name: 'La Provinciale lungo la Sieve', - map: [70, 70], + name: 'La Provinciale lungo la Carza', + map: [66, 73], background: 'assets/scenes/verso-luna.png', walk: [[0, 360], [0, 200], [168, 190], [182, 196], [330, 360]], depth: [[192, 0.45], [360, 1.7]], - entry: [150, 350], + entry: [150, 206], hotspots: [ { - id: 'verso_la_luna', name: 'avanti, verso La Luna e Campomigliaio', box: [0, 186, 172, 26], at: [110, 200], face: 'up', default: 'walk', + id: 'verso_la_luna', name: 'avanti, verso La Luna e Campomigliaio', box: [0, 334, 330, 26], at: [150, 352], face: 'down', default: 'walk', actions: { walk: [{ if: 'rione:campomigliaio', @@ -1063,9 +1099,9 @@ export const story = { }, }, { - id: 'sieve', name: 'la Sieve', box: [340, 205, 300, 155], at: [250, 300], face: 'right', + id: 'carza', name: 'la Carza', box: [340, 205, 300, 155], at: [250, 300], face: 'right', actions: { - look: [{ say: 'La Sieve. Più sassi che acqua: a farci il bagno ci si sbuccia le ginocchia.' }], + look: [{ say: 'La Carza, la stessa che passa sotto il ponticino per Cardetole. Più sassi che acqua.' }, { say: 'Da quando scavano le gallerie dell\'alta velocità, d\'estate sparisce. Torna viva d\'inverno.' }], use: [{ say: 'Scendere nel greto ora? Il palio non sta mica sotto un sasso.' }], }, }, @@ -1079,17 +1115,17 @@ export const story = { { id: 'casa_bianca', name: 'casa bianca', box: [84, 112, 80, 64], at: [120, 220], face: 'up', actions: { - look: [{ say: 'Una casa bianca sotto il bosco, coi pini sopra. Bella vista sulla Sieve, e sui camion della Provinciale.' }], + look: [{ say: 'Una casa bianca sotto il bosco, coi pini sopra. Bella vista sulla Carza, e sui camion della Provinciale.' }], }, }, { id: 'muretto', name: 'muretto', box: [178, 196, 120, 150], at: [190, 300], face: 'right', actions: { - look: [{ say: 'Il muretto lungo la Sieve, tutto licheni. È più vecchio di me, e sta in piedi meglio.' }], + look: [{ say: 'Il muretto lungo la Carza, tutto licheni. È più vecchio di me, e sta in piedi meglio.' }], }, }, { - id: 'al_bivio', name: 'indietro, verso il bivio', box: [0, 334, 330, 26], at: [150, 354], exit: { scene: 'bivio_provinciale', at: [12, 248], face: 'right' }, + id: 'al_viale', name: 'indietro, verso il paese', box: [0, 182, 240, 26], at: [150, 200], exit: { scene: 'provinciale_viale', at: [435, 238], face: 'down' }, }, ], }, diff --git a/style.css b/style.css index 5d78fd9..3080a11 100644 --- a/style.css +++ b/style.css @@ -145,7 +145,13 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins #pause .hint { color: var(--dim); max-width: 34em; text-align: center; } .load-error { margin: .6em 0 0; padding: .4em .8em; color: #fff; background: #7a1f1f; border-radius: 6px; font-size: max(9px, calc(var(--u) * 1.4)); } #map { z-index: 3; justify-content: flex-start; padding-top: 2.5%; background: rgba(15, 10, 20, .94); } -#map h2 { font-size: clamp(12.96px, 2.16vw, 23.04px); } +#map h2 { font-size: clamp(12.96px, 2.16vw, 23.04px); margin: 0; } +#map-head { display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: .4em 1.2em; width: 94%; margin: .3em 0; } +#map-search { font-size: clamp(8px, 1.1vw, 11px); padding: .5em .7em; width: 16em; max-width: 60%; border-radius: 6px; + border: 2px solid #5a4866; background: #231a29; color: var(--ink); } +#map-search:focus { outline: none; border-color: var(--accent); } +#map-none { margin: .6em 0 0; font-size: clamp(8px, 1.1vw, 11px); color: #b9a9c2; } +#map-board button[hidden], .map-band[hidden] { display: none; } /* La mappa a gradini: una fascia per livello del paese, schede con miniatura e nome; si scorre se non ci sta. */ #map-scroll { width: 94%; flex: 1; min-height: 0; margin: .3em 0; overflow: auto; -webkit-overflow-scrolling: touch; border-radius: 8px; background: radial-gradient(circle at 50% 60%, #3a2d42, #231a29); } diff --git a/tests/browser.mjs b/tests/browser.mjs index f5abe82..269cd8e 100644 --- a/tests/browser.mjs +++ b/tests/browser.mjs @@ -237,7 +237,18 @@ try { assert.equal(await page.locator('#map-board button[data-scene="pieve"]').count(), 0, 'la Pieve non è ancora vista'); assert.ok(await page.isVisible('#map-board button.here[data-scene="trebbio"]')); await page.screenshot({ path: `${OUT}/07b-mappa.png` }); - await page.click('#map-board button[data-scene="piazza"]'); + // La ricerca: scrivendo restano solo i posti giusti; una «p» nella casella non chiude la mappa; Invio ci va. + await page.fill('#map-search', 'xyz'); + assert.ok(await page.isVisible('#map-none'), 'nessun posto: lo dice'); + await page.fill('#map-search', ''); + await page.click('#map-search'); + await page.keyboard.type('piaz'); + assert.ok(await page.isVisible('#map'), 'scrivere «p» nella ricerca non chiude la mappa'); + assert.ok(await page.isHidden('#map-none')); + const shown = await page.locator('#map-board button:visible').evaluateAll(bs => bs.map(b => b.dataset.scene)); + assert.deepEqual(shown, ['piazza']); + await page.screenshot({ path: `${OUT}/07c-mappa-ricerca.png` }); + await page.keyboard.press('Enter'); assert.ok(await page.isHidden('#map')); assert.equal(await page.evaluate(() => game.state.scene), 'piazza'); diff --git a/tests/story.test.mjs b/tests/story.test.mjs index a52dbb5..46648c5 100644 --- a/tests/story.test.mjs +++ b/tests/story.test.mjs @@ -3,7 +3,7 @@ import assert from 'node:assert/strict'; import { backgroundOrder } from '../src/engine/render.js'; import { story } from '../src/story/story.js'; import { solution } from '../src/story/solution.js'; -import { Game, VERBS, checkSave, introText, shuffledCredits } from '../src/engine/game.js'; +import { Game, VERBS, checkSave, introText, shuffledCredits, matchPlace } from '../src/engine/game.js'; import { inside } from '../src/engine/geometry.js'; import { existsSync, readFileSync } from 'node:fs'; import { settle } from './helpers.mjs'; @@ -101,6 +101,17 @@ test('i crediti: i nomi di ogni voce escono in ordine casuale, sempre tutti', () assert.ok(orders.size > 3, `${idee}: l'ordine non cambia`); }); +test('la ricerca della mappa: pezzi di parola, senza maiuscole, accenti e apostrofi', () => { + assert.ok(matchPlace('Campomigliaio', 'camp')); + assert.ok(matchPlace('Via de\' Medici', 'de medici')); + assert.ok(matchPlace('Via de\' Medici', 'MEDICI via'), 'le parole in qualsiasi ordine'); + assert.ok(matchPlace('La Pieve di San Pietro', 'pieve ')); + assert.ok(matchPlace('Piazza', ''), 'casella vuota: tutti'); + assert.ok(!matchPlace('Piazza', 'pieve')); + const names = Object.values(story.scenes).filter(s => s.map).map(s => s.name); + assert.ok(names.filter(n => matchPlace(n, 'provinciale')).length >= 2, 'più posti sulla Provinciale'); +}); + test('la soluzione si percorre tutta dall\'inizio alla fine', () => { const saves = []; const g = new Game(story, { onSave: s => saves.push(s) });