Il libro dell'oratorio in primo piano: copertina, frontespizio, una pagina che gira col fruscio e «FINE» (immagini di Codex, testi disegnati dal codice); l'Arcu a scelte (Borgo e la Casa dell'Armando); il cancello dell'oratorio e l'A. Bettini

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ks2K99uqCZjAxvy7Rdj1R
This commit is contained in:
2026-10-25 14:06:12 +01:00
co-authored by Claude Opus 5.5
parent 9d9dc81bcb
commit f6fd2fdfdc
31 changed files with 359 additions and 53 deletions
+40 -2
View File
@@ -22,6 +22,9 @@ const SPEED = 95;
// Solo profilo, come i cicli di camminata: 'up' e 'down' lasciano il verso com'è.
const FACING = { left: -1, right: 1 };
const WALK = ['walk_1', 'walk_2', 'walk_3', 'walk_4'], STEP = 15, REACH = 0.45;
// Il primo piano di un libro (story.closeups, effetto `closeup`): copertina → frontespizio → una pagina girata →
// l'ultima pagina, poi dopo una pausa il commento. Tempi in secondi.
export const PAGE_TURN = 0.7, CLOSEUP_PAUSE = 1.4;
// Condizioni: 'flag', '!flag', 'has:oggetto', '!has:oggetto', oppure un elenco (tutte vere).
export function check(cond, state) {
@@ -115,15 +118,16 @@ export class Game {
this.hurry = false; // doppio clic: si cammina al doppio della velocità
this.reach = 0; // secondi di braccio teso quando prende qualcosa
this.back = false; // di spalle davanti a una porta o a una bacheca
this.closeup = null; // il primo piano aperto: { id, step: 'cover' | 'title' | 'turn' | 'end', t, age }
}
get scene() { return this.story.scenes[this.state.scene]; }
// 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.
get busy() { return !!this.speech || 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); }
get idle() { return !this.busy && !this.task && !this.dialogue; }
// Fermo: nessuna battuta, nessun effetto in coda, nessuna camminata che fa parte di una sequenza.
get steady() { return !this.speech && this.reach <= 0 && !this.queue.length && !(this.task && !this.task.interruptible); }
get steady() { return !this.speech && !this.closeup && this.reach <= 0 && !this.queue.length && !(this.task && !this.task.interruptible); }
// Lo stato da salvare adesso: quello attuale se è fermo, altrimenti l'ultimo punto fermo.
savable() { return this.steady ? this.snapshot() : this.checkpoint; }
// A sequenza finita, se qualcosa è cambiato, nuovo punto fermo e salvataggio.
@@ -221,6 +225,27 @@ export class Game {
return false;
}
// Il primo piano: un tocco sfoglia (copertina → frontespizio → si gira la pagina); all'ultima pagina non fa niente.
closeupTap() {
const c = this.closeup;
if (!c || this.speech) return false;
if (c.step === 'cover') Object.assign(c, { step: 'title', t: 0 });
else if (c.step === 'title') Object.assign(c, { step: 'turn', t: 0 });
else return false;
return true;
}
// Chiuso il libro si torna alla scena, e gli effetti che seguivano il primo piano ripartono.
closeupClose() {
const c = this.closeup;
if (!c) return false;
this.closeup = null;
this.speech = null;
// Chi è arrivato all'ultima pagina, chiudendo, la commenta (`closing`).
if (c.step === 'end') this.queue.unshift(...(this.story.closeups?.[c.id]?.closing ?? []));
return true;
}
options() {
if (!this.dialogue || this.busy) return [];
const node = this.story.dialogues[this.dialogue.id].nodes[this.dialogue.node];
@@ -271,6 +296,8 @@ export class Game {
}
speakerPoint(who) {
// Nel primo piano Andrea parla da sopra il libro, non dalla scena oscurata dietro.
if (this.closeup && (!who || who === 'andrea')) return [320, 34];
if (!who || who === 'andrea') return [this.state.x, this.state.y - 62 * this.scaleAt(this.state.y)];
if (this.scene.speakers?.[who]) return this.scene.speakers[who];
const h = this.hotspot(who) ?? (this.scene.hotspots ?? []).find(x => x.id === who);
@@ -280,6 +307,7 @@ export class Game {
update(dt) {
this.time += dt;
if (this.closeup) this.closeup.age += dt;
if (this.speech) {
this.speech.left -= dt;
if (this.speech.left > 0) return;
@@ -310,6 +338,15 @@ export class Game {
this.walking = false;
this.task = null;
}
// Nel primo piano il tempo scorre solo per la pagina che gira e per la pausa prima del commento.
const c = this.closeup;
if (c) {
c.t += dt;
if (c.step === 'turn' && c.t >= PAGE_TURN) Object.assign(c, { step: 'end', t: 0 });
if (c.step === 'end' && !c.lines && c.t >= CLOSEUP_PAUSE) c.lines = [...(this.story.closeups?.[c.id]?.comment ?? [])];
if (c.lines?.length) this.run(c.lines.shift());
return;
}
// Esegue gli effetti istantanei di fila e si ferma al primo che richiede tempo.
while (!this.speech && !this.walking && this.queue.length) this.run(this.queue.shift());
this.commit();
@@ -335,6 +372,7 @@ export class Game {
else if (e.node) this.dialogue.node = e.node;
else if (e.endDialogue) this.dialogue = null;
else if (e.end) s.ended = true;
else if (e.closeup) this.closeup = { id: e.closeup, step: 'cover', t: 0, age: 0 };
else throw new Error('Effetto sconosciuto: ' + JSON.stringify(e));
}