Prototipo del motore: scena, camminata, verbi, inventario, dialoghi, salvataggio

Motore guidato dai dati con due scene di prova (Piazza e salita alla Fortezza),
percorsi su area concava, clic intelligente e pressione lunga, test della soluzione
completa in node e nel browser con Playwright. Nel documento la leggenda del nonno.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-23 00:08:00 +02:00
co-authored by Claude Opus 5.5
parent 0c5cd240e4
commit 826f5f99ec
22 changed files with 1572 additions and 1 deletions
+72
View File
@@ -0,0 +1,72 @@
// 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.
export const WIDTH = 640, HEIGHT = 360;
function drawShape(ctx, s) {
if (s.rect) {
ctx.fillStyle = s.fill;
ctx.fillRect(...s.rect);
} else if (s.poly) {
ctx.fillStyle = s.fill;
ctx.beginPath();
s.poly.forEach(([x, y], i) => i ? ctx.lineTo(x, y) : ctx.moveTo(x, y));
ctx.closePath();
ctx.fill();
} else if (s.lines) {
// Righe di riempimento: fughe del selciato o sbarre di un cancello.
const { from: [x0, y0], to: [x1, y1], step, vertical } = s.lines;
ctx.fillStyle = s.stroke;
if (vertical) for (let x = x0; x <= x1; x += step) ctx.fillRect(x, y0, 2, y1 - y0);
else for (let y = y0; y <= y1; y += step) ctx.fillRect(x0, y, x1 - x0, 1);
} else if (s.text) {
ctx.fillStyle = s.fill;
ctx.font = 'bold 11px monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(s.text, ...s.at);
}
}
// Andrea provvisorio, disegnato a blocchi: maglia blu (il rione di Campomigliaio), gambe che si alternano.
const BODY = [
// [x, y, w, h, colore] con l'origine ai piedi, rivolto a destra.
[-6, -60, 12, 4, '#4a2e1c'], [-6, -56, 12, 12, '#e8b48c'], [3, -53, 2, 2, '#2a1a10'], [-7, -60, 4, 10, '#4a2e1c'],
[-8, -44, 16, 20, '#2f5fb3'], [-4, -42, 8, 3, '#f4efe4'],
];
export function drawAndrea(ctx, x, y, scale, facing, walkPhase) {
ctx.save();
ctx.translate(Math.round(x), Math.round(y));
ctx.scale(scale * facing, scale);
ctx.fillStyle = 'rgba(0,0,0,0.22)';
ctx.fillRect(-10, -2, 20, 4);
const swing = walkPhase == null ? 0 : Math.round(Math.sin(walkPhase) * 5);
ctx.fillStyle = '#3a3f52';
ctx.fillRect(-6 + swing, -24, 5, 22);
ctx.fillRect(1 - swing, -24, 5, 22);
ctx.fillStyle = '#e8e3d6';
ctx.fillRect(-7 + swing, -3, 7, 3);
ctx.fillRect(0 - swing, -3, 7, 3);
for (const [bx, by, w, h, c] of BODY) {
ctx.fillStyle = c;
ctx.fillRect(bx, by, w, h);
}
ctx.fillStyle = '#e8b48c';
ctx.fillRect(-2 - swing, -40, 4, 16);
ctx.restore();
}
export function render(ctx, game, { hover } = {}) {
const scene = game.scene, s = game.state;
ctx.clearRect(0, 0, WIDTH, HEIGHT);
for (const shape of scene.art ?? []) drawShape(ctx, shape);
for (const h of game.hotspots()) for (const shape of h.art ?? []) drawShape(ctx, shape);
const phase = game.walking ? (s.x + s.y) / 6 : null;
drawAndrea(ctx, s.x, s.y, game.scaleAt(s.y), s.facing, phase);
if (hover) {
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
ctx.setLineDash([3, 3]);
ctx.strokeRect(...hover.box);
ctx.setLineDash([]);
}
}