- si salva solo a sequenza finita: uscire a metà di una risposta o della casetta riporta a prima; la squama presa resta salvata anche senza cambiare scena; test di interruzione su soluzione e catena del ghiacciolo - «Salva su file» nella pausa e «Carica» nel titolo, con controllo di versione e contenuto prima di sostituire il salvataggio del browser - cursore del volume delle voci («bla»), 50% di partenza; il muto zittisce subito la voce, la pausa la riprende - col dito un oggetto mancato di poco si prende, e i piccoli vincono sui grandi che li contengono (la squama); verbi su due colonne nel telefono in orizzontale - sfondi caricati per vicinanza alla scena di partenza, con tentativi limitati se falliscono; a gioco fermo non si ridisegna - modalità ?debug per scrivere le scene: area calpestabile, hotspot, punti, coordinate copiabili - corretto: i crediti dal titolo restavano dietro il titolo Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ApqNBvNbNCwXqR2RLo8QJh
101 lines
4.4 KiB
JavaScript
101 lines
4.4 KiB
JavaScript
// Area calpestabile: un poligono semplice (anche concavo) in coordinate di scena.
|
|
// Il percorso passa per i vertici concavi (grafo di visibilità + A*), così il personaggio
|
|
// gira attorno agli angoli invece di attraversare i muri.
|
|
const EPS = 1e-6;
|
|
|
|
export const dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
|
|
|
|
function onSegment(p, a, b, tol = 0.5) {
|
|
const [px, py] = p, [ax, ay] = a, [bx, by] = b;
|
|
const len2 = (bx - ax) ** 2 + (by - ay) ** 2;
|
|
const t = len2 ? Math.max(0, Math.min(1, ((px - ax) * (bx - ax) + (py - ay) * (by - ay)) / len2)) : 0;
|
|
return dist(p, [ax + t * (bx - ax), ay + t * (by - ay)]) <= tol;
|
|
}
|
|
|
|
// Dentro o sul bordo (con tolleranza di mezzo pixel).
|
|
export function inside(poly, p) {
|
|
let hit = false;
|
|
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
const a = poly[i], b = poly[j];
|
|
if (onSegment(p, a, b)) return true;
|
|
if ((a[1] > p[1]) !== (b[1] > p[1]) && p[0] < (b[0] - a[0]) * (p[1] - a[1]) / (b[1] - a[1]) + a[0]) hit = !hit;
|
|
}
|
|
return hit;
|
|
}
|
|
|
|
const cross = (o, a, b) => (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0]);
|
|
|
|
// Incrocio proprio: i due segmenti si tagliano in un punto interno a entrambi.
|
|
function crosses(p1, p2, q1, q2) {
|
|
const d1 = cross(q1, q2, p1), d2 = cross(q1, q2, p2), d3 = cross(p1, p2, q1), d4 = cross(p1, p2, q2);
|
|
return ((d1 > EPS && d2 < -EPS) || (d1 < -EPS && d2 > EPS)) && ((d3 > EPS && d4 < -EPS) || (d3 < -EPS && d4 > EPS));
|
|
}
|
|
|
|
export function visible(poly, a, b) {
|
|
if (!inside(poly, a) || !inside(poly, b)) return false;
|
|
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++)
|
|
if (crosses(a, b, poly[i], poly[j])) return false;
|
|
// Un segmento che passa per due vertici può uscire senza incrociare nessun lato: si campiona.
|
|
for (let k = 1; k < 8; k++) {
|
|
const t = k / 8;
|
|
if (!inside(poly, [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t])) return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function nearestInside(poly, p) {
|
|
if (inside(poly, p)) return [p[0], p[1]];
|
|
let best = null, bestD = Infinity;
|
|
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) {
|
|
const [ax, ay] = poly[j], [bx, by] = poly[i];
|
|
const len2 = (bx - ax) ** 2 + (by - ay) ** 2;
|
|
const t = len2 ? Math.max(0, Math.min(1, ((p[0] - ax) * (bx - ax) + (p[1] - ay) * (by - ay)) / len2)) : 0;
|
|
const q = [ax + t * (bx - ax), ay + t * (by - ay)], d = dist(p, q);
|
|
if (d < bestD) { bestD = d; best = q; }
|
|
}
|
|
return best;
|
|
}
|
|
|
|
function signedArea(poly) {
|
|
let s = 0;
|
|
for (let i = 0, j = poly.length - 1; i < poly.length; j = i++) s += poly[j][0] * poly[i][1] - poly[i][0] * poly[j][1];
|
|
return s / 2;
|
|
}
|
|
|
|
// I vertici concavi (svolta contraria al verso del poligono) sono gli unici dove un percorso minimo può piegare.
|
|
export function concaveVertices(poly) {
|
|
const sign = Math.sign(signedArea(poly)), n = poly.length;
|
|
return poly.filter((v, i) => sign * cross(poly[(i - 1 + n) % n], v, poly[(i + 1) % n]) < -EPS);
|
|
}
|
|
|
|
export function findPath(poly, from, to) {
|
|
const start = nearestInside(poly, from), goal = nearestInside(poly, to);
|
|
if (visible(poly, start, goal)) return [goal];
|
|
const nodes = [start, goal, ...concaveVertices(poly)];
|
|
const g = new Map([[0, 0]]), prev = new Map(), open = new Set([0]), done = new Set();
|
|
while (open.size) {
|
|
let cur = -1, curF = Infinity;
|
|
for (const i of open) {
|
|
const f = g.get(i) + dist(nodes[i], goal);
|
|
if (f < curF) { curF = f; cur = i; }
|
|
}
|
|
if (cur === 1) break;
|
|
open.delete(cur);
|
|
done.add(cur);
|
|
for (let i = 0; i < nodes.length; i++) {
|
|
if (i === cur || done.has(i) || !visible(poly, nodes[cur], nodes[i])) continue;
|
|
const cost = g.get(cur) + dist(nodes[cur], nodes[i]);
|
|
if (cost < (g.get(i) ?? Infinity)) { g.set(i, cost); prev.set(i, cur); open.add(i); }
|
|
}
|
|
}
|
|
if (!prev.has(1)) return [start];
|
|
const path = [];
|
|
for (let i = 1; i !== 0; i = prev.get(i)) path.unshift(nodes[i]);
|
|
return path;
|
|
}
|
|
|
|
export const inBox = ([x, y, w, h], p) => p[0] >= x && p[0] <= x + w && p[1] >= y && p[1] <= y + h;
|
|
|
|
// Distanza da un punto al rettangolo [x, y, w, h]; zero se ci sta dentro.
|
|
export const boxDistance = ([x, y, w, h], p) => Math.hypot(Math.max(x - p[0], 0, p[0] - x - w), Math.max(y - p[1], 0, p[1] - y - h));
|