Effetti ambientali nelle altre 35 scene (lavoro di Codex): piccioni, merli, gatto, farfalle, libellula, falena, acqua che scorre, fontanella, tende, ombre delle fronde e delle nuvole

Controllato: test, partita completa e il giro di schermate degli effetti (npm run browser:effects).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
2026-10-25 00:55:39 +02:00
co-authored by Claude Opus 5.5
parent 55bb814cc0
commit aec43e6ee4
8 changed files with 411 additions and 14 deletions
+39 -13
View File
@@ -1,6 +1,7 @@
// Disegno della scena su un canvas 640×360, ingrandito dal CSS senza sfocatura.
// Le forme provvisorie (`art`) verranno sostituite da sfondi e sprite in pixel art.
import { check } from './game.js';
import { ambientEffects, ambientHash } from './ambient.js';
export const WIDTH = 640, HEIGHT = 360;
function drawShape(ctx, s) {
@@ -258,6 +259,7 @@ const hash = n => (((n | 0) * 2654435761) >>> 0) / 4294967296;
// Gli effetti delle scene (`effects`), ognuno col suo `type`: piccole animazioni disegnate sopra lo sfondo.
export const EFFECTS = {
...ambientEffects,
// Lucine dei festoni: ognuna si accende e si spegne per conto suo, coi suoi colori.
bulbs(ctx, e, time) {
const colors = e.colors ?? ['#ff5a5a', '#ffd25a', '#5ad2ff', '#7dff6a'];
@@ -345,6 +347,8 @@ export const EFFECTS = {
// 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;
ctx.globalAlpha = e.opacity ?? 1;
ctx.globalCompositeOperation = e.blend ?? 'source-over';
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)';
@@ -355,9 +359,10 @@ export const EFFECTS = {
},
// Rondini: ogni pochi secondi un gruppetto attraversa il cielo in diagonale, a «V» con le ali che battono.
birds(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, every = e.every ?? 6000, run = Math.floor(time / every), t = (time % every) / every;
if (t > 0.6) return;
const k = t / 0.6, rtl = hash(run * 13) < 0.5;
const [ax, ay, aw, ah] = e.area, every = e.every ?? 6000, duration = e.duration ?? every * 0.6;
const elapsed = (time + (e.offset ?? 0)) % every, run = Math.floor((time + (e.offset ?? 0)) / every);
if (elapsed >= duration) return;
const k = elapsed / duration, rtl = hash(run * 13) < 0.5;
for (let i = 0; i < (e.count ?? 4); i++) {
const lag = i * 0.05, kk = k - lag;
if (kk < 0) continue;
@@ -411,16 +416,23 @@ export const EFFECTS = {
lc.globalCompositeOperation = 'source-over';
ctx.drawImage(layer, 0, 0);
},
// Le ombre delle fronde sul terreno: macchie scure e chiare che ondeggiano piano col vento, come sotto un albero.
// Ombre di foglie in piccoli gruppi irregolari. Il seme mescolato evita file di
// ovali equidistanti (che sembravano sassi); niente rettangoli di sfondo deformati.
leafshadow(ctx, e, time) {
const [ax, ay, aw, ah] = e.area, t = time / 1000;
ctx.globalAlpha = e.opacity ?? 1;
for (let i = 0; i < (e.count ?? 14); i++) {
const bx = ax + hash(i * 7) * aw, by = ay + hash(i * 13) * ah, r = 3 + hash(i * 3) * (e.size ?? 6);
const x = bx + Math.sin(t * 0.9 + i) * 2 + Math.sin(t * 0.37 + i * 2) * 2, y = by + Math.sin(t * 0.7 + i * 3) * 1;
ctx.fillStyle = i % 3 ? 'rgba(30, 20, 45, 0.24)' : 'rgba(255, 235, 170, 0.18)';
for (let dy = -r; dy <= r; dy += 1) {
const half = Math.round(Math.sqrt(r * r - dy * dy) * 1.6);
ctx.fillRect(Math.round(x - half), Math.round(y + dy / 2), half * 2, 1);
const bx = ax + ambientHash(i * 7 + ax) * aw, by = ay + ambientHash(i * 13 + ay) * ah;
const x = bx + Math.sin(t * 0.9 + i) * 2 + Math.sin(t * 0.37 + i * 2), y = by + Math.sin(t * 0.7 + i * 3);
for (let leaf = 0; leaf < 4; leaf++) {
const n = i * 31 + leaf * 7, dx = (ambientHash(n + 3) - 0.5) * 22, dy = (ambientHash(n + 9) - 0.5) * 9;
const w = 3 + ambientHash(n + 21) * (e.size ?? 7), h = 1.5 + ambientHash(n + 4) * 2.5;
const px = x + dx, py = y + dy;
ctx.fillStyle = leaf === 3 ? 'rgba(255,235,170,0.10)' : 'rgba(30,29,40,0.15)';
ctx.beginPath();
ctx.moveTo(px - w, py); ctx.lineTo(px - w * 0.2, py - h);
ctx.lineTo(px + w * 0.6, py - h * 0.5); ctx.lineTo(px + w, py + h * 0.3);
ctx.lineTo(px, py + h); ctx.closePath(); ctx.fill();
}
}
},
@@ -468,10 +480,24 @@ export const EFFECTS = {
ctx.restore();
},
};
const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow']);
const BACKGROUND_EFFECTS = new Set(['sway', 'clouds', 'cloth', 'leafshadow', ...Object.keys(ambientEffects)]);
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);
for (const e of game.scene.effects ?? []) {
if (BACKGROUND_EFFECTS.has(e.type) !== background || !onNow(e, game)) continue;
ctx.save();
// Acqua solo nell'acqua, tende e sagome solo dietro il vetro; il ritaglio può
// seguire una riva obliqua senza toccare il terreno o gli oggetti degli enigmi.
if (e.clip) {
ctx.beginPath();
e.clip.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
ctx.closePath();
ctx.clip();
}
// Gli eventi nuovi seguono il tempo giocato: pausa e scheda nascosta non li fanno saltare.
const at = ambientEffects[e.type] ? (game.time ?? time / 1000) * 1000 : time;
EFFECTS[e.type]?.(ctx, e, at, game);
ctx.restore();
}
}
// Le scene buie (`shade`: il velo scuro sopra tutto, Andrea compreso) e le fiammelle (`lights`: candele accese).