Andrea vero: camminata, parlato, prendere e di spalle dal foglio sprite
tools/slice_sprites.py da Manzolo Rush e tools/prepare_character.py, che allinea i fotogrammi sul busto e sui piedi; il motore sceglie il fotogramma da quello che Andrea sta facendo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+24
-4
@@ -17,6 +17,7 @@ const DEFAULT_LINES = {
|
||||
const SPEED = 95;
|
||||
// Solo profilo, come i cicli di camminata: 'up' e 'down' lasciano il verso com'è.
|
||||
const FACING = { left: -1, right: 1 };
|
||||
const WALK = ['walk_1', 'walk_2', 'walk_3', 'walk_4'], STEP = 15, REACH = 0.45;
|
||||
|
||||
// Condizioni: 'flag', '!flag', 'has:oggetto', '!has:oggetto', oppure un elenco (tutte vere).
|
||||
export function check(cond, state) {
|
||||
@@ -57,12 +58,15 @@ export class Game {
|
||||
this.dialogue = null;
|
||||
this.walking = false;
|
||||
this.time = 0;
|
||||
this.stride = 0; // strada fatta, per scegliere il passo della camminata
|
||||
this.reach = 0; // secondi di braccio teso quando prende qualcosa
|
||||
this.back = false; // di spalle davanti a una porta o a una bacheca
|
||||
}
|
||||
|
||||
get scene() { return this.story.scenes[this.state.scene]; }
|
||||
// Occupato: c'è qualcosa in corso che il giocatore non può interrompere. La camminata verso un punto
|
||||
// o verso un oggetto si può sempre cambiare con un altro clic: la coda che la seguiva viene scartata.
|
||||
get busy() { return !!this.speech || (this.task ? !this.task.interruptible : this.queue.length > 0); }
|
||||
get busy() { return !!this.speech || this.reach > 0 || (this.task ? !this.task.interruptible : this.queue.length > 0); }
|
||||
get idle() { return !this.busy && !this.task && !this.dialogue; }
|
||||
|
||||
snapshot() {
|
||||
@@ -151,6 +155,15 @@ export class Game {
|
||||
startWalk(p) {
|
||||
this.path = findPath(this.scene.walk, [this.state.x, this.state.y], p);
|
||||
this.walking = this.path.length > 0;
|
||||
this.back = false;
|
||||
}
|
||||
|
||||
// Il fotogramma di Andrea in questo istante: un passo ogni STEP pixel percorsi (a grandezza piena).
|
||||
pose() {
|
||||
if (this.reach > 0) return 'take';
|
||||
if (this.walking) return WALK[Math.floor(this.stride / STEP) % WALK.length];
|
||||
if (this.speech?.who === 'andrea') return Math.floor(this.time / 0.2) % 2 ? 'talk' : 'stand';
|
||||
return this.back ? 'back' : 'stand';
|
||||
}
|
||||
|
||||
scaleAt(y) {
|
||||
@@ -174,8 +187,14 @@ export class Game {
|
||||
if (this.speech.left > 0) return;
|
||||
this.speech = null;
|
||||
}
|
||||
if (this.reach > 0) {
|
||||
this.reach -= dt;
|
||||
if (this.reach > 0) return;
|
||||
}
|
||||
if (this.walking) {
|
||||
let step = SPEED * this.scaleAt(this.state.y) * dt;
|
||||
const scale = this.scaleAt(this.state.y);
|
||||
let step = SPEED * scale * dt;
|
||||
this.stride += step / scale;
|
||||
while (step > 0 && this.path.length) {
|
||||
const target = this.path[0], d = dist([this.state.x, this.state.y], target);
|
||||
if (Math.abs(target[0] - this.state.x) > 0.5) this.state.facing = target[0] > this.state.x ? 1 : -1;
|
||||
@@ -203,8 +222,8 @@ export class Game {
|
||||
else if (e.walk) { this.task = e; this.startWalk(e.walk); }
|
||||
// `go` prima di `face`: un cambio scena porta con sé il verso d'arrivo.
|
||||
else if (e.go) this.go(e.go, e.at, e.face);
|
||||
else if (e.face) s.facing = FACING[e.face] ?? s.facing;
|
||||
else if (e.give) { if (!s.inventory.includes(e.give)) s.inventory.push(e.give); }
|
||||
else if (e.face) { s.facing = FACING[e.face] ?? s.facing; this.back = e.face === 'up'; }
|
||||
else if (e.give) { if (!s.inventory.includes(e.give)) s.inventory.push(e.give); this.reach = REACH; this.back = false; }
|
||||
else if (e.drop) s.inventory = s.inventory.filter(i => i !== e.drop);
|
||||
else if (e.set) s.flags[e.set] = true;
|
||||
else if (e.unset) delete s.flags[e.unset];
|
||||
@@ -223,6 +242,7 @@ export class Game {
|
||||
this.state.facing = FACING[face] ?? this.state.facing;
|
||||
this.path = [];
|
||||
this.walking = false;
|
||||
this.back = false;
|
||||
this.queue.unshift(...(this.scene.enter ?? []));
|
||||
this.onSave(this.snapshot());
|
||||
}
|
||||
|
||||
+47
-11
@@ -56,15 +56,50 @@ export function drawAndrea(ctx, x, y, scale, facing, walkPhase) {
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
// Sfondi delle scene, caricati una volta sola. Finché un'immagine non c'è si vedono le forme `art`.
|
||||
const backgrounds = new Map();
|
||||
export function loadBackgrounds(story) {
|
||||
return Promise.all(Object.values(story.scenes).filter(s => s.background).map(s => new Promise(resolve => {
|
||||
const img = new Image();
|
||||
img.onload = () => { backgrounds.set(s.background, img); resolve(true); };
|
||||
img.onerror = () => resolve(false);
|
||||
img.src = s.background;
|
||||
})));
|
||||
// Sfondi e fogli dei personaggi, caricati una volta sola. Finché un'immagine non c'è si vedono i segnaposto.
|
||||
const backgrounds = new Map(), sprites = new Map();
|
||||
const loadImage = src => new Promise(resolve => {
|
||||
const img = new Image();
|
||||
img.onload = () => resolve(img);
|
||||
img.onerror = () => resolve(null);
|
||||
img.src = src;
|
||||
});
|
||||
|
||||
export function loadAssets(story) {
|
||||
const scenes = Object.values(story.scenes).filter(s => s.background).map(async s => {
|
||||
const img = await loadImage(s.background);
|
||||
if (img) backgrounds.set(s.background, img);
|
||||
return !!img;
|
||||
});
|
||||
const people = Object.entries(story.characters).filter(([, c]) => c.sprite).map(async ([id, c]) => {
|
||||
try {
|
||||
const atlas = await (await fetch(c.sprite)).json();
|
||||
const img = await loadImage(new URL(atlas.image, new URL(c.sprite, location.href)).href);
|
||||
if (img) sprites.set(id, { atlas, img });
|
||||
return !!img;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return Promise.all([...scenes, ...people]);
|
||||
}
|
||||
|
||||
// Il personaggio alto PERSON pixel a grandezza 1, con i piedi in (x, y); rivolto a sinistra si specchia.
|
||||
const PERSON = 62;
|
||||
function drawSprite(ctx, { atlas, img }, frame, x, y, scale, facing) {
|
||||
const f = atlas.frames[frame] ?? atlas.frames.stand, k = PERSON * scale / atlas.height;
|
||||
ctx.save();
|
||||
ctx.translate(Math.round(x), Math.round(y));
|
||||
ctx.fillStyle = 'rgba(0,0,0,0.22)';
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(0, 0, 13 * scale, 3 * scale, 0, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
// La pagina è ingrandita a pixel netti: qui invece si rimpicciolisce, e la media dei pixel evita lo sfarfallio.
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
ctx.imageSmoothingQuality = 'high';
|
||||
if (frame !== 'back') ctx.scale(facing, 1);
|
||||
ctx.drawImage(img, f.x, f.y, f.w, f.h, -f.ax * k, -f.ay * k, f.w * k, f.h * k);
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
export function render(ctx, game, { hover } = {}) {
|
||||
@@ -74,8 +109,9 @@ export function render(ctx, game, { hover } = {}) {
|
||||
if (bg) ctx.drawImage(bg, 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);
|
||||
const andrea = sprites.get('andrea');
|
||||
if (andrea) drawSprite(ctx, andrea, game.pose(), s.x, s.y, game.scaleAt(s.y), s.facing);
|
||||
else drawAndrea(ctx, s.x, s.y, game.scaleAt(s.y), s.facing, game.walking ? game.stride / 6 : null);
|
||||
if (hover) {
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.35)';
|
||||
ctx.setLineDash([3, 3]);
|
||||
|
||||
Reference in New Issue
Block a user