feat(vida): modo autônomo, o Carinha como bichinho virtual

Novo módulo vida.py: energia, alegria, companhia e diversão caem com o
tempo e sobem com presença (PIR), movimento, conversa e carinho. Delas sai
o humor e, do humor, os atos: gestos, cochilo quando cansado, dormir à
noite sem ninguém por perto, acenar pedindo atenção quando carente com
gente perto, tocar reações da biblioteca e inventar figurinhas novas com
o Gemini (no máximo 3 por dia, 90 min entre elas).

- O celular recebe "humor x" e escolhe os gestos soltos por ele.
- Toque na tela do celular conta como carinho/cutucada (/vida/cuidar).
- Respeita E-Stop e o modo presença (ninguém em casa = quieto).
- Painel: cartão Vida no Ao vivo com humor, barras, cuidados, ritmo e
  diário; liga por padrão.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-06 04:21:23 -03:00
co-authored by Claude Opus 5.5
parent 8d410dbf24
commit b456f80716
11 changed files with 1036 additions and 16 deletions
+32 -4
View File
@@ -359,6 +359,20 @@ const r = {
const SEQS = sequencias();
const LIVRES = ["feliz", "olhar_esq", "olhar_dir", "olhar_baixo",
"surpreso", "coracao", "piscadinha", "bravo", "triste", "alegre"];
/* Modo vida (servidor): o humor do bichinho escolhe os gestos soltos entre um ato e outro. */
const LIVRES_HUMOR = {
feliz: ["feliz", "alegre", "piscadinha", "coracao", "olhar_esq", "olhar_dir"],
tranquilo: ["olhar_esq", "olhar_dir", "feliz", "piscadinha", "olhar_baixo"],
entediado: ["olhar_esq", "olhar_dir", "olhar_baixo", "pensar"],
carente: ["olhar_baixo", "triste", "olhar_esq", "olhar_dir"],
triste: ["triste", "olhar_baixo", "olhar_baixo"],
cansado: ["olhar_baixo", "piscar2"],
};
let HUMOR = "normal";
function gestoSolto() {
const lista = LIVRES_HUMOR[HUMOR] || LIVRES;
return lista[Math.floor(Math.random() * lista.length)];
}
function proximo() {
if (!r.fila.length) { r.kf = null; return; }
@@ -527,6 +541,8 @@ function trata(ordem) {
if (v.length === 3 && v.every((n) => !isNaN(n))) {
CIANO.splice(0, 3, ...v); r.cor = v;
}
} else if (ordem.indexOf("humor ") === 0) {
HUMOR = ordem.slice(6).trim();
} else if (ordem === "dormir" || ordem === "sono") {
r.dormindo = true;
} else if (ordem === "acordar") {
@@ -627,9 +643,10 @@ if (MODO_PAINEL) {
if (!inicio) return;
const dx = e.clientX - inicio.x, dy = e.clientY - inicio.y, dur = performance.now() - inicio.t;
inicio = null;
if (r.dormindo) { trata('acordar'); return; }
if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) && dur < 900) { animar('coracao'); return; }
if (r.dormindo) { trata('acordar'); cuidar('cutucar'); return; }
if (Math.abs(dx) > 60 && Math.abs(dx) > Math.abs(dy) && dur < 900) { animar('coracao'); cuidar('carinho'); return; }
if (dur < 350 && Math.hypot(dx, dy) < 15) {
cuidar('cutucar');
animar('surpreso');
setTimeout(() => animar('feliz'), 700);
}
@@ -736,6 +753,15 @@ function relatarSom(evento, n) {
if (evento !== "adormeceu") registrarEvento("som", evento, n);
}
/* Toque na tela vira cuidado no modo vida (carinho/cutucada). Só o celular conta. */
function cuidar(acao) {
if (new URLSearchParams(location.search).get("aparelho") !== "1") return;
try {
fetch("/vida/cuidar", { method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ acao }) }).catch(() => {});
} catch (e) {}
}
/* Histórico de detecções no servidor (o app tira a foto do evento). Só o celular registra. */
function registrarEvento(tipo, evento, nivel) {
if (new URLSearchParams(location.search).get("aparelho") !== "1") return;
@@ -1105,8 +1131,10 @@ function laco(t) {
animar("piscar");
proxPiscar = t + 1600 + Math.random() * 3500;
} else if (AUTO && t >= proxGesto) {
animar(LIVRES[Math.floor(Math.random() * LIVRES.length)]);
proxGesto = t + 7000 + Math.random() * 8000;
animar(gestoSolto());
// cansado e triste mexem menos; feliz, mais
const ritmo = { cansado: 1.8, triste: 1.5, feliz: 0.8 }[HUMOR] || 1;
proxGesto = t + (7000 + Math.random() * 8000) * ritmo;
}
} else if (t >= proxGesto) {
proxGesto = t + 7000 + Math.random() * 8000;