Dentro la Pieve in penombra con le candele accese che tremolano; al tennis della chiesa si entra solo dal cancello verde
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
+35
-1
@@ -139,7 +139,7 @@ function drawSprite(ctx, { atlas, img }, frame, x, y, scale, facing, size = 1) {
|
|||||||
ctx.restore();
|
ctx.restore();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function render(ctx, game, { hover, spots, debug } = {}) {
|
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, bg = backgrounds.get(scene.background);
|
||||||
if (!bg && scene.background) loadBackground(scene.background);
|
if (!bg && scene.background) loadBackground(scene.background);
|
||||||
ctx.imageSmoothingEnabled = false;
|
ctx.imageSmoothingEnabled = false;
|
||||||
@@ -158,6 +158,7 @@ export function render(ctx, game, { hover, spots, debug } = {}) {
|
|||||||
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) });
|
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) });
|
||||||
}
|
}
|
||||||
for (const q of queue.sort((a, b) => a.y - b.y)) q.draw();
|
for (const q of queue.sort((a, b) => a.y - b.y)) q.draw();
|
||||||
|
drawLights(ctx, scene, time);
|
||||||
if (spots) drawSpots(ctx, game);
|
if (spots) drawSpots(ctx, game);
|
||||||
if (debug) drawDebug(ctx, game, debug);
|
if (debug) drawDebug(ctx, game, debug);
|
||||||
if (hover) {
|
if (hover) {
|
||||||
@@ -168,6 +169,39 @@ export function render(ctx, game, { hover, spots, debug } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Le scene buie (`shade`: il velo scuro sopra tutto, Andrea compreso) e le fiammelle (`lights`: candele accese).
|
||||||
|
// Ogni fiamma ha due disegni che si alternano a caso, e intorno un alone caldo che pulsa piano.
|
||||||
|
const FLAME_STEP = 140;
|
||||||
|
export function flameFrame(time, i) {
|
||||||
|
const n = Math.floor(time / FLAME_STEP) * 31 + i * 17;
|
||||||
|
return ((n * 2654435761) >>> 0) % 7 < 3 ? 1 : 0;
|
||||||
|
}
|
||||||
|
function drawLights(ctx, scene, time) {
|
||||||
|
if (scene.shade) {
|
||||||
|
ctx.fillStyle = scene.shade;
|
||||||
|
ctx.fillRect(0, 0, WIDTH, HEIGHT);
|
||||||
|
}
|
||||||
|
if (!scene.lights?.length) return;
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalCompositeOperation = 'lighter';
|
||||||
|
scene.lights.forEach(({ at: [x, y], glow = 30 }, i) => {
|
||||||
|
const pulse = 0.85 + 0.15 * Math.sin(time / 260 + i * 1.7);
|
||||||
|
const g = ctx.createRadialGradient(x, y - 2, 0, x, y - 2, glow * pulse);
|
||||||
|
g.addColorStop(0, 'rgba(255, 170, 70, 0.4)');
|
||||||
|
g.addColorStop(1, 'rgba(255, 120, 40, 0)');
|
||||||
|
ctx.fillStyle = g;
|
||||||
|
ctx.fillRect(x - glow, y - 2 - glow, glow * 2, glow * 2);
|
||||||
|
});
|
||||||
|
ctx.restore();
|
||||||
|
scene.lights.forEach(({ at: [x, y] }, i) => {
|
||||||
|
const tall = flameFrame(time, i);
|
||||||
|
ctx.fillStyle = '#ff9a2e';
|
||||||
|
ctx.fillRect(x - 1, y - 3 - tall, 2, 3 + tall);
|
||||||
|
ctx.fillStyle = '#ffe9a0';
|
||||||
|
ctx.fillRect(tall ? x - 1 : x, y - 2 - tall, 1, 2);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Il pulsante «?»: un segno su ogni cosa che si può toccare, col suo nome. Le uscite in giallo, il resto in bianco.
|
// Il pulsante «?»: un segno su ogni cosa che si può toccare, col suo nome. Le uscite in giallo, il resto in bianco.
|
||||||
// I nomi non si sovrappongono: placeLabels cerca per ognuno un posto libero vicino al suo segno.
|
// I nomi non si sovrappongono: placeLabels cerca per ognuno un posto libero vicino al suo segno.
|
||||||
function drawSpots(ctx, game) {
|
function drawSpots(ctx, game) {
|
||||||
|
|||||||
+3
-6
@@ -506,6 +506,9 @@ export const story = {
|
|||||||
name: 'Dentro la Pieve',
|
name: 'Dentro la Pieve',
|
||||||
map: [56, 39],
|
map: [56, 39],
|
||||||
background: 'assets/scenes/pieve-interno.png',
|
background: 'assets/scenes/pieve-interno.png',
|
||||||
|
// Penombra, e le sei candele dell'altare accese (Andrea, 24 ottobre).
|
||||||
|
shade: 'rgba(14, 8, 24, 0.62)',
|
||||||
|
lights: [{ at: [284, 157] }, { at: [294, 159] }, { at: [304, 155] }, { at: [336, 155] }, { at: [346, 159] }, { at: [356, 157] }],
|
||||||
walk: [[262, 360], [290, 256], [350, 256], [380, 360]],
|
walk: [[262, 360], [290, 256], [350, 256], [380, 360]],
|
||||||
depth: [[256, 0.7], [360, 1.7]],
|
depth: [[256, 0.7], [360, 1.7]],
|
||||||
entry: [320, 352],
|
entry: [320, 352],
|
||||||
@@ -1294,9 +1297,6 @@ export const story = {
|
|||||||
{
|
{
|
||||||
id: 'al_cancello', name: 'dal cancello verde, verso il posticino della Carza', box: [380, 300, 120, 60], at: [440, 330], exit: { scene: 'carza', at: [440, 290], face: 'left' },
|
id: 'al_cancello', name: 'dal cancello verde, verso il posticino della Carza', box: [380, 300, 120, 60], at: [440, 330], exit: { scene: 'carza', at: [440, 290], face: 'left' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'alla_carza', name: 'verso il ponticino della Carza', box: [470, 140, 120, 130], at: [515, 280], exit: { scene: 'ponticino', at: [400, 350], face: 'up' },
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// Fra la Fortezza e i cipressi: la stradina fra le vigne, la casa del maestro Bacci, e lassù il Trebbio.
|
// Fra la Fortezza e i cipressi: la stradina fra le vigne, la casa del maestro Bacci, e lassù il Trebbio.
|
||||||
@@ -1536,9 +1536,6 @@ export const story = {
|
|||||||
{
|
{
|
||||||
id: 'alla_stazione', name: 'a destra, verso la stazione', box: [486, 160, 90, 40], at: [474, 202], exit: { scene: 'stazione', at: [20, 300], face: 'right' },
|
id: 'alla_stazione', name: 'a destra, verso la stazione', box: [486, 160, 90, 40], at: [474, 202], exit: { scene: 'stazione', at: [20, 300], face: 'right' },
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: 'al_tennis', name: 'giù, verso il tennis della chiesa', box: [0, 160, 230, 70], at: [300, 330], exit: { scene: 'tennis_chiesa', at: [515, 280], face: 'left' },
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
id: 'alla_carza', name: 'indietro, verso la Pieve', box: [0, 330, 640, 30], at: [400, 354], exit: { scene: 'carza', at: [325, 222], face: 'down' },
|
id: 'alla_carza', name: 'indietro, verso la Pieve', box: [0, 330, 640, 30], at: [400, 354], exit: { scene: 'carza', at: [325, 222], face: 'down' },
|
||||||
},
|
},
|
||||||
|
|||||||
+10
-1
@@ -1,6 +1,6 @@
|
|||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import { loadBackground, hasBackground, RETRIES, RETRY_WAIT } from '../src/engine/render.js';
|
import { loadBackground, hasBackground, RETRIES, RETRY_WAIT, flameFrame } from '../src/engine/render.js';
|
||||||
|
|
||||||
// Un Image finto: fallisce le prime `fails` volte per ogni indirizzo, e conta le richieste.
|
// Un Image finto: fallisce le prime `fails` volte per ogni indirizzo, e conta le richieste.
|
||||||
function fakeImage(fails) {
|
function fakeImage(fails) {
|
||||||
@@ -56,3 +56,12 @@ test('i nomi del «?» non si sovrappongono, in nessuna scena', async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('le candele tremolano: due disegni che si alternano a caso, non tutte insieme, e stanno dentro la scena', async () => {
|
||||||
|
const { story } = await import('../src/story/story.js');
|
||||||
|
const frames = Array.from({ length: 60 }, (_, k) => [0, 1, 2].map(i => flameFrame(k * 140, i)));
|
||||||
|
for (const i of [0, 1, 2]) assert.deepEqual(new Set(frames.map(f => f[i])), new Set([0, 1]), `la candela ${i} non cambia mai`);
|
||||||
|
assert.ok(frames.some(f => new Set(f).size > 1), 'le fiamme cambiano tutte insieme');
|
||||||
|
for (const [id, sc] of Object.entries(story.scenes)) for (const { at: [x, y] } of sc.lights ?? [])
|
||||||
|
assert.ok(x >= 0 && x < 640 && y >= 4 && y < 360, `${id}: candela fuori scena`);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user