Al Trebbio parla lo spirito del castello: compare accanto alla torre, semitrasparente, con la lanterna
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 23 KiB |
@@ -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) });
|
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();
|
||||||
|
drawApparitions(ctx, game, time);
|
||||||
drawLights(ctx, scene, time);
|
drawLights(ctx, scene, time);
|
||||||
for (const shape of art) if (shape.top) drawShape(ctx, shape);
|
for (const shape of art) if (shape.top) drawShape(ctx, shape);
|
||||||
if (spots) drawSpots(ctx, game);
|
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).
|
// 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.
|
// Ogni fiamma ha due disegni che si alternano a caso, e intorno un alone caldo che pulsa piano.
|
||||||
const FLAME_STEP = 140;
|
const FLAME_STEP = 140;
|
||||||
|
|||||||
+4
-2
@@ -99,7 +99,7 @@ export const story = {
|
|||||||
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
|
andrea: { name: 'Andrea', color: '#ffffff', sprite: 'assets/characters/andrea.json' },
|
||||||
vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' },
|
vecchietta: { name: 'Vecchietta', color: '#ffd27a', sprite: 'assets/characters/vecchietta.json' },
|
||||||
vecchietto: { name: 'Vecchietto', color: '#9fe3ff', sprite: 'assets/characters/vecchietto.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' },
|
leone: { name: 'Il leone', color: '#f0a070' },
|
||||||
san_pietro: { name: 'San Pietro', color: '#e8e0c8' },
|
san_pietro: { name: 'San Pietro', color: '#e8e0c8' },
|
||||||
tosca: { name: 'La Tosca', color: '#ffb0d0' },
|
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]],
|
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]],
|
||||||
entry: [110, 345],
|
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.' }] }],
|
enter: [{ if: '!trebbio_visto', then: [{ set: 'trebbio_visto' }, { say: 'Il Trebbio. Quassù anche i pensieri vanno più piano.' }] }],
|
||||||
hotspots: [
|
hotspots: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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 ?? [])
|
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`);
|
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`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user