Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
de418c0da8 | ||
|
|
713e328138 | ||
|
|
6a5a80d7af | ||
|
|
88f0439f79 |
Binary file not shown.
|
After Width: | Height: | Size: 228 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1187,14 +1187,6 @@
|
|||||||
"file": "andrea-00f2e70a4d.mp3",
|
"file": "andrea-00f2e70a4d.mp3",
|
||||||
"sec": 2.95
|
"sec": 2.95
|
||||||
},
|
},
|
||||||
"andrea|La Ghiacciomobile nera. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.": {
|
|
||||||
"file": "andrea-7be29434e0.mp3",
|
|
||||||
"sec": 7.25
|
|
||||||
},
|
|
||||||
"andrea|Con questo sole, di ghiaccio c'è rimasto giusto il nome. Vo a piedi.": {
|
|
||||||
"file": "andrea-def44809bc.mp3",
|
|
||||||
"sec": 5.08
|
|
||||||
},
|
|
||||||
"andrea|Il circolino: il Circolo SMS di Campomigliaio. Un'istituzione, qui i volontari hanno sempre fatto miracoli.": {
|
"andrea|Il circolino: il Circolo SMS di Campomigliaio. Un'istituzione, qui i volontari hanno sempre fatto miracoli.": {
|
||||||
"file": "andrea-90bbad0de0.mp3",
|
"file": "andrea-90bbad0de0.mp3",
|
||||||
"sec": 9.59
|
"sec": 9.59
|
||||||
@@ -1627,6 +1619,14 @@
|
|||||||
"file": "andrea-7c17aa384c.mp3",
|
"file": "andrea-7c17aa384c.mp3",
|
||||||
"sec": 3.2
|
"sec": 3.2
|
||||||
},
|
},
|
||||||
|
"andrea|La Ghiacciomobile nera, mezza dentro il muro. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.": {
|
||||||
|
"file": "andrea-decbc1c807.mp3",
|
||||||
|
"sec": 9.49
|
||||||
|
},
|
||||||
|
"andrea|Per tirarla fuori di lì ci vuole altro che le mie braccia.": {
|
||||||
|
"file": "andrea-a193a43f3e.mp3",
|
||||||
|
"sec": 3.78
|
||||||
|
},
|
||||||
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
|
"andrea|Nessuno ha mai capito davvero il tuo immenso valore.": {
|
||||||
"file": "andrea-3e85200509.mp3",
|
"file": "andrea-3e85200509.mp3",
|
||||||
"sec": 2.93
|
"sec": 2.93
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ ne è un'opera derivata. Le foto scattate da Andrea non hanno problemi; quelle p
|
|||||||
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
|
| `provinciale-pignone.png`, `piazzetta-cambray-digny.png`, `via-cipriani.png`, `via-massorondinaio.png`, `mozzete.png`, `ponticino-mozzete.png`, `coldaia.png`, `falegnameria-coldaia.png`, `gabbiano.png` | Codex | screenshot di Google Street View | Google | **no**: da rifare con foto di Andrea |
|
||||||
| `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì |
|
| `closeups/micro-parcheggio.png`, `closeups/micro/*.png` (macchinine) | Codex, solo testo | nessuna | — | sì |
|
||||||
| `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) |
|
| `characters/chiaru.png` | Codex | `foto/chiaru.jpg` | Andrea (29 ottobre) | sì (il Chiaru consenziente) |
|
||||||
|
| `via-liguri-magelli-muro.png` | Codex, ritocco di `via-liguri-magelli.png` con la Ghiacciomobile nel muro | come `via-liguri-magelli.png` | Street View | **no**: come lo sfondo da cui viene |
|
||||||
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
|
| `cardetole.png` | Codex | `cardetole.jpg` | Andrea (EXIF) | **da decidere**: è la casa vera di Andrea, pubblicarla dice dove abita |
|
||||||
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
|
| `pieve-interno.png` | Codex | `pieve-interno.png` | Mariano Mirannalti (mandata ad Andrea su WhatsApp) | sì, se Mariano è d'accordo; citarlo nei crediti |
|
||||||
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
|
| `panorama-intro.png` | Codex | `altre/panorama-intro.png` | da internet (screenshot con l'icona di Google Lens) | **no**, finché non si sostituisce |
|
||||||
|
|||||||
@@ -18,6 +18,11 @@ Online su 50037.manzolo.it tutto fino agli effetti ambientali di Codex. Aperto,
|
|||||||
|
|
||||||
## Dove siamo arrivati
|
## Dove siamo arrivati
|
||||||
|
|
||||||
|
- **La Ghiacciomobile nel muro (29 ottobre)**: non è più al circolino di Campomigliaio. In Via Liguri Magelli, prima
|
||||||
|
del tramonto (`!indizio_regolo`), è schiantata dentro il muro giallo coi mattoni rotti e senza targa
|
||||||
|
(`via-liguri-magelli-muro.png`, ritocco di Codex); dal tramonto il muro è rifatto e c'è la targa. Il motore ha le
|
||||||
|
varianti di sfondo per scena (`backdrops`, con `when`; `game.backdrop()`). [DA ANDREA] lo zio Mario che guarda
|
||||||
|
come tirarla fuori, e la battuta dello schianto.
|
||||||
- **GP di Fortezza, guida semplice (29 ottobre)**: alla bona e Seria si tiene premuto per il gas e lasciando si
|
- **GP di Fortezza, guida semplice (29 ottobre)**: alla bona e Seria si tiene premuto per il gas e lasciando si
|
||||||
frena; lo sterzo segue la strada da solo (scansa le buche, sorpassa, prende la miscela), ma oltre la velocità che la
|
frena; lo sterzo segue la strada da solo (scansa le buche, sorpassa, prende la miscela), ma oltre la velocità che la
|
||||||
curva regge il motorino scivola fuori verso la paglia (`SIMPLE` in `gara.js`). Ginocchio in terra resta con lo
|
curva regge il motorino scivola fuori verso la paglia (`SIMPLE` in `gara.js`). Ginocchio in terra resta con lo
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "leggenda-del-regolo",
|
"name": "leggenda-del-regolo",
|
||||||
"version": "0.1.42",
|
"version": "0.1.44",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -162,6 +162,9 @@ export class Game {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get scene() { return this.story.scenes[this.state.scene]; }
|
get scene() { return this.story.scenes[this.state.scene]; }
|
||||||
|
// Lo sfondo di adesso: la prima variante della scena (`backdrops`, ognuna con la sua condizione `when`) che vale,
|
||||||
|
// sennò il suo `background`. Così un posto cambia con la storia (la Ghiacciomobile nel muro, poi la targa).
|
||||||
|
backdrop(scene = this.scene) { return scene.backdrops?.find(b => check(b.when, this.state))?.src ?? scene.background; }
|
||||||
// Occupato: c'è qualcosa in corso che il giocatore non può interrompere. La camminata verso un punto
|
// Occupato: c'è qualcosa in corso che il giocatore non può interrompere. La camminata verso un punto
|
||||||
// o verso un oggetto si può sempre cambiare con un altro clic: la coda che la seguiva viene scartata.
|
// o verso un oggetto si può sempre cambiare con un altro clic: la coda che la seguiva viene scartata.
|
||||||
get busy() { return !!this.speech || !!this.closeup || this.reach > 0 || (this.task ? !this.task.interruptible : this.queue.length > 0); }
|
get busy() { return !!this.speech || !!this.closeup || this.reach > 0 || (this.task ? !this.task.interruptible : this.queue.length > 0); }
|
||||||
|
|||||||
+3
-1
@@ -495,4 +495,6 @@ export function status(c) {
|
|||||||
if (simple(c)) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.clean !== undefined && c.t - c.clean < 1.5 ? 'Curva pulita!' : mustLift(c) && c.holding ? 'Lascia!' : mustLift(c, PREAVVISO) && c.holding ? 'Curva!' : c.holding ? 'Gas!' : 'Freno.'}`;
|
if (simple(c)) return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.clean !== undefined && c.t - c.clean < 1.5 ? 'Curva pulita!' : mustLift(c) && c.holding ? 'Lascia!' : mustLift(c, PREAVVISO) && c.holding ? 'Curva!' : c.holding ? 'Gas!' : 'Freno.'}`;
|
||||||
return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.braking ? 'Freno!' : c.holding ? 'Gas!' : 'Senza gas.'}`;
|
return `Giro ${lapOf(me)} di ${LAPS}, ${place}° posto. ${c.braking ? 'Freno!' : c.holding ? 'Gas!' : 'Senza gas.'}`;
|
||||||
}
|
}
|
||||||
export const button = c => c.step === 'intro' ? 'Avanti' : c.step === 'result' ? 'Finita' : c.holding ? 'Molla' : 'Gas';
|
// Nella guida semplice il pulsante si tiene premuto: la scritta resta Gas (su iPhone un pulsante che cambia sotto il dito
|
||||||
|
// perde il tocco).
|
||||||
|
export const button = c => c.step === 'intro' ? 'Avanti' : c.step === 'result' ? 'Finita' : simple(c) ? 'Gas' : c.holding ? 'Molla' : 'Gas';
|
||||||
|
|||||||
@@ -131,7 +131,7 @@ export function backgroundOrder(story, first) {
|
|||||||
for (const h of story.scenes[queue[i]]?.hotspots ?? [])
|
for (const h of story.scenes[queue[i]]?.hotspots ?? [])
|
||||||
if (h.exit && story.scenes[h.exit.scene] && !seen.has(h.exit.scene)) { seen.add(h.exit.scene); queue.push(h.exit.scene); }
|
if (h.exit && story.scenes[h.exit.scene] && !seen.has(h.exit.scene)) { seen.add(h.exit.scene); queue.push(h.exit.scene); }
|
||||||
for (const id of Object.keys(story.scenes)) if (!seen.has(id)) queue.push(id);
|
for (const id of Object.keys(story.scenes)) if (!seen.has(id)) queue.push(id);
|
||||||
return [...new Set(queue.map(id => story.scenes[id].background).filter(Boolean))];
|
return [...new Set(queue.flatMap(id => [story.scenes[id].background, ...(story.scenes[id].backdrops ?? []).map(b => b.src)]).filter(Boolean))];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Il personaggio alto PERSON pixel a grandezza 1 (per `size` 1), con i piedi in (x, y); rivolto a sinistra si specchia.
|
// Il personaggio alto PERSON pixel a grandezza 1 (per `size` 1), con i piedi in (x, y); rivolto a sinistra si specchia.
|
||||||
@@ -153,8 +153,8 @@ function drawSprite(ctx, { atlas, img }, frame, x, y, scale, facing, size = 1) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function render(ctx, game, { hover, spots, debug, time = performance.now() } = {}) {
|
export function render(ctx, game, { hover, spots, debug, time = performance.now() } = {}) {
|
||||||
const scene = game.scene, s = game.state, bg = backgrounds.get(scene.background);
|
const scene = game.scene, s = game.state, backdrop = game.backdrop(), bg = backgrounds.get(backdrop);
|
||||||
if (!bg && scene.background) loadBackground(scene.background);
|
if (!bg && backdrop) loadBackground(backdrop);
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
ctx.clearRect(0, 0, WIDTH, HEIGHT);
|
ctx.clearRect(0, 0, WIDTH, HEIGHT);
|
||||||
if (bg) ctx.drawImage(bg, 0, 0, WIDTH, HEIGHT);
|
if (bg) ctx.drawImage(bg, 0, 0, WIDTH, HEIGHT);
|
||||||
@@ -395,7 +395,7 @@ export const EFFECTS = {
|
|||||||
// Alberi al vento (i cipressi): il pezzo di sfondo si piega tutto insieme dalla base, di frazioni di pixel e con
|
// Alberi al vento (i cipressi): il pezzo di sfondo si piega tutto insieme dalla base, di frazioni di pixel e con
|
||||||
// due onde lente sovrapposte, così il movimento è morbido e non a scatti.
|
// due onde lente sovrapposte, così il movimento è morbido e non a scatti.
|
||||||
sway(ctx, e, time, game) {
|
sway(ctx, e, time, game) {
|
||||||
const bg = backgrounds.get(game.scene.background);
|
const bg = backgrounds.get(game.backdrop());
|
||||||
if (!bg) return;
|
if (!bg) return;
|
||||||
const k = bg.width / WIDTH, amount = e.amount ?? 3.2;
|
const k = bg.width / WIDTH, amount = e.amount ?? 3.2;
|
||||||
for (const [i, [x, y, w, h]] of e.rects.entries()) {
|
for (const [i, [x, y, w, h]] of e.rects.entries()) {
|
||||||
@@ -412,7 +412,7 @@ export const EFFECTS = {
|
|||||||
// ognuna a una sua altezza e velocità, uscita da un lato rientra dall'altro. Si vedono solo dove lo sfondo è
|
// ognuna a una sua altezza e velocità, uscita da un lato rientra dall'altro. Si vedono solo dove lo sfondo è
|
||||||
// cielo (skyMask): passano dietro alberi, torri e tetti. Le due grandi solo coi cieli larghi (`big`).
|
// cielo (skyMask): passano dietro alberi, torri e tetti. Le due grandi solo coi cieli larghi (`big`).
|
||||||
clouds(ctx, e, time, game) {
|
clouds(ctx, e, time, game) {
|
||||||
const bg = backgrounds.get(game.scene.background), mask = bg && skyMask(bg);
|
const bg = backgrounds.get(game.backdrop()), mask = bg && skyMask(bg);
|
||||||
if (!mask) return;
|
if (!mask) return;
|
||||||
const layer = scratch(), lc = layer.getContext('2d'), [ax, ay, aw, ah] = e.area;
|
const layer = scratch(), lc = layer.getContext('2d'), [ax, ay, aw, ah] = e.area;
|
||||||
const pool = e.big ? CLOUDS : CLOUDS.slice(2);
|
const pool = e.big ? CLOUDS : CLOUDS.slice(2);
|
||||||
@@ -452,7 +452,7 @@ export const EFFECTS = {
|
|||||||
// Una bandiera dipinta nello sfondo che sventola: a strisce verticali che salgono e scendono, ferme dalla parte
|
// Una bandiera dipinta nello sfondo che sventola: a strisce verticali che salgono e scendono, ferme dalla parte
|
||||||
// dell'asta (`pole`: 'left' o 'right') e sempre più mosse verso il lembo libero.
|
// dell'asta (`pole`: 'left' o 'right') e sempre più mosse verso il lembo libero.
|
||||||
cloth(ctx, e, time, game) {
|
cloth(ctx, e, time, game) {
|
||||||
const bg = backgrounds.get(game.scene.background);
|
const bg = backgrounds.get(game.backdrop());
|
||||||
if (!bg) return;
|
if (!bg) return;
|
||||||
const k = bg.width / WIDTH;
|
const k = bg.width / WIDTH;
|
||||||
for (const [i, [x, y, w, h]] of e.rects.entries()) {
|
for (const [i, [x, y, w, h]] of e.rects.entries()) {
|
||||||
|
|||||||
+17
-3
@@ -634,7 +634,17 @@ for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) $('gara-p
|
|||||||
padPointer = null; $('gara-pad').classList.remove('active');
|
padPointer = null; $('gara-pad').classList.remove('active');
|
||||||
});
|
});
|
||||||
$('gara-pad').addEventListener('contextmenu', e => e.preventDefault());
|
$('gara-pad').addEventListener('contextmenu', e => e.preventDefault());
|
||||||
$('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech) mini().shoot?.(c); });
|
// GP, guida semplice: il pulsante Gas si tiene premuto (giù gas, su freno) invece di accendersi e spegnersi al clic;
|
||||||
|
// sul telefono, alla bona, il clic arrivava solo alzando il dito (Andrea, 29 ottobre).
|
||||||
|
const holdGas = c => !!c && story.closeups[c.id].type === 'gara' && gara.simple(c) && c.step !== 'intro' && c.step !== 'result';
|
||||||
|
$('bocce-throw').addEventListener('click', () => { const c = miniState(); if (c && !game.speech && !holdGas(c)) mini().shoot?.(c); });
|
||||||
|
$('bocce-throw').addEventListener('pointerdown', e => {
|
||||||
|
const c = miniState(); if (!holdGas(c) || game.speech) return;
|
||||||
|
e.preventDefault();
|
||||||
|
if (gara.press(c, 'gas-button')) try { $('bocce-throw').setPointerCapture(e.pointerId); } catch { /* un tocco sintetico: va bene lo stesso */ }
|
||||||
|
});
|
||||||
|
for (const ev of ['pointerup', 'pointercancel', 'lostpointercapture']) $('bocce-throw').addEventListener(ev, () => { const c = miniState(); if (holdGas(c)) gara.release(c, 'gas-button'); });
|
||||||
|
$('bocce-throw').addEventListener('contextmenu', e => { if (holdGas(miniState())) e.preventDefault(); });
|
||||||
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
|
// Le carte da gioco: dove si tocca la carta sul tavolo, gioco per gioco.
|
||||||
const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt };
|
const CARD_AT = { briscola: cardAt, tressette: tressetteAt, scopa: scopaAt };
|
||||||
for (let i = 0; i < 3; i++) {
|
for (let i = 0; i < 3; i++) {
|
||||||
@@ -935,7 +945,10 @@ function updateUi() {
|
|||||||
updateSpeech();
|
updateSpeech();
|
||||||
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
|
const mg = miniState() && mini(), def = mg && story.closeups[c.id];
|
||||||
const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type);
|
const cards = ['briscola', 'tressette', 'scopa'].includes(def?.type);
|
||||||
setProp($('bocce-controls'), 'hidden', !mg || cards);
|
// La presentazione dei piloti del GP a tutto schermo: i comandi arrivano col semaforo (Andrea, 29 ottobre).
|
||||||
|
const presenta = !!mg && story.closeups[c.id].type === 'gara' && c.step === 'intro';
|
||||||
|
$('app').classList.toggle('presenta', presenta);
|
||||||
|
setProp($('bocce-controls'), 'hidden', !mg || cards || presenta);
|
||||||
setProp($('bocce-controls'), 'inert', !running);
|
setProp($('bocce-controls'), 'inert', !running);
|
||||||
setProp($('briscola-controls'), 'hidden', def?.type !== 'briscola');
|
setProp($('briscola-controls'), 'hidden', def?.type !== 'briscola');
|
||||||
setProp($('briscola-controls'), 'inert', !running);
|
setProp($('briscola-controls'), 'inert', !running);
|
||||||
@@ -1027,6 +1040,7 @@ function updateUi() {
|
|||||||
setText($('bocce-left'), def.type === 'gara' ? '← Sterza' : '← Mira');
|
setText($('bocce-left'), def.type === 'gara' ? '← Sterza' : '← Mira');
|
||||||
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : 'Mira →');
|
setText($('bocce-right'), def.type === 'gara' ? 'Sterza →' : 'Mira →');
|
||||||
setProp($('gara-brake'), 'hidden', def.type !== 'gara' || easy);
|
setProp($('gara-brake'), 'hidden', def.type !== 'gara' || easy);
|
||||||
|
$('bocce-throw').classList.toggle('hold', easy);
|
||||||
setProp($('gara-brake'), 'disabled', !(mg.canAdjust?.(c) ?? false) || !!game.speech);
|
setProp($('gara-brake'), 'disabled', !(mg.canAdjust?.(c) ?? false) || !!game.speech);
|
||||||
setText($('bocce-throw'), mg.button?.(c) ?? (c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira'));
|
setText($('bocce-throw'), mg.button?.(c) ?? (c.step === 'result' ? 'Finita' : c.step === 'rolling' ? 'In movimento…' : c.current === 'cpu' ? `Tira ${def.names.cpu}…` : def.type === 'bocce' ? 'Lancia' : 'Tira'));
|
||||||
setProp($('bocce-help'), 'hidden', def.type !== 'bocce');
|
setProp($('bocce-help'), 'hidden', def.type !== 'bocce');
|
||||||
@@ -1085,7 +1099,7 @@ function frame(now) {
|
|||||||
ambience.update(soundscapeFor(game?.state.scene, game?.closeup?.id), game?.phase()?.id, dt);
|
ambience.update(soundscapeFor(game?.state.scene, game?.closeup?.id), game?.phase()?.id, dt);
|
||||||
// A gioco fermo (titolo, pausa, fine) la scena non cambia: un ultimo disegno e poi niente, per la batteria del
|
// A gioco fermo (titolo, pausa, fine) la scena non cambia: un ultimo disegno e poi niente, per la batteria del
|
||||||
// telefono. Si ridisegna solo se nel frattempo arriva lo sfondo che mancava.
|
// telefono. Si ridisegna solo se nel frattempo arriva lo sfondo che mancava.
|
||||||
const still = game && !running && `${game.state.scene}|${hasBackground(game.scene.background)}`;
|
const still = game && !running && `${game.state.scene}|${hasBackground(game.backdrop())}`;
|
||||||
if (game && (!still || still !== drawnStill)) {
|
if (game && (!still || still !== drawnStill)) {
|
||||||
if (running) game.update(dt);
|
if (running) game.update(dt);
|
||||||
render(ctx, game, { hover: debug && hoverTarget ? game.hotspot(hoverTarget) : null, spots: running && now < spotsUntil, debug: debugView() });
|
render(ctx, game, { hover: debug && hoverTarget ? game.hotspot(hoverTarget) : null, spots: running && now < spotsUntil, debug: debugView() });
|
||||||
|
|||||||
+16
-15
@@ -2116,23 +2116,11 @@ export const story = {
|
|||||||
entry: [320, 330],
|
entry: [320, 330],
|
||||||
// L'Arcu davanti al circolino, accanto alla fontanella (sprite di Codex, 25 ottobre).
|
// L'Arcu davanti al circolino, accanto alla fontanella (sprite di Codex, 25 ottobre).
|
||||||
speakers: { arcu: [418, 214] },
|
speakers: { arcu: [418, 214] },
|
||||||
|
// La Ghiacciomobile non è più qui: sta nel muro di Via Liguri Magelli (Andrea, 29 ottobre).
|
||||||
people: [
|
people: [
|
||||||
{ who: 'arcu', at: [418, 292], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
{ who: 'arcu', at: [418, 292], facing: -1, idle: 'stand', talk: ['talk', 'explain'], blink: 'blink', every: 4.2 },
|
||||||
// Oggetti statici: ordinati per profondità come la macchina del tempo, senza voce.
|
|
||||||
// Con la depth della scena misura circa 160 px; resta sopra il bordo di walk. (Il Betino dell'Arcu sta in
|
|
||||||
// cima alla salita di Via Liguri Magelli, Andrea 28 ottobre.)
|
|
||||||
{ who: 'ghiacciomobile', at: [176, 286], size: 0.986, idle: 'idle', talk: [] },
|
|
||||||
],
|
],
|
||||||
hotspots: [
|
hotspots: [
|
||||||
{
|
|
||||||
id: 'ghiacciomobile', name: 'la Ghiacciomobile', box: [96, 215, 160, 71], at: [240, 308], face: 'up', default: 'look',
|
|
||||||
// La Corsa nera degli amici (sprite di Codex, 26 ottobre). Parlandole, Andrea ricorda; lei non risponde.
|
|
||||||
actions: {
|
|
||||||
look: [{ say: 'La Ghiacciomobile nera. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.' }],
|
|
||||||
talk: [{ dialogue: 'ghiacciomobile' }],
|
|
||||||
use: [{ say: 'Con questo sole, di ghiaccio c\'è rimasto giusto il nome. Vo a piedi.' }],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'circolo', name: 'il circolino', box: [100, 0, 380, 205], at: [300, 290], face: 'up',
|
id: 'circolo', name: 'il circolino', box: [100, 0, 380, 205], at: [300, 290], face: 'up',
|
||||||
actions: {
|
actions: {
|
||||||
@@ -2710,6 +2698,9 @@ export const story = {
|
|||||||
name: 'Via Liguri Magelli',
|
name: 'Via Liguri Magelli',
|
||||||
map: [76, 97],
|
map: [76, 97],
|
||||||
background: 'assets/scenes/via-liguri-magelli.png',
|
background: 'assets/scenes/via-liguri-magelli.png',
|
||||||
|
// All'inizio la Ghiacciomobile è dentro il muro giallo, coi mattoni rotti (Codex, 29 ottobre); dal tramonto
|
||||||
|
// (le impronte del Regolo) il muro è rifatto e c'è la sua targa (Andrea, 29 ottobre).
|
||||||
|
backdrops: [{ when: '!indizio_regolo', src: 'assets/scenes/via-liguri-magelli-muro.png' }],
|
||||||
// La strada gira intorno all'Audi nera parcheggiata davanti al Trappola.
|
// La strada gira intorno all'Audi nera parcheggiata davanti al Trappola.
|
||||||
walk: [[0, 360], [0, 320], [140, 262], [226, 198], [272, 198], [300, 242], [370, 246], [400, 252], [580, 318], [640, 330], [640, 360]],
|
walk: [[0, 360], [0, 320], [140, 262], [226, 198], [272, 198], [300, 242], [370, 246], [400, 252], [580, 318], [640, 330], [640, 360]],
|
||||||
depth: [[198, 0.3], [360, 1.1]],
|
depth: [[198, 0.3], [360, 1.1]],
|
||||||
@@ -2745,11 +2736,21 @@ export const story = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'targa', name: 'targa della Ghiacciomobile', box: [570, 130, 62, 78], at: [565, 336], face: 'right', default: 'look',
|
id: 'targa', name: 'targa della Ghiacciomobile', box: [570, 130, 62, 78], at: [565, 336], face: 'right', default: 'look', when: 'indizio_regolo',
|
||||||
actions: {
|
actions: {
|
||||||
look: [{ say: '«Qui riposa la Ghiacciomobile, che si è schiantata lì.»' }, { say: 'C\'è anche la fotina. Pareva sempre nuova.' }],
|
look: [{ say: '«Qui riposa la Ghiacciomobile, che si è schiantata lì.»' }, { say: 'C\'è anche la fotina. Pareva sempre nuova.' }],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// La Ghiacciomobile nel muro (prima del tramonto). Le battute erano del circolino (Andrea, 26 ottobre);
|
||||||
|
// [DA ANDREA] quella dello schianto. Parlandole, Andrea ricorda; lei non risponde.
|
||||||
|
id: 'ghiacciomobile', name: 'la Ghiacciomobile', box: [462, 192, 140, 92], at: [440, 300], face: 'right', default: 'look', when: '!indizio_regolo',
|
||||||
|
actions: {
|
||||||
|
look: [{ say: 'La Ghiacciomobile nera, mezza dentro il muro. Hai visto cose che noi poveri umani non potremmo neppur lontanamente immaginare.' }],
|
||||||
|
talk: [{ dialogue: 'ghiacciomobile' }],
|
||||||
|
use: [{ say: 'Per tirarla fuori di lì ci vuole altro che le mie braccia.' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
// La prima battuta è di Andrea (dal circolino, 26 ottobre).
|
// La prima battuta è di Andrea (dal circolino, 26 ottobre).
|
||||||
id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look',
|
id: 'betino', name: 'il Betino dell\'Arcu', box: [232, 172, 40, 24], at: [250, 206], face: 'up', default: 'look',
|
||||||
@@ -2764,7 +2765,7 @@ export const story = {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
// Il campo del Giannini è laggiù a destra, oltre la siepe (Andrea, 28 ottobre).
|
// Il campo del Giannini è laggiù a destra, oltre la siepe (Andrea, 28 ottobre).
|
||||||
id: 'al_giannini', name: 'verso il campo del Giannini', box: [430, 150, 132, 100], at: [520, 306], exit: { scene: 'campo_giannini', at: [60, 250], face: 'right' },
|
id: 'al_giannini', name: 'verso il campo del Giannini', box: [404, 150, 58, 100], at: [520, 306], exit: { scene: 'campo_giannini', at: [60, 250], face: 'right' },
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
// La versione del gioco: la cambia scripts/bump.sh, insieme a package.json.
|
||||||
export const VERSION = '0.1.42';
|
export const VERSION = '0.1.44';
|
||||||
|
|
||||||
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
// Immagini, musiche e voci con ?v=<versione>: a ogni versione l'indirizzo cambia e il browser le riscarica,
|
||||||
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
// mentre fra una versione e l'altra le tiene in cache a lungo (nginx.conf).
|
||||||
|
|||||||
@@ -56,7 +56,11 @@ body { display: flex; justify-content: center; overflow-x: hidden; -webkit-tap-h
|
|||||||
text-align: center; color: #c5c6b6; border: 2px dashed #897b57; border-radius: 10px; background: #1d2320; }
|
text-align: center; color: #c5c6b6; border: 2px dashed #897b57; border-radius: 10px; background: #1d2320; }
|
||||||
#gara-pad.active { border-style: solid; color: #f3dcac; }
|
#gara-pad.active { border-style: solid; color: #f3dcac; }
|
||||||
#stage, #bocce-controls, #gara-pad { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
|
#stage, #bocce-controls, #gara-pad { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
|
||||||
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; }
|
#bocce-throw { background: #ead09a; color: #202b25; min-width: 100px; touch-action: none; }
|
||||||
|
/* La presentazione dei piloti del GP: la scena da sola, i comandi arrivano col semaforo. */
|
||||||
|
#app.presenta #side { display: none; }
|
||||||
|
/* La guida semplice del GP: il Gas si tiene premuto col pollice, più alto. */
|
||||||
|
#bocce-throw.hold { min-height: 72px; font-size: 14px; }
|
||||||
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
.bocce-adjust button:disabled { opacity: .4; cursor: default; }
|
||||||
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
#bocce-help, #biliardo-help, #basket-help, #pesca-help, #gara-help, #rinquarto-help { margin-top: 9px; color: #c5c6b6; font-size: 10px; }
|
||||||
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary { cursor: pointer; padding: 6px 0; }
|
#bocce-help summary, #biliardo-help summary, #basket-help summary, #pesca-help summary, #gara-help summary, #rinquarto-help summary { cursor: pointer; padding: 6px 0; }
|
||||||
@@ -318,6 +322,8 @@ body.full #sound { position: fixed; z-index: 13; top: max(8px, env(safe-area-ins
|
|||||||
#app { width: 100vw; flex-direction: row; }
|
#app { width: 100vw; flex-direction: row; }
|
||||||
#stage { flex: none; width: min(calc(100dvh * 16 / 9), 68vw); align-self: flex-start; }
|
#stage { flex: none; width: min(calc(100dvh * 16 / 9), 68vw); align-self: flex-start; }
|
||||||
#stage.biliardo { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
#stage.biliardo { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
||||||
|
#app.presenta { justify-content: center; }
|
||||||
|
#app.presenta #stage.biliardo { width: min(calc((100dvh - 44px) * 16 / 9), 100vw); }
|
||||||
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
#stage.briscola { width: min(calc((100dvh - 90px) * 16 / 9), 68vw); }
|
||||||
#briscola-controls, #tressette-controls, #scopa-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
|
#briscola-controls, #tressette-controls, #scopa-controls { overflow-y: auto; min-height: 0; padding: 8px; font-size: 9px; }
|
||||||
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
|
/* Nella colonna stretta accanto al tavolo: carte e segni su due colonne, parole che vanno a capo. */
|
||||||
|
|||||||
+15
-4
@@ -29,19 +29,30 @@ try {
|
|||||||
const { settle } = await import('/tests/helpers.mjs');
|
const { settle } = await import('/tests/helpers.mjs');
|
||||||
settle(game); game.run({ closeup: 'gara', level }); settle(game);
|
settle(game); game.run({ closeup: 'gara', level }); settle(game);
|
||||||
}, level);
|
}, level);
|
||||||
await page.waitForSelector('#bocce-controls:not([hidden])');
|
// La presentazione dei piloti a tutto schermo: niente comandi finché non c'è il semaforo.
|
||||||
|
await page.waitForFunction(() => game.closeup?.step === 'intro');
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
assert.equal(await page.locator('#bocce-controls').isVisible(), false, `${name}: comandi nascosti nella presentazione`);
|
||||||
|
assert.equal(await page.locator('#side').isVisible(), false, `${name}: la scena da sola`);
|
||||||
|
await page.screenshot({ path: `${out}/${name}-presentazione.png` });
|
||||||
// Salta la presentazione dei piloti e aspetta il via.
|
// Salta la presentazione dei piloti e aspetta il via.
|
||||||
await page.evaluate(async () => { const g = await import('/src/engine/gara.js'); while (game.closeup.step === 'intro') g.press(game.closeup); });
|
await page.evaluate(async () => { const g = await import('/src/engine/gara.js'); while (game.closeup.step === 'intro') g.press(game.closeup); });
|
||||||
await page.waitForFunction(() => game.closeup.step === 'race', null, { timeout: 15000 });
|
await page.waitForFunction(() => game.closeup.step === 'race', null, { timeout: 15000 });
|
||||||
|
await page.waitForSelector('#bocce-controls:not([hidden])');
|
||||||
const easy = level !== 'male';
|
const easy = level !== 'male';
|
||||||
for (const id of ['#bocce-left', '#bocce-right', '#gara-brake', '#gara-pad'])
|
for (const id of ['#bocce-left', '#bocce-right', '#gara-brake', '#gara-pad'])
|
||||||
assert.equal(await page.locator(id).isVisible(), !easy, `${name}: ${id} ${easy ? 'nascosto' : 'visibile'}`);
|
assert.equal(await page.locator(id).isVisible(), !easy, `${name}: ${id} ${easy ? 'nascosto' : 'visibile'}`);
|
||||||
assert.equal(await page.locator('#bocce-power').isVisible(), !easy, 'la sensibilità solo con lo sterzo vero');
|
assert.equal(await page.locator('#bocce-power').isVisible(), !easy, 'la sensibilità solo con lo sterzo vero');
|
||||||
// Il pulsante Gas accende il gas; un altro tocco lo spegne e nella guida semplice si frena.
|
// Nella guida semplice il pulsante Gas si tiene premuto (giù gas, su freno) e la scritta non cambia; con lo
|
||||||
if (mobile) await page.locator('#bocce-throw').tap(); else await page.locator('#bocce-throw').click();
|
// sterzo vero un tocco accende il gas e un altro lo spegne.
|
||||||
|
const gas = page.locator('#bocce-throw');
|
||||||
|
if (easy) await gas.dispatchEvent('pointerdown', { pointerId: 5, pointerType: mobile ? 'touch' : 'mouse', isPrimary: true });
|
||||||
|
else await gas.click();
|
||||||
await page.waitForFunction(() => game.closeup.riders.find(r => r.id === 'andrea').v > 40, null, { timeout: 15000 });
|
await page.waitForFunction(() => game.closeup.riders.find(r => r.id === 'andrea').v > 40, null, { timeout: 15000 });
|
||||||
|
assert.equal(await gas.textContent(), easy ? 'Gas' : 'Molla');
|
||||||
await page.screenshot({ path: `${out}/${name}-${level}.png` });
|
await page.screenshot({ path: `${out}/${name}-${level}.png` });
|
||||||
if (mobile) await page.locator('#bocce-throw').tap(); else await page.locator('#bocce-throw').click();
|
if (easy) await gas.dispatchEvent('pointerup', { pointerId: 5, pointerType: mobile ? 'touch' : 'mouse', isPrimary: true });
|
||||||
|
else await gas.click();
|
||||||
const v = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
|
const v = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
|
||||||
await page.waitForTimeout(300);
|
await page.waitForTimeout(300);
|
||||||
const after = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
|
const after = await page.evaluate(() => game.closeup.riders.find(r => r.id === 'andrea').v);
|
||||||
|
|||||||
@@ -437,7 +437,7 @@ test('interrompere la catena del ghiacciolo non perde la torcia né la chiavetta
|
|||||||
|
|
||||||
test('gli sfondi si caricano dalla scena di partenza verso le più lontane, tutti e una volta sola', () => {
|
test('gli sfondi si caricano dalla scena di partenza verso le più lontane, tutti e una volta sola', () => {
|
||||||
const order = backgroundOrder(story, story.start.scene);
|
const order = backgroundOrder(story, story.start.scene);
|
||||||
const all = new Set(Object.values(story.scenes).map(s => s.background).filter(Boolean));
|
const all = new Set(Object.values(story.scenes).flatMap(s => [s.background, ...(s.backdrops ?? []).map(b => b.src)]).filter(Boolean));
|
||||||
assert.equal(order.length, all.size);
|
assert.equal(order.length, all.size);
|
||||||
assert.deepEqual(new Set(order), all);
|
assert.deepEqual(new Set(order), all);
|
||||||
assert.equal(order[0], story.scenes[story.start.scene].background, 'prima la piazza');
|
assert.equal(order[0], story.scenes[story.start.scene].background, 'prima la piazza');
|
||||||
|
|||||||
+16
-3
@@ -6,9 +6,10 @@ import { Game } from '../src/engine/game.js';
|
|||||||
import { inside } from '../src/engine/geometry.js';
|
import { inside } from '../src/engine/geometry.js';
|
||||||
import { settle } from './helpers.mjs';
|
import { settle } from './helpers.mjs';
|
||||||
|
|
||||||
// La Ghiacciomobile al circolino è uno sprite; il Betino in cima alla salita di Via Liguri Magelli è dipinto nel quadro.
|
// I veicoli sono dipinti nel quadro: il Betino in cima alla salita di Via Liguri Magelli e, prima del tramonto, la
|
||||||
const SPRITES = [['campomigliaio_circolo', 'ghiacciomobile', [150, 170]]];
|
// Ghiacciomobile dentro il muro giallo (Andrea, 29 ottobre). Nessuno è più uno sprite.
|
||||||
const VEHICLES = [...SPRITES, ['via_liguri_magelli', 'betino']];
|
const SPRITES = [];
|
||||||
|
const VEHICLES = [['via_liguri_magelli', 'betino'], ['via_liguri_magelli', 'ghiacciomobile']];
|
||||||
test('i veicoli restano fuori dal percorso e lasciano liberi gli altri hotspot', () => {
|
test('i veicoli restano fuori dal percorso e lasciano liberi gli altri hotspot', () => {
|
||||||
for (const [scene, id, [lo, hi]] of SPRITES) {
|
for (const [scene, id, [lo, hi]] of SPRITES) {
|
||||||
const g = new Game(story);
|
const g = new Game(story);
|
||||||
@@ -62,3 +63,15 @@ test('guarda e usa i veicoli: parla solo Andrea e non cambia il progresso', () =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('Via Liguri Magelli: prima la Ghiacciomobile nel muro (senza targa), dal tramonto il muro rifatto con la targa', () => {
|
||||||
|
const g = new Game(story);
|
||||||
|
g.go('via_liguri_magelli'); settle(g);
|
||||||
|
assert.equal(g.backdrop(), 'assets/scenes/via-liguri-magelli-muro.png');
|
||||||
|
assert.ok(g.hotspot('ghiacciomobile') && !g.hotspot('targa'));
|
||||||
|
assert.ok(!story.scenes.campomigliaio_circolo.hotspots.some(h => h.id === 'ghiacciomobile'), 'al circolino non c\'è più');
|
||||||
|
g.state.flags.indizio_regolo = true;
|
||||||
|
assert.equal(g.backdrop(), 'assets/scenes/via-liguri-magelli.png');
|
||||||
|
assert.ok(g.hotspot('targa') && !g.hotspot('ghiacciomobile'));
|
||||||
|
assert.ok(existsSync(new URL('../assets/scenes/via-liguri-magelli-muro.png', import.meta.url)));
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user