diff --git a/assets/characters/spirito-trebbio.png b/assets/characters/spirito-trebbio.png new file mode 100644 index 0000000..b48c9fb Binary files /dev/null and b/assets/characters/spirito-trebbio.png differ diff --git a/src/engine/render.js b/src/engine/render.js index 28b896a..051cfd5 100644 --- a/src/engine/render.js +++ b/src/engine/render.js @@ -160,6 +160,7 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now( 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(); + drawApparitions(ctx, game, time); drawLights(ctx, scene, time); for (const shape of art) if (shape.top) drawShape(ctx, shape); if (spots) drawSpots(ctx, game); @@ -172,6 +173,29 @@ export function render(ctx, game, { hover, spots, debug, time = performance.now( } } +// Le apparizioni (`apparitions`): uno spirito che compare solo mentre parla (un dialogo suo, o una sua battuta), +// semitrasparente, che ondeggia piano, con un alone freddo intorno e il caldo della lanterna. +function drawApparitions(ctx, game, time) { + for (const a of game.scene.apparitions ?? []) { + if (game.dialogue?.id !== a.who && game.speech?.who !== a.who) continue; + const img = backgrounds.get(a.src); + if (!img) { loadBackground(a.src); continue; } + const h = a.height, w = Math.round(img.width * h / img.height), bob = Math.round(Math.sin(time / 600) * 2); + const x = Math.round(a.at[0] - w / 2), y = a.at[1] - h + bob; + ctx.save(); + const halo = ctx.createRadialGradient(a.at[0], y + h * 0.45, 0, a.at[0], y + h * 0.45, h * 0.7); + halo.addColorStop(0, 'rgba(180, 230, 255, 0.28)'); + halo.addColorStop(1, 'rgba(180, 230, 255, 0)'); + ctx.globalCompositeOperation = 'lighter'; + ctx.fillStyle = halo; + ctx.fillRect(a.at[0] - h, y - h * 0.3, h * 2, h * 1.6); + ctx.globalCompositeOperation = 'source-over'; + ctx.globalAlpha = 0.82 + 0.08 * Math.sin(time / 380); + ctx.drawImage(img, x, y, w, h); + ctx.restore(); + } +} + // 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; diff --git a/src/story/story.js b/src/story/story.js index 4985baf..06955b0 100644 --- a/src/story/story.js +++ b/src/story/story.js @@ -99,7 +99,7 @@ export const story = { andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' }, vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' }, vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.json' }, - castello: { name: 'Il Castello', color: '#c8f0a0' }, + castello: { name: 'Lo spirito del Trebbio', color: '#c8f0ff' }, leone: { name: 'Il leone', color: '#f0a070' }, san_pietro: { name: 'San Pietro', color: '#e8e0c8' }, tosca: { name: 'La Tosca', color: '#ffb0d0' }, @@ -1736,7 +1736,9 @@ export const story = { walk: [[86, 308], [300, 306], [356, 308], [470, 312], [500, 324], [486, 360], [88, 360], [80, 330]], depth: [[306, 0.75], [360, 1.05]], entry: [110, 345], - speakers: { castello: [389, 22] }, + // Lo spirito del castello (disegnato da Codex, idea di Andrea del 24 ottobre): compare quando parla. + speakers: { castello: [446, 178] }, + apparitions: [{ who: 'castello', src: 'assets/characters/spirito-trebbio.png', at: [446, 304], height: 120 }], enter: [{ if: '!trebbio_visto', then: [{ set: 'trebbio_visto' }, { say: 'Il Trebbio. Quassù anche i pensieri vanno più piano.' }] }], hotspots: [ { diff --git a/tests/render.test.mjs b/tests/render.test.mjs index e489954..825f17a 100644 --- a/tests/render.test.mjs +++ b/tests/render.test.mjs @@ -65,3 +65,13 @@ test('le candele tremolano: due disegni che si alternano a caso, non tutte insie 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`); }); + +test('le apparizioni (lo spirito del Trebbio) hanno la loro immagine e parlano con una voce del gioco', async () => { + const { story } = await import('../src/story/story.js'); + const { existsSync } = await import('node:fs'); + for (const [id, sc] of Object.entries(story.scenes)) for (const a of sc.apparitions ?? []) { + assert.ok(existsSync(new URL('../' + a.src, import.meta.url)), `${id}: manca ${a.src}`); + assert.ok(story.characters[a.who], `${id}: ${a.who} non è un personaggio`); + assert.ok(sc.speakers?.[a.who], `${id}: il fumetto di ${a.who} non ha un posto`); + } +});