Lucciole, finestre che si accendono di sera, rondini, cipressi al vento e dissolvenza fra le scene
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
@@ -374,6 +374,7 @@ export class Game {
|
|||||||
go(scene, at, face) {
|
go(scene, at, face) {
|
||||||
if (!this.story.scenes[scene]) throw new Error('Scena sconosciuta: ' + scene);
|
if (!this.story.scenes[scene]) throw new Error('Scena sconosciuta: ' + scene);
|
||||||
this.state.scene = scene;
|
this.state.scene = scene;
|
||||||
|
this.enteredAt = this.time; // per gli effetti che cominciano all'ingresso (le finestre che si accendono)
|
||||||
[this.state.x, this.state.y] = at ?? this.scene.entry;
|
[this.state.x, this.state.y] = at ?? this.scene.entry;
|
||||||
this.state.facing = FACING[face] ?? this.state.facing;
|
this.state.facing = FACING[face] ?? this.state.facing;
|
||||||
this.path = [];
|
this.path = [];
|
||||||
|
|||||||
+58
-2
@@ -145,6 +145,8 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now(
|
|||||||
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);
|
||||||
|
// Gli effetti che muovono pezzi di sfondo (i cipressi al vento) vanno subito sopra lo sfondo, sotto tutto il resto.
|
||||||
|
drawEffects(ctx, game, time, true);
|
||||||
for (const shape of scene.art ?? []) drawShape(ctx, shape);
|
for (const shape of scene.art ?? []) drawShape(ctx, shape);
|
||||||
// Una forma può avere la sua condizione (`when`): la chiavetta nella griglia sparisce quando la si prende.
|
// Una forma può avere la sua condizione (`when`): la chiavetta nella griglia sparisce quando la si prende.
|
||||||
// I primi piani (`top`) invece vanno sopra a tutto, anche al buio e alle candele: si disegnano dopo.
|
// I primi piani (`top`) invece vanno sopra a tutto, anche al buio e alle candele: si disegnano dopo.
|
||||||
@@ -290,6 +292,58 @@ export const EFFECTS = {
|
|||||||
ctx.fillStyle = e.fill;
|
ctx.fillStyle = e.fill;
|
||||||
for (const [x, y, w, h] of e.rects) closed ? ctx.fillRect(x, y + Math.floor(h / 2), w, 1) : ctx.fillRect(x, y, w, h);
|
for (const [x, y, w, h] of e.rects) closed ? ctx.fillRect(x, y + Math.floor(h / 2), w, 1) : ctx.fillRect(x, y, w, h);
|
||||||
},
|
},
|
||||||
|
// Lucciole di sera: puntini gialli che vagano piano e si accendono e si spengono.
|
||||||
|
fireflies(ctx, e, time) {
|
||||||
|
const [ax, ay, aw, ah] = e.area;
|
||||||
|
for (let i = 0; i < (e.count ?? 8); i++) {
|
||||||
|
const t = time / 1000, glow = Math.sin(t * (1.3 + hash(i) * 1.2) + i * 2);
|
||||||
|
if (glow < 0.15) continue;
|
||||||
|
const x = ax + aw * (0.5 + 0.45 * Math.sin(t * 0.21 + i * 2.3) * Math.cos(t * 0.13 + i)), y = ay + ah * (0.5 + 0.45 * Math.sin(t * 0.17 + i * 1.7));
|
||||||
|
ctx.fillStyle = `rgba(255, 240, 120, ${0.25 * glow})`;
|
||||||
|
ctx.fillRect(Math.round(x) - 1, Math.round(y) - 1, 3, 3);
|
||||||
|
ctx.fillStyle = `rgba(230, 255, 140, ${glow})`;
|
||||||
|
ctx.fillRect(Math.round(x), Math.round(y), 1, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Le finestre che si accendono di sera, una dopo l'altra, appena si entra nella scena.
|
||||||
|
windows(ctx, e, time, game) {
|
||||||
|
const since = ((game.time ?? 0) - (game.enteredAt ?? 0)) * 1000;
|
||||||
|
e.rects.forEach(([x, y, w, h], i) => {
|
||||||
|
if (since < 400 + hash(i * 7 + 3) * (e.over ?? 3500)) return;
|
||||||
|
ctx.fillStyle = 'rgba(255, 196, 100, 0.72)';
|
||||||
|
ctx.fillRect(x, y, w, h);
|
||||||
|
ctx.fillStyle = 'rgba(255, 236, 170, 0.5)';
|
||||||
|
ctx.fillRect(x + 1, y + 1, Math.max(1, w - 2), Math.max(1, Math.floor(h / 3)));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
// Rondini: ogni tanto un gruppetto attraversa il cielo, da destra a sinistra.
|
||||||
|
birds(ctx, e, time) {
|
||||||
|
const [ax, ay, aw, ah] = e.area, every = e.every ?? 9000, t = (time % every) / every, run = Math.floor(time / every);
|
||||||
|
if (t > 0.45) return;
|
||||||
|
const k = t / 0.45;
|
||||||
|
for (let i = 0; i < (e.count ?? 3); i++) {
|
||||||
|
const x = Math.round(ax + aw - k * (aw + 40) + i * 9 + hash(run * 5 + i) * 10);
|
||||||
|
const y = Math.round(ay + hash(run * 3 + i) * ah + Math.sin(k * 12 + i) * 3);
|
||||||
|
const up = Math.floor(time / 120 + i) % 2;
|
||||||
|
ctx.fillStyle = '#1e1a24';
|
||||||
|
ctx.fillRect(x, y, 2, 1);
|
||||||
|
ctx.fillRect(x - 2, y - up, 2, 1);
|
||||||
|
ctx.fillRect(x + 2, y - up, 2, 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// Alberi al vento (i cipressi): un pezzo di sfondo ridisegnato inclinato appena, più in cima che alla base.
|
||||||
|
sway(ctx, e, time, game) {
|
||||||
|
const bg = backgrounds.get(game.scene.background);
|
||||||
|
if (!bg) return;
|
||||||
|
const k = bg.width / WIDTH;
|
||||||
|
for (const [i, [x, y, w, h]] of e.rects.entries()) {
|
||||||
|
const a = Math.sin(time / (e.speed ?? 900) + i * 1.3) * (e.amount ?? 1.6);
|
||||||
|
for (let r = 0; r < h; r += 2) {
|
||||||
|
const dx = Math.round(a * (1 - r / h));
|
||||||
|
if (dx) ctx.drawImage(bg, x * k, (y + r) * k, w * k, 2 * k, x + dx, y + r, w, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
// Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`).
|
// Lampioni: un alone caldo che si accende (di solito solo di sera, con `phase`).
|
||||||
lamps(ctx, e, time) {
|
lamps(ctx, e, time) {
|
||||||
ctx.save();
|
ctx.save();
|
||||||
@@ -305,8 +359,10 @@ export const EFFECTS = {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
function drawEffects(ctx, game, time) {
|
const BACKGROUND_EFFECTS = new Set(['sway']);
|
||||||
for (const e of game.scene.effects ?? []) if (onNow(e, game)) EFFECTS[e.type]?.(ctx, e, time);
|
function drawEffects(ctx, game, time, background = false) {
|
||||||
|
for (const e of game.scene.effects ?? [])
|
||||||
|
if (BACKGROUND_EFFECTS.has(e.type) === background && onNow(e, game)) EFFECTS[e.type]?.(ctx, e, time, game);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Le scene buie (`shade`: il velo scuro sopra tutto, Andrea compreso) e le fiammelle (`lights`: candele accese).
|
// Le scene buie (`shade`: il velo scuro sopra tutto, Andrea compreso) e le fiammelle (`lights`: candele accese).
|
||||||
|
|||||||
+9
-1
@@ -538,7 +538,8 @@ function updateUi() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let last = performance.now(), drawnStill = false;
|
let last = performance.now(), drawnStill = false, fade = 0, fadeScene = null;
|
||||||
|
const FADE = 0.4; // secondi della dissolvenza fra le scene
|
||||||
function frame(now) {
|
function frame(now) {
|
||||||
// Il primo timestamp di requestAnimationFrame può precedere `last`: un dt negativo darebbe alla musica un volume
|
// Il primo timestamp di requestAnimationFrame può precedere `last`: un dt negativo darebbe alla musica un volume
|
||||||
// sotto zero, il browser lancerebbe un errore e il ciclo dei fotogrammi si fermerebbe (gioco congelato).
|
// sotto zero, il browser lancerebbe un errore e il ciclo dei fotogrammi si fermerebbe (gioco congelato).
|
||||||
@@ -556,6 +557,13 @@ function frame(now) {
|
|||||||
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() });
|
||||||
|
// Cambiando scena, un attimo di nero che sfuma: la dissolvenza dei vecchi giochi, invece del taglio netto.
|
||||||
|
if (game.state.scene !== fadeScene) { if (fadeScene) fade = 1; fadeScene = game.state.scene; }
|
||||||
|
if (fade > 0) {
|
||||||
|
ctx.fillStyle = `rgba(0, 0, 0, ${fade})`;
|
||||||
|
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||||
|
fade = Math.max(0, fade - dt / FADE);
|
||||||
|
}
|
||||||
updateUi();
|
updateUi();
|
||||||
}
|
}
|
||||||
drawnStill = still;
|
drawnStill = still;
|
||||||
|
|||||||
+8
-3
@@ -249,6 +249,7 @@ export const story = {
|
|||||||
name: 'Piazza Colonna',
|
name: 'Piazza Colonna',
|
||||||
map: [32, 45],
|
map: [32, 45],
|
||||||
background: 'assets/scenes/piazza-colonna.png',
|
background: 'assets/scenes/piazza-colonna.png',
|
||||||
|
effects: [{ type: 'windows', phase: 'sera', rects: [[16, 37, 8, 18], [48, 42, 8, 18], [50, 110, 8, 16], [97, 77, 8, 12], [122, 80, 8, 12], [170, 90, 8, 12], [204, 103, 8, 12], [239, 111, 8, 12], [268, 117, 7, 14], [287, 125, 6, 14], [97, 130, 9, 14], [127, 133, 9, 14], [170, 142, 9, 14], [204, 150, 8, 12], [239, 155, 8, 12], [268, 160, 7, 14], [287, 163, 6, 14], [386, 163, 14, 16], [424, 160, 14, 16], [472, 157, 14, 16], [524, 155, 14, 16], [390, 212, 14, 16], [526, 208, 14, 16]] }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [150, 20, 460, 80] }],
|
||||||
walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]],
|
walk: [[84, 244], [330, 238], [345, 230], [362, 238], [462, 246], [466, 276], [544, 276], [548, 248], [588, 250], [600, 290], [640, 300], [640, 360], [0, 360], [0, 330], [84, 318]],
|
||||||
depth: [[236, 0.5], [360, 1.35]],
|
depth: [[236, 0.5], [360, 1.35]],
|
||||||
entry: [345, 240],
|
entry: [345, 240],
|
||||||
@@ -1141,7 +1142,7 @@ export const story = {
|
|||||||
name: 'La Provinciale lungo la Carza',
|
name: 'La Provinciale lungo la Carza',
|
||||||
map: [66, 73],
|
map: [66, 73],
|
||||||
background: 'assets/scenes/verso-luna.png',
|
background: 'assets/scenes/verso-luna.png',
|
||||||
effects: [{ type: 'sparkle', area: [380, 288, 200, 30], count: 5 }, { type: 'sparkle', area: [430, 210, 40, 30], count: 2 }, { type: 'smoke', at: [113, 116] }, { type: 'lamps', phase: 'sera', points: [[165, 93]] }],
|
effects: [{ type: 'fireflies', phase: 'sera', area: [200, 210, 220, 80], count: 8 }, { type: 'sparkle', area: [380, 288, 200, 30], count: 5 }, { type: 'sparkle', area: [430, 210, 40, 30], count: 2 }, { type: 'smoke', at: [113, 116] }, { type: 'lamps', phase: 'sera', points: [[165, 93]] }],
|
||||||
walk: [[0, 360], [0, 200], [168, 190], [182, 196], [330, 360]],
|
walk: [[0, 360], [0, 200], [168, 190], [182, 196], [330, 360]],
|
||||||
depth: [[192, 0.45], [360, 1.7]],
|
depth: [[192, 0.45], [360, 1.7]],
|
||||||
entry: [150, 206],
|
entry: [150, 206],
|
||||||
@@ -1220,6 +1221,7 @@ export const story = {
|
|||||||
name: 'Campomigliaio, il circolino',
|
name: 'Campomigliaio, il circolino',
|
||||||
map: [55, 82],
|
map: [55, 82],
|
||||||
background: 'assets/scenes/campomigliaio-circolo.png',
|
background: 'assets/scenes/campomigliaio-circolo.png',
|
||||||
|
effects: [{ type: 'windows', phase: 'sera', rects: [[55, 186, 50, 28], [110, 186, 50, 28], [165, 186, 36, 28], [316, 180, 16, 44]] }],
|
||||||
walk: [[0, 360], [0, 335], [60, 300], [400, 280], [640, 250], [640, 360]],
|
walk: [[0, 360], [0, 335], [60, 300], [400, 280], [640, 250], [640, 360]],
|
||||||
depth: [[250, 1.0], [360, 1.5]],
|
depth: [[250, 1.0], [360, 1.5]],
|
||||||
entry: [320, 330],
|
entry: [320, 330],
|
||||||
@@ -1274,7 +1276,7 @@ export const story = {
|
|||||||
name: 'Campomigliaio, il ponte vecchio',
|
name: 'Campomigliaio, il ponte vecchio',
|
||||||
map: [50, 86],
|
map: [50, 86],
|
||||||
background: 'assets/scenes/campomigliaio-ponte.png',
|
background: 'assets/scenes/campomigliaio-ponte.png',
|
||||||
effects: [{ type: 'sparkle', area: [150, 205, 160, 90], count: 7 }, { type: 'sparkle', area: [330, 300, 110, 40], count: 3 }, { type: 'smoke', at: [605, 50] }],
|
effects: [{ type: 'fireflies', phase: 'sera', area: [0, 220, 150, 120], count: 7 }, { type: 'sparkle', area: [150, 205, 160, 90], count: 7 }, { type: 'sparkle', area: [330, 300, 110, 40], count: 3 }, { type: 'smoke', at: [605, 50] }],
|
||||||
walk: [[0, 360], [0, 232], [100, 225], [140, 240], [170, 275], [260, 330], [300, 360]],
|
walk: [[0, 360], [0, 232], [100, 225], [140, 240], [170, 275], [260, 330], [300, 360]],
|
||||||
depth: [[225, 0.6], [360, 1.5]],
|
depth: [[225, 0.6], [360, 1.5]],
|
||||||
entry: [80, 330],
|
entry: [80, 330],
|
||||||
@@ -1430,7 +1432,7 @@ export const story = {
|
|||||||
name: 'Villa Adami',
|
name: 'Villa Adami',
|
||||||
map: [46, 32],
|
map: [46, 32],
|
||||||
background: 'assets/scenes/villa-adami.png',
|
background: 'assets/scenes/villa-adami.png',
|
||||||
effects: [{ type: 'flag', at: [251, 4], w: 9, h: 6, colors: ['#f4f1e8', '#2a2a2a'] }],
|
effects: [{ type: 'windows', phase: 'sera', rects: [[148, 104, 10, 24], [205, 110, 10, 22], [300, 120, 12, 26], [372, 128, 10, 20], [412, 130, 10, 20], [448, 134, 10, 18], [150, 186, 12, 22], [205, 190, 12, 22], [372, 190, 10, 20], [412, 190, 10, 20], [448, 192, 10, 18]] }, { type: 'flag', at: [251, 4], w: 9, h: 6, colors: ['#f4f1e8', '#2a2a2a'] }],
|
||||||
walk: [[0, 272], [30, 246], [60, 244], [110, 256], [190, 268], [360, 296], [440, 288], [520, 276], [560, 268], [612, 262], [614, 272], [520, 288], [500, 292], [500, 360], [0, 360]],
|
walk: [[0, 272], [30, 246], [60, 244], [110, 256], [190, 268], [360, 296], [440, 288], [520, 276], [560, 268], [612, 262], [614, 272], [520, 288], [500, 292], [500, 360], [0, 360]],
|
||||||
depth: [[244, 0.5], [360, 1.2]],
|
depth: [[244, 0.5], [360, 1.2]],
|
||||||
entry: [300, 345],
|
entry: [300, 345],
|
||||||
@@ -1528,6 +1530,7 @@ export const story = {
|
|||||||
name: 'Il posticino della Carza',
|
name: 'Il posticino della Carza',
|
||||||
map: [60, 58],
|
map: [60, 58],
|
||||||
background: 'assets/scenes/carza.png',
|
background: 'assets/scenes/carza.png',
|
||||||
|
effects: [{ type: 'fireflies', phase: 'sera', area: [80, 200, 480, 100], count: 9 }],
|
||||||
walk: [[60, 360], [250, 262], [300, 218], [350, 218], [420, 262], [600, 360]],
|
walk: [[60, 360], [250, 262], [300, 218], [350, 218], [420, 262], [600, 360]],
|
||||||
depth: [[218, 0.55], [360, 1.65]],
|
depth: [[218, 0.55], [360, 1.65]],
|
||||||
entry: [300, 350],
|
entry: [300, 350],
|
||||||
@@ -1735,6 +1738,7 @@ export const story = {
|
|||||||
name: 'Viale dei cipressi',
|
name: 'Viale dei cipressi',
|
||||||
map: [60, 6],
|
map: [60, 6],
|
||||||
background: 'assets/scenes/viale-cipressi.png',
|
background: 'assets/scenes/viale-cipressi.png',
|
||||||
|
effects: [{ type: 'sway', rects: [[322, 58, 30, 150], [346, 8, 32, 190], [312, 108, 18, 110], [244, 118, 26, 100]] }],
|
||||||
walk: [[30, 360], [200, 300], [288, 252], [312, 250], [420, 300], [600, 360]],
|
walk: [[30, 360], [200, 300], [288, 252], [312, 250], [420, 300], [600, 360]],
|
||||||
depth: [[250, 0.16], [360, 1.2]],
|
depth: [[250, 0.16], [360, 1.2]],
|
||||||
entry: [300, 350],
|
entry: [300, 350],
|
||||||
@@ -1758,6 +1762,7 @@ export const story = {
|
|||||||
name: 'Castello del Trebbio',
|
name: 'Castello del Trebbio',
|
||||||
map: [84, 6],
|
map: [84, 6],
|
||||||
background: 'assets/scenes/trebbio.png',
|
background: 'assets/scenes/trebbio.png',
|
||||||
|
effects: [{ type: 'sway', rects: [[237, 140, 20, 60], [165, 160, 20, 48], [305, 132, 14, 30], [560, 120, 22, 80], [600, 120, 20, 80], [620, 100, 20, 100], [530, 160, 20, 40]] }, { type: 'fireflies', phase: 'sera', area: [80, 290, 440, 60], count: 10 }, { type: 'birds', phase: ['pomeriggio', 'tramonto'], area: [200, 20, 400, 90], every: 12000 }],
|
||||||
music: 'assets/audio/ricordi.mp3',
|
music: 'assets/audio/ricordi.mp3',
|
||||||
walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]],
|
walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]],
|
||||||
depth: [[306, 0.75], [360, 1.05]],
|
depth: [[306, 0.75], [360, 1.05]],
|
||||||
|
|||||||
Reference in New Issue
Block a user