Si comincia svegliandosi sulla panchina dei vecchietti dopo una notte brava: gli occhi che si aprono a fatica, la piazza sfocata e doppia; e dal Barone il telefonino col sito di San Piero, che si apre a tutto schermo, e il Muro del Sanpierino vero coi messaggi del 2007, «Aggiorna» compreso (Andrea)
- Il risveglio (src/engine/risveglio.js): primo piano sopra la scena, con le battute mentre le palpebre si aprono e i vecchietti che danno la notizia del palio; «Salta» lo salta. - Il telefonino del Barone (src/engine/telefono.js): sale dal basso come il libro, la home di sanpiero50037.it dalla Wayback Machine nello schermo, il mattone del muro apre il muro. - Il muro (assets/muro, tools/muro.py): la pagina vera rifatta statica dal muro riesumato, coi 24 messaggi del dump (senza email né IP) e quelli del gioco, che arrivano con la storia; «Scrivi» spento. - Il gioco sgancia i service worker di altre app rimasti sul suo indirizzo (la Bussola su localhost:8080). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GiECd9RWGMKV7n3K1mdy8T
This commit is contained in:
@@ -14,6 +14,8 @@ import * as micro from './micro.js';
|
||||
import * as cerbottana from './cerbottana.js';
|
||||
import { newCorsa, updateCorsa } from './corsa.js';
|
||||
import { newIncanto, updateIncanto } from './incanto.js';
|
||||
import { newRisveglio, updateRisveglio } from './risveglio.js';
|
||||
import { newTelefono, updateTelefono, tapTelefono } from './telefono.js';
|
||||
import * as condor from './condor.js';
|
||||
import * as campana from './campana.js';
|
||||
import * as poker from './poker.js';
|
||||
@@ -312,6 +314,8 @@ export class Game {
|
||||
this.queue.unshift(...(this.story.closeups[c.id].tap ?? []));
|
||||
return true;
|
||||
}
|
||||
// Il telefonino del Barone: un tocco apre il sito, e sul sito il link apre il muro.
|
||||
if (this.story.closeups[c.id].type === 'telefono') return tapTelefono(c, this.story.closeups[c.id]);
|
||||
if (this.story.closeups[c.id].type) return false; // torcia e minigiochi non si sfogliano
|
||||
if (c.step === 'cover') Object.assign(c, { step: 'title', t: 0 });
|
||||
else if (c.step === 'title') Object.assign(c, { step: 'turn', t: 0 });
|
||||
@@ -352,6 +356,14 @@ export class Game {
|
||||
this.queue.unshift(...(def.complete ?? []));
|
||||
return true;
|
||||
}
|
||||
// Il telefonino: chi ha visto il muro, chiudendo, lo commenta.
|
||||
if (def?.type === 'telefono') { if (c.step === 'wall') this.queue.unshift(...(def.closing ?? [])); return true; }
|
||||
// Il risveglio, saltato a metà (Esc), finisce comunque: le battute rimaste e poi quelle di dopo.
|
||||
if (def?.type === 'risveglio') {
|
||||
const left = (def.lines ?? []).slice(c.fired).map(({ at, ...e }) => e);
|
||||
this.queue.unshift(...left, ...(def.complete ?? []));
|
||||
return true;
|
||||
}
|
||||
// I minigiochi: a partita finita (risultato deciso, anche se il conteggio si sta ancora mostrando) la battuta
|
||||
// di chi ha vinto; uscendo prima, quella di chi lascia.
|
||||
if (MINIGAMES[def?.type]) {
|
||||
@@ -364,6 +376,13 @@ export class Game {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Il muro del telefonino (una pagina vera, assets/muro): «Home» torna alla home del sito.
|
||||
closeupHome() {
|
||||
const c = this.closeup;
|
||||
if (!c || this.story.closeups[c.id].type !== 'telefono' || c.step !== 'wall') return false;
|
||||
Object.assign(c, { step: 'site', t: 0 });
|
||||
return true;
|
||||
}
|
||||
// Bocce e biliardo: si tiene premuto per caricare la forza e si lascia per tirare (mouse, dito o spazio).
|
||||
minigame() { const c = this.closeup; return c ? MINIGAMES[this.story.closeups[c.id].type] ?? null : null; }
|
||||
closeupPress(pointerId) { const m = this.minigame(); return !!m && !this.speech && !!m.press?.(this.closeup, pointerId); }
|
||||
@@ -377,6 +396,8 @@ export class Game {
|
||||
const c = this.closeup;
|
||||
const m = this.minigame();
|
||||
if (m) return !!m.aim?.(c, [x, y]);
|
||||
// Sul sito del telefonino conta dove si tocca (il link del muro).
|
||||
if (c && this.story.closeups[c.id].type === 'telefono') { c.aim = [x, y]; return true; }
|
||||
if (!c || this.story.closeups[c.id].type !== 'torch' || !Number.isFinite(x) || !Number.isFinite(y)) return false;
|
||||
c.beam = [Math.max(70, Math.min(570, x)), Math.max(65, Math.min(295, y))];
|
||||
return true;
|
||||
@@ -527,6 +548,18 @@ export class Game {
|
||||
if (now === 'end') this.closeupClose();
|
||||
return;
|
||||
}
|
||||
if (this.story.closeups[c.id].type === 'telefono') {
|
||||
updateTelefono(c, dt);
|
||||
if (c.step === 'wall' && !c.lines && c.t >= CLOSEUP_PAUSE) c.lines = [...(this.story.closeups[c.id].comment ?? [])];
|
||||
if (c.lines?.length) this.run(c.lines.shift());
|
||||
return;
|
||||
}
|
||||
if (this.story.closeups[c.id].type === 'risveglio') {
|
||||
const { due, end } = updateRisveglio(c, this.story.closeups[c.id], dt);
|
||||
for (const e of due) this.run(e);
|
||||
if (end) this.closeupClose();
|
||||
return;
|
||||
}
|
||||
const m = this.minigame();
|
||||
if (m) {
|
||||
m.update(c, dt);
|
||||
@@ -571,6 +604,8 @@ export class Game {
|
||||
: type === 'viaggio' ? { id: e.closeup, t: 0, age: 0, events: [], typed: 0 }
|
||||
: type === 'corsa' ? newCorsa(e.closeup)
|
||||
: type === 'incanto' ? newIncanto(e.closeup)
|
||||
: type === 'risveglio' ? newRisveglio(e.closeup)
|
||||
: type === 'telefono' ? newTelefono(e.closeup)
|
||||
: type === 'poster' ? { id: e.closeup, step: 'poster', t: 0, age: 0 }
|
||||
// `level`: «Alla bona», «Seria» o «A fassi male», scelto nel dialogo con l'avversario.
|
||||
// `decks`: il mazzo da usare quando il suo flag è acceso (briscola: le napoletane al posto delle toscane).
|
||||
|
||||
+18
-2
@@ -17,6 +17,8 @@ import { drawCampana } from './campana-render.js';
|
||||
import { drawPoker } from './poker-render.js';
|
||||
import { drawViaggio } from './viaggio.js';
|
||||
import { drawIncanto } from './incanto.js';
|
||||
import { drawRisveglio } from './risveglio.js';
|
||||
import { drawTelefono, telefonoHint } from './telefono.js';
|
||||
import { drawBriscola } from './briscola-render.js';
|
||||
import { drawTressette } from './tressette-render.js';
|
||||
import { drawScopa } from './scopa-render.js';
|
||||
@@ -178,7 +180,9 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now(
|
||||
const andrea = sprites.get('andrea');
|
||||
const drawAndreaNow = () => andrea ? drawSprite(ctx, andrea, game.pose(), s.x, s.y, game.scaleAt(s.y), s.facing)
|
||||
: drawAndrea(ctx, s.x, s.y, game.scaleAt(s.y), s.facing, game.walking ? game.stride / 6 : null);
|
||||
const queue = [{ y: s.y, draw: drawAndreaNow }];
|
||||
// Durante il risveglio si guarda dagli occhi di Andrea: lui non si vede.
|
||||
const pov = game.closeup && game.story.closeups?.[game.closeup.id]?.type === 'risveglio';
|
||||
const queue = pov ? [] : [{ y: s.y, draw: drawAndreaNow }];
|
||||
for (const p of game.people()) {
|
||||
const sheet = sprites.get(p.who);
|
||||
if (sheet) queue.push({ y: p.at[1], draw: () => drawSprite(ctx, sheet, game.personPose(p), ...p.at, game.scaleAt(p.at[1]), p.facing ?? 1, p.size) });
|
||||
@@ -919,6 +923,15 @@ function drawCloseup(ctx, game, time) {
|
||||
}
|
||||
// L'incantesimo va sopra la scena (con Andrea dentro), già disegnata.
|
||||
if (def.type === 'incanto') { drawIncanto(ctx, c); return; }
|
||||
// Il risveglio, visto da dentro gli occhi: anche lui sopra la scena già disegnata.
|
||||
if (def.type === 'risveglio') { drawRisveglio(ctx, c); return; }
|
||||
// Il telefonino del Barone: sale come il libro, e il sito nello schermo si allarga a tutta la scena.
|
||||
if (def.type === 'telefono') {
|
||||
const pic = src => { if (!backgrounds.has(src)) loadBackground(src); return backgrounds.get(src); };
|
||||
drawTelefono(ctx, c, def, { phone: pic(def.phone), site: pic(def.site) }, time);
|
||||
closeupHint(ctx, game.speech ? null : telefonoHint(c), time);
|
||||
return;
|
||||
}
|
||||
// Il manifesto ingrandito: la scena si scurisce e lui viene avanti, poi l'invito a toccarlo.
|
||||
if (def.type === 'poster') {
|
||||
if (!backgrounds.has(def.picture)) loadBackground(def.picture);
|
||||
@@ -1043,7 +1056,10 @@ function drawCloseup(ctx, game, time) {
|
||||
ctx.restore();
|
||||
// Il suggerimento in alto, finché c'è qualcosa da sfogliare.
|
||||
const hint = c.step === 'cover' ? 'Clic o tocco: apri il libro' : c.step === 'title' ? 'Clic o tocco: gira la pagina' : null;
|
||||
if (hint && !game.speech && c.age > 0.5) {
|
||||
closeupHint(ctx, !game.speech && c.age > 0.5 ? hint : null, time);
|
||||
}
|
||||
function closeupHint(ctx, hint, time) {
|
||||
if (hint) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = 0.65 + 0.35 * Math.sin(time / 300);
|
||||
ctx.font = `7px ${FONT}`;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Il risveglio (story.closeups, type 'risveglio'; Andrea, 31 ottobre: «si ritrova aprendo piano piano gli occhi su
|
||||
// una panchina, come svegliato da una notte brava, con effetto annebbiato»). Sopra la scena di adesso, vista da
|
||||
// dentro gli occhi: le palpebre si aprono a fatica, ricascano, si riaprono; tutto è sfocato e doppio, e ondeggia.
|
||||
// Le battute (`lines`, ognuna col suo `at` in secondi) partono mentre gli occhi si aprono; mentre Andrea parla il
|
||||
// tempo del risveglio si ferma (la battuta va letta), l'ondeggiare no. Unità: i pixel della scena 640×360.
|
||||
|
||||
// Quanto sono aperti gli occhi (0 chiusi, 1 aperti) nel tempo: tre tentativi, l'ultimo è quello buono.
|
||||
const OPEN = [[0, 0], [1.4, 0], [2.1, 0.22], [2.5, 0.04], [2.9, 0.04], [3.8, 0.5], [4.15, 0.12], [4.45, 0.2], [5.6, 0.85], [5.85, 0.55], [6.9, 1]];
|
||||
export const END = 8.2;
|
||||
const clamp01 = k => Math.max(0, Math.min(1, k));
|
||||
const smooth = k => k * k * (3 - 2 * k);
|
||||
|
||||
export function openness(t) {
|
||||
for (let i = 1; i < OPEN.length; i++) {
|
||||
const [t1, o1] = OPEN[i];
|
||||
if (t <= t1) { const [t0, o0] = OPEN[i - 1]; return o0 + (o1 - o0) * smooth(clamp01((t - t0) / (t1 - t0))); }
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
// La nebbia: sfocatura e vista doppia, che si diradano fino a sparire alla fine.
|
||||
export const haze = t => 1 - smooth(clamp01((t - 1.4) / (END - 1.6)));
|
||||
|
||||
export function newRisveglio(id) { return { id, t: 0, age: 0, fired: 0 }; }
|
||||
|
||||
// Il tempo che passa: torna le battute da dire adesso, e 'end' alla fine.
|
||||
export function updateRisveglio(c, def, dt) {
|
||||
c.t += dt;
|
||||
const lines = def.lines ?? [], due = [];
|
||||
while (c.fired < lines.length && c.t >= lines[c.fired].at) { const { at, ...e } = lines[c.fired++]; due.push(e); }
|
||||
return { due, end: c.t >= END && c.fired >= lines.length };
|
||||
}
|
||||
|
||||
let scratch = null;
|
||||
export function drawRisveglio(ctx, c) {
|
||||
const t = c.t, h = haze(t), open = openness(t), W = 640, H = 360;
|
||||
ctx.save();
|
||||
// La scena annebbiata: sfocata, e una seconda copia spostata che ondeggia (la vista doppia).
|
||||
if (h > 0.01 && typeof document !== 'undefined') {
|
||||
if (!scratch) { scratch = document.createElement('canvas'); scratch.width = W; scratch.height = H; }
|
||||
const s = scratch.getContext('2d');
|
||||
s.clearRect(0, 0, W, H);
|
||||
s.drawImage(ctx.canvas, 0, 0, W, H);
|
||||
const sway = 14 * h, dx = Math.sin(c.age * 1.3) * sway, dy = Math.sin(c.age * 0.9 + 1) * sway * 0.35;
|
||||
ctx.filter = `blur(${(5 * h).toFixed(2)}px)`;
|
||||
ctx.drawImage(scratch, 0, 0);
|
||||
ctx.globalAlpha = 0.45 * h;
|
||||
ctx.drawImage(scratch, dx, dy);
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.filter = 'none';
|
||||
// La luce del mattino che abbaglia.
|
||||
ctx.fillStyle = `rgba(255,244,214,${0.22 * h})`;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
}
|
||||
// Gli angoli scuri, che si aprono con gli occhi.
|
||||
const v = ctx.createRadialGradient(W / 2, H / 2, 60 + 220 * open, W / 2, H / 2, 420);
|
||||
v.addColorStop(0, 'rgba(10,2,4,0)');
|
||||
v.addColorStop(1, `rgba(10,2,4,${0.85 * (1 - open * 0.75) * Math.max(h, 0.3)})`);
|
||||
ctx.fillStyle = v;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
// Le palpebre: sopra e sotto, col bordo curvo e morbido; chiuse, si vede il rosso scuro della luce che passa.
|
||||
if (open < 0.999) {
|
||||
const gap = 200 * open, mid = H / 2, bulge = 70 + 60 * open;
|
||||
ctx.filter = 'blur(6px)';
|
||||
ctx.fillStyle = 'rgb(22,4,6)';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-20, -20); ctx.lineTo(W + 20, -20); ctx.lineTo(W + 20, mid - gap + bulge);
|
||||
ctx.quadraticCurveTo(W / 2, mid - gap - bulge, -20, mid - gap + bulge);
|
||||
ctx.closePath(); ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(-20, H + 20); ctx.lineTo(W + 20, H + 20); ctx.lineTo(W + 20, mid + gap - bulge);
|
||||
ctx.quadraticCurveTo(W / 2, mid + gap + bulge, -20, mid + gap - bulge);
|
||||
ctx.closePath(); ctx.fill();
|
||||
ctx.filter = 'none';
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
// Il telefonino del Barone (story.closeups, type 'telefono'; Andrea, 31 ottobre: «prenda il cellulare, nello stile
|
||||
// di come si sfoglia la storia infinita, e quando si clicca viene a tutto schermo il sito di San Piero»).
|
||||
// Come il libro: la scena si scurisce e le mani alzano il telefonino dal basso; nello schermo c'è il sito
|
||||
// (`site`, un'immagine 16:9 della home di sanpiero50037.it). Un tocco: lo schermo si allarga a tutta la scena.
|
||||
// Sul sito si tocca il link (`link`, in pixel dell'immagine del sito) e si apre il muro: una pagina vera
|
||||
// (assets/muro, la mette main.js sopra la scena), che qui sotto lascia solo il fondo. A muro aperto partono le
|
||||
// battute `comment`, e chiudendo `closing`. Unità: i pixel della scena 640×360.
|
||||
|
||||
export const RISE = 0.45, ZOOM = 0.55;
|
||||
const W = 640, H = 360;
|
||||
const clamp01 = k => Math.max(0, Math.min(1, k));
|
||||
const ease = k => (1 - Math.cos(Math.PI * clamp01(k))) / 2;
|
||||
|
||||
export function newTelefono(id) { return { id, step: 'phone', t: 0, age: 0, aim: null }; }
|
||||
|
||||
// Il tempo: lo zoom finisce sul sito; a muro aperto, dopo un attimo, le battute (una alla volta, come il libro).
|
||||
export function updateTelefono(c, dt) {
|
||||
c.t += dt;
|
||||
if (c.step === 'zoom' && c.t >= ZOOM) Object.assign(c, { step: 'site', t: 0 });
|
||||
}
|
||||
|
||||
// Dove sta il link del muro sulla scena, a sito aperto: il riquadro `link` riportato da immagine a scena.
|
||||
export function linkBox(def, site) {
|
||||
const [x, y, w, h] = def.link, kx = W / site[0], ky = H / site[1];
|
||||
return [x * kx, y * ky, w * kx, h * ky];
|
||||
}
|
||||
const inside = ([x, y, w, h], [px, py]) => px >= x && px <= x + w && py >= y && py <= y + h;
|
||||
|
||||
// Un tocco: sul telefonino apre il sito; sul sito, solo sul link, apre il muro. Torna true se è successo qualcosa.
|
||||
export function tapTelefono(c, def) {
|
||||
if (c.step === 'phone') { if (c.age < RISE) return false; Object.assign(c, { step: 'zoom', t: 0 }); return true; }
|
||||
if (c.step === 'site' && c.aim && inside(linkBox(def, def.siteSize), c.aim)) { Object.assign(c, { step: 'wall', t: 0 }); return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
// Il riquadro dello schermo sulla scena, col telefonino che sale dal basso.
|
||||
function phoneRect(def, img, age) {
|
||||
const rise = 1 - clamp01(age / RISE), k = def.phoneScale ?? 1;
|
||||
const w = img.width * k, h = img.height * k, x = Math.round((W - w) / 2), y = Math.round(H - h + 140 * rise * rise);
|
||||
return { x, y, w, h, k };
|
||||
}
|
||||
|
||||
export function drawTelefono(ctx, c, def, { phone, site }, time) {
|
||||
ctx.save();
|
||||
ctx.fillStyle = `rgba(8, 5, 12, ${Math.min(0.78, c.age / 0.35 * 0.78)})`;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
if (!phone || !site) { ctx.restore(); return; }
|
||||
const p = phoneRect(def, phone, c.age), [sx, sy, sw, sh] = def.screen;
|
||||
const screen = [p.x + sx * p.k, p.y + sy * p.k, sw * p.k, sh * p.k];
|
||||
const z = c.step === 'phone' ? 0 : c.step === 'zoom' ? ease(c.t / ZOOM) : 1;
|
||||
// Il telefonino, finché lo schermo non ha preso tutta la scena; poi sfuma.
|
||||
if (z < 1) {
|
||||
ctx.globalAlpha = 1 - z;
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.drawImage(phone, p.x, p.y, p.w, p.h);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
// Il sito: dentro lo schermo, poi sempre più grande fino a tutta la scena. Lo schermo ha le sue proporzioni:
|
||||
// il sito ci sta intero, centrato, sul fondo del sito.
|
||||
const r = screen.map((v, i) => v + ([0, 0, W, H][i] - v) * z);
|
||||
const fit = Math.min(r[2] / site.width, r[3] / site.height), dw = site.width * fit, dh = site.height * fit;
|
||||
ctx.fillStyle = def.paper ?? '#f7e0c0';
|
||||
ctx.fillRect(r[0], r[1], r[2], r[3]);
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.drawImage(site, r[0] + (r[2] - dw) / 2, r[1] + (r[3] - dh) / 2, dw, dh);
|
||||
// Sotto il muro (la pagina vera) resta il fondo del sito.
|
||||
if (c.step === 'wall') { ctx.fillStyle = def.paper ?? '#f7e0c0'; ctx.fillRect(0, 0, W, H); }
|
||||
// Il link da toccare: un riquadro che pulsa intorno al mattone del muro.
|
||||
if (c.step === 'site') {
|
||||
const [x, y, w, h] = linkBox(def, def.siteSize), pulse = 0.5 + 0.5 * Math.sin(time / 260);
|
||||
ctx.strokeStyle = `rgba(255, 210, 90, ${0.5 + 0.5 * pulse})`;
|
||||
ctx.lineWidth = 2 + pulse;
|
||||
ctx.strokeRect(Math.round(x) - 2, Math.round(y) - 2, Math.round(w) + 4, Math.round(h) + 4);
|
||||
}
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Il suggerimento in alto, finché c'è qualcosa da toccare.
|
||||
export const telefonoHint = c => c.step === 'phone' && c.age > RISE ? 'Clic o tocco: guarda il sito'
|
||||
: c.step === 'site' ? 'Clic o tocco sul Muro del Sanpierino' : null;
|
||||
Reference in New Issue
Block a user