diff --git a/android/app/assets/carinha.html b/android/app/assets/carinha.html
index e2438e7..a9d9c6d 100644
--- a/android/app/assets/carinha.html
+++ b/android/app/assets/carinha.html
@@ -212,7 +212,7 @@ function caminhoBoca(tipo) {
// repouso: leve, com as microexpressões (canto que sobe, boca de lado, entreaberta)
const f = FEICAO.atual, w = l * 0.30, mx = cx + f.dx * l * 0.10;
const yE = cy - Math.max(0, f.lado) * l * 0.12, yD = cy - Math.max(0, -f.lado) * l * 0.12;
- const curva = l * (0.18 + f.curva * 0.35);
+ const curva = l * (0.18 + f.curva * 0.35 - ZANGA * 0.3);
let d = "M " + (mx - w) + "," + yE + " Q " + mx + "," + (cy + curva - f.abre * l * 0.12) + " " + (mx + w) + "," + yD;
if (f.abre > 0.04) d += " Q " + mx + "," + (cy + curva + f.abre * l * 0.30) + " " + (mx - w) + "," + yE;
return d;
@@ -224,7 +224,8 @@ function caminhoSobrancelha(i, esc) {
const ab = i === 0 ? r.ab : (r.ab2 === null ? r.ab : r.ab2);
const piscando = 1 - Math.min(1, Math.max(0, ab));
const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC - (f.sob[i] - piscando * 3) * ESC;
- const inc = ((r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : 0) + f.inc[i]) * lado * ESC;
+ const base = r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : ZANGA * 14; // zanga franze a testa
+ const inc = (base + f.inc[i]) * lado * ESC;
const ele = r.esc > 1.1 ? -15 * ESC : 0;
return 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele);
}
@@ -401,12 +402,17 @@ const LIVRES = ["feliz", "olhar_esq", "olhar_dir", "olhar_baixo",
const LIVRES_HUMOR = {
feliz: ["feliz", "alegre", "piscadinha", "coracao", "olhar_esq", "olhar_dir"],
tranquilo: ["olhar_esq", "olhar_dir", "feliz", "piscadinha", "olhar_baixo", "timido"],
+ bravo: ["bravo", "olhar_baixo", "olhar_esq", "olhar_dir", "confuso"],
entediado: ["olhar_esq", "olhar_dir", "olhar_baixo", "pensar", "bocejar", "confuso"],
carente: ["olhar_baixo", "triste", "olhar_esq", "olhar_dir", "timido"],
triste: ["triste", "olhar_baixo", "olhar_baixo"],
cansado: ["olhar_baixo", "piscar2", "bocejar"],
};
let HUMOR = "normal";
+/* Modo vida: zanga (0 a 1) deixa o rosto emburrado aos poucos quando a alegria cai; sono profundo =
+ dormiu de cansaço, e aí som, movimento e câmera não acordam (como na madrugada). */
+let ZANGA = 0;
+let SONO_PROFUNDO = false;
/* Microexpressões acordado: de vez em quando a boca e as sobrancelhas mexem sozinhas (um canto
da boca sobe, uma sobrancelha arqueia, um suspiro) e voltam devagar ao neutro. O humor do modo
@@ -431,6 +437,7 @@ const MICRO_HUMOR = {
carente: ["preocupado", "preocupado", "suspiro", "meioSorriso"],
triste: ["preocupado", "suspiro", "pensativo"],
cansado: ["suspiro", "suspiro", "ladinho"],
+ bravo: ["pensativo", "pensativo", "ladinho", "suspiro"],
normal: ["meioSorriso", "arquear", "sorrisinho", "ladinho", "interesse", "pensativo", "suspiro"],
};
function atualizarFeicao(s) {
@@ -495,11 +502,11 @@ function passo(dt) { // avanca a fila; true se ha gesto em
/* Atividade em andamento (rosto-motor.js): para onde olhar e que cara fazer. Atualizado por quadro. */
let ATIV = null;
-function atividade(nome) {
+function atividade(nome, segundos) {
if (!window.MotorRosto || !window.MotorRosto.atividade) return;
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
if (r.dormindo) { r.dormindo = false; animar("surpreso"); }
- if (window.MotorRosto.atividade(nome) && nome === "cantar") tocarSom("musica");
+ if (window.MotorRosto.atividade(nome, segundos) && nome === "cantar") tocarSom("musica");
}
function repouso(t) { // pose calma: olha devagar, "respira"
@@ -526,6 +533,8 @@ function repouso(t) { // pose calma: olha devagar, "respira
if (ATIV.forma) r.forma = ATIV.forma;
if (ATIV.boca) r.boca = ATIV.boca;
if (ATIV.ab) r.ab = ATIV.ab;
+ } else if (ZANGA >= 0.7) { // alegria caindo: emburrado (a cor escolhida fica)
+ r.forma = "bravo";
}
}
@@ -634,7 +643,13 @@ function trata(ordem) {
} else if (ordem.indexOf("ritmo ") === 0) {
PERFIL = PERFIS[ordem.slice(6).trim()] || PERFIS.normal;
} else if (ordem.indexOf("atividade ") === 0) {
- atividade(ordem.slice(10).trim());
+ const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180"
+ atividade(nome, parseFloat(seg) || 0);
+ } else if (ordem.indexOf("zanga ") === 0) {
+ const z = parseFloat(ordem.slice(6));
+ if (!isNaN(z)) ZANGA = Math.max(0, Math.min(1, z));
+ } else if (ordem === "sono profundo" || ordem === "sono leve") {
+ SONO_PROFUNDO = ordem === "sono profundo";
} else if (ordem === "dormir" || ordem === "sono") {
r.dormindo = true;
atividade(null);
@@ -790,6 +805,7 @@ const MIC = {
/* Acordar com som: precisa de N barulhos em 1 minuto (ajuste "Sons para acordar"). Na madrugada
silenciosa só barulho ALTO conta, e movimento/câmera não acordam (alguém se mexendo na cama). */
const DESPERTAR = { sons: 3, noite: true, ini: 23, fim: 7 };
+function sonoRespeitado() { return madrugada() || SONO_PROFUNDO; }
function madrugada() {
if (!DESPERTAR.noite || DESPERTAR.ini === DESPERTAR.fim) return false;
const h = new Date().getHours();
@@ -855,7 +871,7 @@ function som(nivel) {
if (r.dormindo) {
const precisa = Math.max(1, Math.round(DESPERTAR.sons * PERFIL.acordarSom));
- if (contar(CONTA.somAcordar, madrugada() ? alto : temSom, agora, precisa, "ouvido: barulho")) { // acordou
+ if (contar(CONTA.somAcordar, sonoRespeitado() ? alto : temSom, agora, precisa, "ouvido: barulho")) { // acordou
r.dormindo = false;
animar("surpreso");
MIC.proxSurpresa = agora + MIC.cooldown;
@@ -878,6 +894,7 @@ function checarSilencio(agora) {
if (!MIC.ativo) return; // telinha do Pi: sem ouvido
if (agora - MIC.visto > 2) return; // o ouvido parou de mandar dado
if (r.dormindo) return;
+ if (HUMOR !== "normal") return; // modo vida ligado: quem manda dormir é a energia
// movimento tambem conta como "tem alguem aqui": nao adormece com gente passando
const atividade = Math.max(MIC.ultimoSom, MOV.ativo ? MOV.ultimoMov : 0);
if (agora - atividade >= MIC.segSilencio) {
@@ -949,7 +966,7 @@ function rastrear(t) {
MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65;
if(t.atividade===true) {
MOV.ultimoMov=agora;
- if(r.dormindo && !madrugada() && PERFIL !== PERFIS.calmo) { r.dormindo=false;animar('surpreso'); }
+ if(r.dormindo && !sonoRespeitado() && PERFIL !== PERFIS.calmo) { r.dormindo=false;animar('surpreso'); }
}
}
function movimento(vel, cx, cy) {
@@ -981,7 +998,7 @@ function movimento(vel, cx, cy) {
const forte = v >= limiar(MOV.limiarForte);
if (r.dormindo) {
// acordou: algo se mexeu na frente (de dia; calmo precisa de vários movimentos)
- if (!madrugada() && contar(CONTA.movAcordar, mexeu, agora, PERFIL.acordarMov, "presenca: movimento")) {
+ if (!sonoRespeitado() && contar(CONTA.movAcordar, mexeu, agora, PERFIL.acordarMov, "presenca: movimento")) {
r.dormindo = false;
animar("surpreso");
MOV.proxSurpresa = agora + MOV.cooldown;
diff --git a/android/app/assets/rosto-motor.js b/android/app/assets/rosto-motor.js
index 201b874..d17a361 100644
--- a/android/app/assets/rosto-motor.js
+++ b/android/app/assets/rosto-motor.js
@@ -524,12 +524,26 @@
// vale para qualquer rosto (SVG ou motor). O carinha.html pergunta para onde olhar
// (olharAtividade) e chama palco() a cada quadro.
const ATIVIDADES = {
- ler: { nome: 'Lendo', emoji: '📖', dur: [25, 45] },
- cantar: { nome: 'Cantando', emoji: '🎤', dur: [18, 30] },
- bola: { nome: 'Jogando bola', emoji: '⚽', dur: [20, 35] },
- videogame: { nome: 'Videogame', emoji: '🎮', dur: [25, 45] },
- pintar: { nome: 'Pintando', emoji: '🎨', dur: [25, 40] },
- malhar: { nome: 'Malhando', emoji: '🏋️', dur: [15, 25] },
+ ler: { nome: 'Lendo', emoji: '📖', dur: [25, 45] },
+ cantar: { nome: 'Cantando', emoji: '🎤', dur: [18, 30] },
+ bola: { nome: 'Jogando bola', emoji: '⚽', dur: [20, 35] },
+ videogame: { nome: 'Videogame', emoji: '🎮', dur: [25, 45] },
+ pintar: { nome: 'Pintando', emoji: '🎨', dur: [25, 40] },
+ malhar: { nome: 'Malhando', emoji: '🏋️', dur: [15, 25] },
+ dormir: { nome: 'Cochilando', emoji: '🛏️', dur: [40, 80] },
+ cafe: { nome: 'Tomando café', emoji: '☕', dur: [20, 35] },
+ cozinhar: { nome: 'Cozinhando', emoji: '🍳', dur: [20, 35] },
+ regar: { nome: 'Regando planta', emoji: '🪴', dur: [20, 30] },
+ violao: { nome: 'Tocando violão', emoji: '🎸', dur: [20, 35] },
+ dancar: { nome: 'Dançando', emoji: '💃', dur: [15, 30] },
+ corda: { nome: 'Pulando corda', emoji: '🪢', dur: [15, 25] },
+ meditar: { nome: 'Meditando', emoji: '🧘', dur: [25, 45] },
+ telescopio: { nome: 'Olhando estrelas', emoji: '🔭', dur: [25, 40] },
+ pipa: { nome: 'Soltando pipa', emoji: '🪁', dur: [20, 35] },
+ bolhas: { nome: 'Bolhas de sabão', emoji: '🫧', dur: [18, 30] },
+ skate: { nome: 'Andando de skate', emoji: '🛹', dur: [18, 30] },
+ computador: { nome: 'No computador', emoji: '💻', dur: [25, 45] },
+ pescar: { nome: 'Pescando', emoji: '🎣', dur: [30, 50] },
};
const ativ = { nome: null, t0: 0, dur: 0, fim: 0 };
let palcoTela = null, pctx = null;
@@ -582,6 +596,38 @@
const p = pincel(k); info.x = 16 + lado * 5 + p.x * 4; info.y = -1 + p.y * 5;
} else if (ativ.nome === 'malhar') {
const f = forca(k); info.x = lado * 14; info.y = 6 - f * 12; info.ab = f > 0.85 ? 0.55 : 1;
+ } else if (ativ.nome === 'dormir') {
+ info.forma = 'dormir'; info.boca = 'sono'; info.x = lado * 6; info.y = 4;
+ } else if (ativ.nome === 'cafe') {
+ const f = (k % 5) / 5, gole = f > 0.55 && f < 0.92;
+ info.x = lado * 12; info.y = gole ? -3 : 3; if (gole) info.forma = 'feliz';
+ } else if (ativ.nome === 'cozinhar') {
+ const cc = (k % 2.6) / 2.6, h = cc < 0.55 ? 4 * (cc / 0.55) * (1 - cc / 0.55) : 0;
+ info.x = lado * 16; info.y = 4 - h * 14;
+ } else if (ativ.nome === 'regar') {
+ info.x = lado * 18; info.y = 8;
+ } else if (ativ.nome === 'violao') {
+ info.x = lado * 8 + 3 * Math.sin(k * 1.1); info.y = 6 + 1.5 * Math.sin(k * 2.2); info.boca = 'sorriso';
+ if (Math.sin(k * 0.7) > 0.3) info.forma = 'feliz';
+ } else if (ativ.nome === 'dancar') {
+ info.x = lado * 10 + 6 * Math.sin(k * 5); info.y = -2 + 4 * Math.abs(Math.sin(k * 5)); info.forma = 'feliz'; info.boca = 'sorriso';
+ } else if (ativ.nome === 'corda') {
+ const th = k * Math.PI * 2 * 0.85; info.x = lado * 12; info.y = 4 - Math.pow(Math.max(0, Math.cos(th)), 0.6) * 10;
+ } else if (ativ.nome === 'meditar') {
+ info.forma = 'dormir'; info.boca = 'sorriso'; info.x = lado * 6; info.y = -2 + 2 * Math.sin(k * 1.2);
+ } else if (ativ.nome === 'telescopio') {
+ info.x = lado * 16; info.y = -11;
+ } else if (ativ.nome === 'pipa') {
+ info.x = lado * 14 + 4 * Math.sin(k * 0.8); info.y = -11 + 2 * Math.cos(k * 1.1);
+ } else if (ativ.nome === 'bolhas') {
+ info.x = lado * 14 + 4 * Math.sin(k * 0.9); info.y = -8 + 3 * Math.sin(k * 0.6);
+ } else if (ativ.nome === 'skate') {
+ info.x = lado * 12 + 7 * Math.sin(k * 0.9); info.y = 6;
+ } else if (ativ.nome === 'computador') {
+ info.x = lado * 18 + Math.sin(k * 20) * 0.6; info.y = 2 + ((k * 2.5) % 1) * 1.5;
+ } else if (ativ.nome === 'pescar') {
+ const cc = k % 8; info.x = lado * 18; info.y = 7;
+ if (cc > 6.2 && cc < 7.6) { info.forma = 'feliz'; info.boca = 'sorriso'; info.y = 0; }
}
return info;
}
@@ -618,8 +664,9 @@
caixa(c, -s * 0.66, -s * 0.98, s * 1.32, s * 0.98, s * 0.28); c.fill(); c.stroke();
const ox = (p.olhoX || 0) * s * 0.12, oy = (p.olhoY || 0) * s * 0.1;
const pisca = (k % 3.7) < 0.12 ? 0.15 : 1;
- c.save(); c.fillStyle = css(cor);
+ c.save(); c.fillStyle = css(cor); c.strokeStyle = css(cor);
for (const lado of [-1, 1]) {
+ if (p.fechados) { c.lineWidth = Math.max(1.5, s * 0.07); c.beginPath(); c.arc(lado * s * 0.27, -s * 0.52, s * 0.12, 0.2, Math.PI - 0.2); c.stroke(); continue; }
c.beginPath(); c.ellipse(lado * s * 0.27 + ox, -s * 0.5 + oy, s * 0.11, s * 0.16 * pisca, 0, 0, Math.PI * 2); c.fill();
}
c.restore(); c.restore();
@@ -644,6 +691,56 @@
c.beginPath(); c.moveTo(x + s * 0.2, y - s * 0.7); c.quadraticCurveTo(x + s * 0.5, y - s * 0.55, x + s * 0.42, y - s * 0.3); c.stroke();
}
+ const ROSA_ = [255, 112, 176], AMARELO_ = [255, 218, 96], VERDE_ = [120, 255, 160], LARANJA_ = [255, 150, 70];
+ const SENTADO = { sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35] };
+ const rnd = (i, sem) => { const a = Math.sin(i * 12.9898 + sem * 78.233) * 43758.5453; return a - Math.floor(a); };
+ function estrela(c, x, y, r) {
+ c.beginPath();
+ for (let i = 0; i < 10; i++) { const a = -Math.PI / 2 + i * Math.PI / 5, rr = i % 2 ? r * 0.45 : r; c.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr); }
+ c.closePath(); c.fill();
+ }
+ function vapor(c, x, y, s, k, cor) { // três fios subindo e ondulando
+ c.save(); c.strokeStyle = css(cor, 0.7); c.lineWidth = Math.max(1, s * 0.04);
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 3; i++) {
+ const f = (k * 0.5 + i / 3) % 1, bx = x + (i - 1) * s * 0.1, by = y - f * s * 0.7;
+ c.globalAlpha = alfa * (1 - f) * Math.min(1, f / 0.2);
+ c.beginPath(); c.moveTo(bx, by); c.bezierCurveTo(bx + s * 0.1, by - s * 0.12, bx - s * 0.1, by - s * 0.24, bx, by - s * 0.36); c.stroke();
+ }
+ c.restore();
+ }
+ function xicara(c, x, y, s, cor) {
+ c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.moveTo(x - s * 0.18, y - s * 0.2); c.lineTo(x + s * 0.18, y - s * 0.2); c.lineTo(x + s * 0.14, y + s * 0.08); c.lineTo(x - s * 0.14, y + s * 0.08); c.closePath(); c.fill(); c.stroke();
+ c.beginPath(); c.arc(x + s * 0.2, y - s * 0.07, s * 0.08, -1.4, 1.4); c.stroke();
+ }
+ function chama(c, x, y, alt, s) {
+ c.fillStyle = css(LARANJA_);
+ c.beginPath(); c.moveTo(x, y - alt); c.quadraticCurveTo(x + s * 0.13, y - alt * 0.3, x, y); c.quadraticCurveTo(x - s * 0.13, y - alt * 0.3, x, y - alt); c.fill();
+ c.fillStyle = css(AMARELO_);
+ c.beginPath(); c.moveTo(x, y - alt * 0.6); c.quadraticCurveTo(x + s * 0.06, y - alt * 0.2, x, y); c.quadraticCurveTo(x - s * 0.06, y - alt * 0.2, x, y - alt * 0.6); c.fill();
+ }
+ function zzz(c, x, y, s, k, cor) { // Zs subindo
+ c.save(); c.strokeStyle = css(cor); c.lineCap = 'round'; c.lineJoin = 'round';
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 3; i++) {
+ const f = (k * 0.3 + i / 3) % 1, tam = s * (0.18 + 0.2 * f), zx = x - s * 0.5 * f, zy = y - s * 1.3 * f;
+ c.globalAlpha = alfa * Math.min(1, f / 0.15) * (1 - f); c.lineWidth = Math.max(1.5, tam * 0.2);
+ c.beginPath(); c.moveTo(zx - tam / 2, zy - tam / 2); c.lineTo(zx + tam / 2, zy - tam / 2); c.lineTo(zx - tam / 2, zy + tam / 2); c.lineTo(zx + tam / 2, zy + tam / 2); c.stroke();
+ }
+ c.restore();
+ }
+ function notas(c, x, y, s, k, cor, dir) {
+ c.fillStyle = css(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 3; i++) {
+ const f = (k * 0.45 + i / 3) % 1;
+ c.globalAlpha = alfa * Math.min(1, f / 0.15) * (1 - f);
+ nota(c, x + dir * s * (0.3 + 0.5 * i) + Math.sin(f * 6 + i) * s * 0.2, y - f * s * 1.6, s * 0.7);
+ }
+ c.globalAlpha = alfa;
+ }
+
const CENAS = {
ler(c, x, y, s, k, cor) {
const b = boneco(c, x, y, s, {
@@ -786,6 +883,321 @@
c.beginPath(); c.arc(b.cabeca.x + s * 0.72, b.cabeca.y + s * 0.3 + g * s * 0.5, s * 0.07, 0, Math.PI * 2); c.fill();
}
},
+ dormir(c, x, y, s, k, cor) {
+ // cama com cabeceira, travesseiro e cobertor que sobe e desce com a respiração
+ const bx0 = x - s * 1.7, bx1 = x + s * 1.9, topo = y - s * 0.75;
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07);
+ c.beginPath(); c.moveTo(bx0 + s * 0.12, y); c.lineTo(bx0 + s * 0.12, topo); c.moveTo(bx1 - s * 0.12, y); c.lineTo(bx1 - s * 0.12, topo); c.stroke();
+ caixa(c, bx0 - s * 0.08, topo - s * 1.25, s * 0.24, s * 1.3, s * 0.1); c.fill(); c.stroke();
+ caixa(c, bx0, topo - s * 0.3, bx1 - bx0, s * 0.32, s * 0.12); c.fill(); c.stroke();
+ c.fillStyle = claro(cor, 0.9);
+ c.beginPath(); c.ellipse(bx0 + s * 0.85, topo - s * 0.5, s * 0.6, s * 0.24, 0, 0, Math.PI * 2); c.fill(); c.stroke();
+ // bonequinho deitado: desenhado em pé e girado 90°, pés à direita, cabeça no travesseiro
+ const e = 0.72, linha = topo - s * 0.3 - s * e * 0.5;
+ c.save(); c.translate(x + s * 1.5, linha); c.rotate(-Math.PI / 2);
+ boneco(c, 0, 0, s * e, { fechados: true, bE: [-0.1, 0], bD: [0.1, 0] }, cor, k);
+ c.restore();
+ const resp = Math.sin(k * 1.4) * s * 0.04;
+ c.fillStyle = css(cor.map((v) => v * 0.45)); c.strokeStyle = css(cor);
+ caixa(c, x - s * 0.3, linha - s * 0.42 - resp, s * 2.15, topo - s * 0.3 - (linha - s * 0.42 - resp) + s * 0.06, s * 0.18); c.fill(); c.stroke();
+ c.strokeStyle = css(cor, 0.5); c.beginPath(); c.moveTo(x - s * 0.1, linha - s * 0.2 - resp); c.lineTo(x + s * 1.7, linha - s * 0.2 - resp); c.stroke();
+ zzz(c, x - s * 0.9, linha - s * 0.9, s, k, cor);
+ },
+ cafe(c, x, y, s, k, cor) {
+ const f = (k % 5) / 5, gole = suave(faixaDe(f, 0.5, 0.64)) * (1 - suave(faixaDe(f, 0.84, 0.97)));
+ const b = boneco(c, x, y, s, Object.assign({}, SENTADO, {
+ olhoX: 0.2, olhoY: 0.3 - gole * 0.8, fechados: gole > 0.85, cabeca: -0.04,
+ bE: [0.15, 0.95], bD: [-0.15 + gole * 1.75, -0.95 - gole * 3.05],
+ }), cor, k);
+ xicara(c, b.mD.x, b.mD.y, s, cor);
+ if (gole < 0.3) vapor(c, b.mD.x, b.mD.y - s * 0.3, s, k, cor);
+ },
+ cozinhar(c, x, y, s, k, cor) {
+ const ciclo = (k % 2.6) / 2.6, voo = ciclo < 0.55 ? ciclo / 0.55 : -1, h = voo >= 0 ? 4 * voo * (1 - voo) : 0;
+ const fx = x + s * 1.0, fw = s * 1.7, fh = s * 1.2;
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07);
+ caixa(c, fx, y - fh, fw, fh, s * 0.08); c.fill(); c.stroke();
+ caixa(c, fx + fw * 0.15, y - fh * 0.72, fw * 0.7, fh * 0.5, s * 0.06); c.stroke();
+ for (let i = 0; i < 3; i++) chama(c, fx + fw * (0.3 + 0.2 * i), y - fh, s * (0.22 + 0.07 * Math.sin(k * 17 + i * 2)), s);
+ const b = boneco(c, x - s * 0.45, y, s, {
+ olhoX: 1, olhoY: 0.3 - h, cabeca: -0.05, bE: [-0.4, 0.3],
+ bD: [1.2 - (voo >= 0 && voo < 0.15 ? 0.25 : 0), 0.35],
+ }, cor, k);
+ const px = b.mD.x + s * 0.62, py = b.mD.y + s * 0.02;
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(2, s * 0.09);
+ c.beginPath(); c.moveTo(b.mD.x, b.mD.y); c.lineTo(px - s * 0.42, py); c.stroke();
+ c.fillStyle = escuro(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ c.beginPath(); c.ellipse(px, py, s * 0.46, s * 0.13, 0, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.save(); c.translate(px, py - s * 0.1 - h * s * 1.6); c.rotate(voo >= 0 ? voo * Math.PI * 2 : 0);
+ c.fillStyle = css(AMARELO_); c.beginPath(); c.ellipse(0, 0, s * 0.32, s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.restore();
+ },
+ regar(c, x, y, s, k, cor) {
+ const cresce = Math.min(1, k / 18), px = x + s * 2.0;
+ // vaso
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07);
+ c.beginPath(); c.moveTo(px - s * 0.42, y - s * 0.6); c.lineTo(px + s * 0.42, y - s * 0.6); c.lineTo(px + s * 0.3, y); c.lineTo(px - s * 0.3, y); c.closePath(); c.fill(); c.stroke();
+ // planta crescendo: caule, folhas e, no fim, a flor
+ const alt = s * (0.3 + 1.25 * cresce), base = y - s * 0.6, balanca = Math.sin(k * 1.5) * s * 0.06;
+ c.strokeStyle = css(VERDE_); c.lineWidth = Math.max(1.5, s * 0.07);
+ c.beginPath(); c.moveTo(px, base); c.quadraticCurveTo(px + balanca, base - alt / 2, px + balanca * 2, base - alt); c.stroke();
+ c.fillStyle = css(VERDE_);
+ for (let i = 1; i <= 3; i++) {
+ if (cresce < i * 0.22) continue;
+ const yy = base - alt * i * 0.25, lado = i % 2 ? 1 : -1;
+ c.beginPath(); c.ellipse(px + balanca * i * 0.5 + lado * s * 0.18, yy, s * 0.2, s * 0.08, lado * -0.5, 0, Math.PI * 2); c.fill();
+ }
+ if (cresce > 0.75) {
+ const f = (cresce - 0.75) / 0.25, fx = px + balanca * 2, fy = base - alt;
+ c.fillStyle = css(ROSA_);
+ for (let i = 0; i < 5; i++) { const a = i * 1.2566 + k * 0.3; c.beginPath(); c.arc(fx + Math.cos(a) * s * 0.16 * f, fy + Math.sin(a) * s * 0.16 * f, s * 0.11 * f, 0, Math.PI * 2); c.fill(); }
+ c.fillStyle = css(AMARELO_); c.beginPath(); c.arc(fx, fy, s * 0.09 * f, 0, Math.PI * 2); c.fill();
+ }
+ const b = boneco(c, x - s * 0.7, y, s, { olhoX: 1, olhoY: 0.8, cabeca: -0.1, inclina: -0.06, bD: [1.35, 0.35], bE: [-0.3, 0.2] }, cor, k);
+ // regador inclinado e as gotas caindo na planta
+ const ang = 0.38, rx = b.mD.x + s * 0.05, ry = b.mD.y;
+ c.save(); c.translate(rx, ry); c.rotate(ang);
+ c.fillStyle = claro(cor, 0.95); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ caixa(c, 0, -s * 0.22, s * 0.6, s * 0.42, s * 0.08); c.fill(); c.stroke();
+ c.beginPath(); c.moveTo(s * 0.55, -s * 0.05); c.lineTo(s * 1.0, -s * 0.32); c.stroke();
+ c.restore();
+ const ex = rx + Math.cos(ang) * s * 1.0 + Math.sin(ang) * s * 0.32, ey = ry + Math.sin(ang) * s * 1.0 - Math.cos(ang) * s * 0.32;
+ c.fillStyle = claro(cor, 1);
+ for (let i = 0; i < 6; i++) {
+ const f = (k * 1.6 + i / 6) % 1;
+ c.beginPath(); c.ellipse(ex + (px - ex) * f * 0.8, ey + (base - alt - ey + s * 0.2) * f * f, s * 0.035, s * 0.07, 0, 0, Math.PI * 2); c.fill();
+ }
+ },
+ violao(c, x, y, s, k, cor) {
+ const toque = Math.sin(k * 11);
+ const b = boneco(c, x, y, s, Object.assign({}, SENTADO, {
+ olhoX: -0.4, olhoY: 0.7, cabeca: 0.08 * Math.sin(k * 1.1),
+ bE: [-1.1, -0.9], bD: [-0.2 + toque * 0.12, -1.0],
+ }), cor, k);
+ // violão atravessado no colo: corpo perto da mão que toca, braço até a outra mão
+ const gx = b.quadril.x + s * 0.3, gy = b.quadril.y - s * 0.35, nx = b.mE.x, ny = b.mE.y;
+ const a = Math.atan2(ny - gy, nx - gx);
+ c.save(); c.translate(gx, gy); c.rotate(a);
+ const L = Math.hypot(nx - gx, ny - gy);
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ caixa(c, s * 0.2, -s * 0.08, L - s * 0.1, s * 0.16, s * 0.04); c.fill(); c.stroke();
+ caixa(c, L - s * 0.05, -s * 0.12, s * 0.3, s * 0.24, s * 0.06); c.fill(); c.stroke();
+ c.fillStyle = claro(cor, 0.95);
+ c.beginPath(); c.arc(-s * 0.28, 0, s * 0.42, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.beginPath(); c.arc(s * 0.22, 0, s * 0.32, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.fillStyle = escuro(cor); c.beginPath(); c.arc(s * 0.05, 0, s * 0.11, 0, Math.PI * 2); c.fill();
+ c.strokeStyle = css(cor, 0.7); c.lineWidth = Math.max(1, s * 0.015);
+ for (const d of [-0.04, 0, 0.04]) { c.beginPath(); c.moveTo(-s * 0.5, d * s); c.lineTo(L, d * s); c.stroke(); }
+ c.restore();
+ c.fillStyle = css(cor); c.beginPath(); c.arc(b.mD.x, b.mD.y, s * 0.13, 0, Math.PI * 2); c.fill();
+ notas(c, b.cabeca.x - s * 1.6, b.cabeca.y + s * 0.2, s, k, cor, -1);
+ },
+ dancar(c, x, y, s, k, cor) {
+ const u = (Math.sin(k * 5) + 1) / 2, mexe = Math.sin(k * 5) * 0.18;
+ // globo espelhado e luzinhas no chão
+ const gx = x, gy = y - s * 4.5;
+ c.strokeStyle = css(cor, 0.6); c.lineWidth = Math.max(1, s * 0.03);
+ c.beginPath(); c.moveTo(gx, gy - s * 0.9); c.lineTo(gx, gy - s * 0.35); c.stroke();
+ c.fillStyle = claro(cor, 0.9); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.arc(gx, gy, s * 0.35, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.strokeStyle = css(cor, 0.5);
+ for (let i = -1; i <= 1; i++) { c.beginPath(); c.ellipse(gx, gy, s * 0.35 * Math.abs(Math.cos(k + i)), s * 0.35, 0, 0, Math.PI * 2); c.stroke(); }
+ const cores = [ROSA_, AMARELO_, VERDE_, cor];
+ for (let i = 0; i < 6; i++) {
+ const lx = x + Math.sin(k * 1.3 + i * 1.7) * s * 2.4, alfa = c.globalAlpha;
+ c.globalAlpha = alfa * 0.35; c.fillStyle = css(cores[i % 4]);
+ c.beginPath(); c.ellipse(lx, y, s * 0.35, s * 0.07, 0, 0, Math.PI * 2); c.fill();
+ c.globalAlpha = alfa;
+ }
+ boneco(c, x, y, s, {
+ dx: mexe, inclina: mexe * 0.5, cabeca: -mexe * 0.6, olhoY: -0.2, olhoX: mexe * 3,
+ bE: [-0.7 - 2.1 * u, 0.5], bD: [0.7 + 2.1 * (1 - u), -0.5],
+ pE: [-0.1 - u * 0.25, u * 0.4], pD: [0.1 + (1 - u) * 0.25, -(1 - u) * 0.4],
+ }, cor, k);
+ for (let i = 0; i < 4; i++) {
+ const f = (k * 0.7 + i / 4) % 1, alfa = c.globalAlpha;
+ c.globalAlpha = alfa * Math.sin(f * Math.PI); c.fillStyle = css(cores[i]);
+ estrela(c, x + Math.cos(i * 1.9) * s * 1.6, y - s * (2 + i * 0.5) - f * s * 0.4, s * 0.12); c.globalAlpha = alfa;
+ }
+ },
+ corda(c, x, y, s, k, cor) {
+ const th = k * Math.PI * 2 * 0.85, pulo = Math.pow(Math.max(0, Math.cos(th)), 0.6), sobe = pulo * s * 0.45;
+ c.save(); c.globalAlpha *= 0.3 * (1 - pulo * 0.5); c.fillStyle = css(cor);
+ c.beginPath(); c.ellipse(x, y, s * 0.6 * (1 - pulo * 0.3), s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.restore();
+ const b = boneco(c, x, y - sobe, s, {
+ olhoY: -0.2, bE: [-0.75 + 0.15 * Math.sin(th), 0.6], bD: [0.75 - 0.15 * Math.sin(th), -0.6],
+ pE: [-0.06, pulo * 0.3], pD: [0.06, -pulo * 0.3],
+ }, cor, k);
+ // a corda passa por baixo dos pés quando ele está no alto do pulo
+ const my = (b.mE.y + b.mD.y) / 2, embaixo = 2 * (y + s * 0.05) - my, emcima = 2 * (b.cabeca.y - s * 1.0) - my;
+ const cy = (embaixo + emcima) / 2 + Math.cos(th) * (embaixo - emcima) / 2;
+ c.strokeStyle = claro(cor, 1); c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.moveTo(b.mE.x, b.mE.y); c.quadraticCurveTo(x, cy, b.mD.x, b.mD.y); c.stroke();
+ },
+ meditar(c, x, y, s, k, cor) {
+ const flutua = (Math.sin(k * 1.2) + 1) / 2 * s * 0.35, cy = y - s * 1.7 - flutua;
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 3; i++) { // aura
+ const f = (k * 0.35 + i / 3) % 1;
+ c.globalAlpha = alfa * 0.35 * (1 - f); c.strokeStyle = css(cor); c.lineWidth = Math.max(1, s * 0.04);
+ c.beginPath(); c.ellipse(x, cy, s * (1.2 + f * 1.3), s * (1.4 + f * 1.3), 0, 0, Math.PI * 2); c.stroke();
+ }
+ c.globalAlpha = alfa * 0.3; c.fillStyle = css(cor);
+ c.beginPath(); c.ellipse(x, y, s * 0.8 * (1 - flutua / s), s * 0.09, 0, 0, Math.PI * 2); c.fill();
+ c.globalAlpha = alfa;
+ boneco(c, x, y - s * 0.3 - flutua, s, {
+ sentado: true, fechados: true, pE: [-1.45, 2.75], pD: [1.45, -2.75], bE: [-0.75, 0.25], bD: [0.75, -0.25],
+ }, cor, k);
+ },
+ telescopio(c, x, y, s, k, cor) {
+ // céu estrelado com uma estrela cadente de vez em quando
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 16; i++) {
+ c.globalAlpha = alfa * (0.35 + 0.65 * Math.abs(Math.sin(k * (1 + rnd(i, 3)) + i)));
+ c.fillStyle = i % 5 ? claro(cor, 1) : css(AMARELO_);
+ estrela(c, x + s * (0.2 + rnd(i, 1) * 3.4), y - s * (3.4 + rnd(i, 2) * 1.5), s * (0.05 + 0.05 * rnd(i, 4)));
+ }
+ const f = (k % 6) / 1.1;
+ if (f < 1) {
+ const sx = x + s * (3.6 - f * 2.2), sy = y - s * (4.9 - f * 0.8);
+ const g = c.createLinearGradient(sx, sy, sx + s * 0.9, sy - s * 0.33);
+ g.addColorStop(0, claro(cor, 1)); g.addColorStop(1, css(cor, 0));
+ c.globalAlpha = alfa * (1 - f); c.strokeStyle = g; c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.moveTo(sx, sy); c.lineTo(sx + s * 0.9, sy - s * 0.33); c.stroke();
+ }
+ c.globalAlpha = alfa;
+ const b = boneco(c, x - s * 0.7, y, s, { olhoX: 1, olhoY: -1, cabeca: 0.15, bD: [2.1, 0.5], bE: [-0.3, 0.2] }, cor, k);
+ const ox = b.cabeca.x + s * 0.78, oy = b.cabeca.y + s * 0.3, tx = ox + s * 1.6, ty = oy - s * 0.95;
+ const mx = (ox + tx) / 2, my = (oy + ty) / 2;
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ c.beginPath(); c.moveTo(mx, my); c.lineTo(mx - s * 0.5, y); c.moveTo(mx, my); c.lineTo(mx + s * 0.5, y); c.moveTo(mx, my); c.lineTo(mx, y); c.stroke();
+ c.fillStyle = escuro(cor);
+ c.beginPath(); capsula(c, ox, oy, tx, ty, s * 0.15); c.fill(); c.stroke();
+ c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(tx, ty, s * 0.17, 0, Math.PI * 2); c.fill(); c.stroke();
+ },
+ pipa(c, x, y, s, k, cor) {
+ const kx = x + s * (2.3 + 0.45 * Math.sin(k * 0.8)), ky = y - s * (4.2 + 0.35 * Math.cos(k * 1.1)), ang = 0.25 * Math.sin(k * 1.6);
+ const b = boneco(c, x - s * 0.9, y, s, { olhoX: 1, olhoY: -1, cabeca: 0.12, bD: [2.55, 0.35], bE: [-0.4, 0.3] }, cor, k);
+ c.strokeStyle = css(cor, 0.8); c.lineWidth = Math.max(1, s * 0.025);
+ c.beginPath(); c.moveTo(b.mD.x, b.mD.y);
+ c.quadraticCurveTo((b.mD.x + kx) / 2 + s * 0.4, (b.mD.y + ky) / 2 + s * 0.7, kx, ky + s * 0.45); c.stroke();
+ // rabiola com lacinhos
+ c.save(); c.translate(kx, ky); c.rotate(ang);
+ c.strokeStyle = css(cor, 0.8); c.beginPath(); c.moveTo(0, s * 0.5);
+ for (let i = 1; i <= 6; i++) c.lineTo(Math.sin(k * 3 + i) * s * 0.18, s * (0.5 + i * 0.22));
+ c.stroke();
+ for (let i = 2; i <= 6; i += 2) {
+ const lx = Math.sin(k * 3 + i) * s * 0.18, ly = s * (0.5 + i * 0.22);
+ c.fillStyle = css(i % 4 ? ROSA_ : AMARELO_);
+ c.beginPath(); c.moveTo(lx, ly); c.lineTo(lx - s * 0.1, ly - s * 0.06); c.lineTo(lx - s * 0.1, ly + s * 0.06); c.closePath(); c.fill();
+ c.beginPath(); c.moveTo(lx, ly); c.lineTo(lx + s * 0.1, ly - s * 0.06); c.lineTo(lx + s * 0.1, ly + s * 0.06); c.closePath(); c.fill();
+ }
+ const pts = [[0, -0.5], [0.38, 0], [0, 0.5], [-0.38, 0]];
+ const cores = [ROSA_, AMARELO_, cor, VERDE_];
+ for (let i = 0; i < 4; i++) {
+ const [ax, ay] = pts[i], [bx, by] = pts[(i + 1) % 4];
+ c.fillStyle = css(cores[i]); c.beginPath(); c.moveTo(0, 0); c.lineTo(ax * s, ay * s); c.lineTo(bx * s, by * s); c.closePath(); c.fill();
+ }
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.04);
+ c.beginPath(); pts.forEach(([px, py], i) => (i ? c.lineTo(px * s, py * s) : c.moveTo(px * s, py * s))); c.closePath(); c.stroke();
+ c.restore();
+ },
+ bolhas(c, x, y, s, k, cor) {
+ const b = boneco(c, x - s * 0.6, y, s, { olhoX: 0.8, olhoY: -0.6, bD: [1.6, -4.0], bE: [-0.3, 0.2] }, cor, k);
+ const wx = b.mD.x + s * 0.35, wy = b.mD.y - s * 0.25;
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.moveTo(b.mD.x, b.mD.y); c.lineTo(wx - s * 0.1, wy + s * 0.08); c.stroke();
+ c.beginPath(); c.arc(wx, wy, s * 0.12, 0, Math.PI * 2); c.stroke();
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 9; i++) {
+ const f = (k * 0.22 + i / 9) % 1, r = s * (0.12 + 0.18 * rnd(i, 4));
+ const bx = wx + f * s * (1.6 + 1.4 * rnd(i, 5)) + Math.sin(k * 2 + i) * s * 0.15, by = wy - f * s * (1.6 + rnd(i, 6));
+ c.globalAlpha = alfa * Math.min(1, f / 0.08) * (f > 0.9 ? (1 - f) / 0.1 : 1);
+ c.fillStyle = css(cor, 0.1); c.strokeStyle = css(cor, 0.9); c.lineWidth = Math.max(1, s * 0.03);
+ c.beginPath(); c.arc(bx, by, r, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.strokeStyle = claro(cor, 1); c.beginPath(); c.arc(bx, by, r * 0.65, -2.4, -1.6); c.stroke();
+ }
+ c.globalAlpha = alfa;
+ },
+ skate(c, x, y, s, k, cor) {
+ const pos = Math.sin(k * 0.9) * 1.3, vel = Math.cos(k * 0.9), ciclo = k % 4;
+ const ollie = ciclo < 0.7 ? Math.sin(ciclo / 0.7 * Math.PI) : 0, sobe = ollie * s * 0.6;
+ const bx = x + pos * s, pe = y - s * 0.3 - sobe;
+ c.save(); c.globalAlpha *= 0.3; c.fillStyle = css(cor);
+ c.beginPath(); c.ellipse(bx, y, s * 0.9, s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.restore();
+ c.strokeStyle = css(cor, 0.4); c.lineWidth = Math.max(1, s * 0.03); // risquinhos de velocidade
+ for (let i = 0; i < 3; i++) { const lx = bx - Math.sign(vel) * s * (1.1 + i * 0.25); c.beginPath(); c.moveTo(lx, pe - s * (0.6 + i * 0.4)); c.lineTo(lx - Math.sign(vel) * s * 0.5 * Math.abs(vel), pe - s * (0.6 + i * 0.4)); c.stroke(); }
+ boneco(c, bx, pe, s, {
+ olhoX: vel * 0.8, inclina: -vel * 0.1, bE: [-1.35, 0.25 + 0.2 * Math.sin(k * 3)], bD: [1.35, -0.25 - 0.2 * Math.sin(k * 3)],
+ pE: [-0.25, 0.35], pD: [0.25, -0.35],
+ }, cor, k);
+ c.save(); c.translate(bx, pe); c.rotate(-ollie * 0.25);
+ c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ caixa(c, -s * 0.85, 0, s * 1.7, s * 0.12, s * 0.06); c.fill(); c.stroke();
+ c.fillStyle = escuro(cor);
+ for (const lado of [-1, 1]) { c.beginPath(); c.arc(lado * s * 0.55, s * 0.2, s * 0.1, 0, Math.PI * 2); c.fill(); c.stroke(); }
+ c.restore();
+ },
+ computador(c, x, y, s, k, cor) {
+ const mx0 = x + s * 0.3, mx1 = x + s * 2.7, mt = y - s * 1.45;
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ c.beginPath(); c.moveTo(mx0 + s * 0.15, mt); c.lineTo(mx0 + s * 0.15, y); c.moveTo(mx1 - s * 0.15, mt); c.lineTo(mx1 - s * 0.15, y); c.stroke();
+ caixa(c, mx0, mt, mx1 - mx0, s * 0.12, s * 0.04); c.fill(); c.stroke();
+ const nx = mx0 + s * 0.6, nw = s * 1.4, nh = s * 0.95, ny = mt - s * 0.08 - nh;
+ caixa(c, nx - s * 0.12, mt - s * 0.08, nw + s * 0.24, s * 0.08, s * 0.03); c.fill(); c.stroke();
+ caixa(c, nx, ny, nw, nh, s * 0.06); c.fill(); c.stroke();
+ c.save(); caixa(c, nx + s * 0.06, ny + s * 0.06, nw - s * 0.12, nh - s * 0.12, s * 0.03); c.clip();
+ const linhas = Math.floor(k * 2.5), visiveis = 6, ini = Math.max(0, linhas - visiveis);
+ const tintas = [cor, VERDE_, AMARELO_, ROSA_];
+ for (let i = ini; i < linhas; i++) {
+ const yy = ny + s * 0.14 + (i - ini) * s * 0.13, recuo = (rnd(i, 7) < 0.4 ? 1 : 0) * s * 0.15;
+ c.fillStyle = css(tintas[i % 4], 0.85); c.fillRect(nx + s * 0.12 + recuo, yy, (nw - s * 0.4) * (0.3 + 0.6 * rnd(i, 8)), s * 0.05);
+ }
+ if (Math.floor(k * 2) % 2) { c.fillStyle = claro(cor, 1); c.fillRect(nx + s * 0.12, ny + s * 0.14 + Math.min(linhas, visiveis) * s * 0.13, s * 0.04, s * 0.08); }
+ c.restore();
+ const tecla = Math.sin(k * 20) * 0.05;
+ boneco(c, x - s * 0.55, y, s, Object.assign({}, SENTADO, {
+ olhoX: 1, olhoY: 0.2, cabeca: -0.05, bE: [0.9 + tecla, 0.9], bD: [1.35 - tecla, 0.45],
+ }), cor, k);
+ },
+ pescar(c, x, y, s, k, cor) {
+ const ciclo = k % 8, fisgou = ciclo > 6.2 && ciclo < 7.6, puxa = fisgou ? suave((ciclo - 6.2) / 0.5) : 0;
+ // água: ondinhas à direita do píer
+ c.strokeStyle = css(cor, 0.55); c.lineWidth = Math.max(1, s * 0.035);
+ for (let fila = 0; fila < 3; fila++) {
+ c.beginPath();
+ for (let i = 0; i <= 24; i++) {
+ const wx = x + s * (0.5 + i * 0.14), wy = y - s * 0.05 + fila * s * 0.16 + Math.sin(k * 2 + i * 0.8 + fila) * s * 0.04;
+ if (i) c.lineTo(wx, wy); else c.moveTo(wx, wy);
+ }
+ c.stroke();
+ }
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ caixa(c, x - s * 2.0, y - s * 0.22, s * 2.5, s * 0.22, s * 0.05); c.fill(); c.stroke();
+ const b = boneco(c, x - s * 0.8, y - s * 0.22, s, Object.assign({}, SENTADO, {
+ olhoX: 1, olhoY: 0.6 - puxa * 1.4, bD: [2.0 - puxa * 0.4, -0.9], bE: [0.5, 1.2],
+ }), cor, k);
+ const vx = b.mD.x + s * (1.4 - puxa * 0.3), vy = b.mD.y - s * (1.6 + puxa * 0.3);
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ c.beginPath(); c.moveTo(b.mE.x, b.mE.y); c.lineTo(b.mD.x, b.mD.y); c.lineTo(vx, vy); c.stroke();
+ const belisca = ciclo > 5.5 && ciclo < 6.2 ? Math.sin(k * 30) * s * 0.06 : 0;
+ let lx = x + s * 2.6, ly = y - s * 0.05 + Math.sin(k * 2) * s * 0.04 + belisca;
+ if (fisgou) { // o peixe pula preso na linha
+ lx = x + s * (2.6 - puxa * 0.6); ly = y - s * (0.2 + Math.sin(puxa * Math.PI) * 1.3 + puxa * 0.3);
+ c.save(); c.translate(lx, ly + s * 0.25); c.rotate(Math.sin(k * 14) * 0.4);
+ c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.04);
+ c.beginPath(); c.ellipse(0, 0, s * 0.12, s * 0.28, 0, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.beginPath(); c.moveTo(0, s * 0.25); c.lineTo(-s * 0.14, s * 0.45); c.lineTo(s * 0.14, s * 0.45); c.closePath(); c.fill(); c.stroke();
+ c.fillStyle = escuro(cor); c.beginPath(); c.arc(s * 0.04, -s * 0.12, s * 0.03, 0, Math.PI * 2); c.fill();
+ c.restore();
+ }
+ c.strokeStyle = css(cor, 0.85); c.lineWidth = Math.max(1, s * 0.02);
+ c.beginPath(); c.moveTo(vx, vy); c.quadraticCurveTo((vx + lx) / 2, Math.max(vy, ly) - s * (fisgou ? 0.1 : 0.4), lx, ly); c.stroke();
+ if (!fisgou) {
+ c.fillStyle = css(ROSA_); c.beginPath(); c.arc(lx, ly, s * 0.09, Math.PI, 0); c.fill();
+ c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(lx, ly, s * 0.09, 0, Math.PI); c.fill();
+ }
+ },
};
/** Desenha a cena da atividade (camada atrás dos olhos). o = { t, cor }. */
diff --git a/carinha.html b/carinha.html
index e2438e7..a9d9c6d 100644
--- a/carinha.html
+++ b/carinha.html
@@ -212,7 +212,7 @@ function caminhoBoca(tipo) {
// repouso: leve, com as microexpressões (canto que sobe, boca de lado, entreaberta)
const f = FEICAO.atual, w = l * 0.30, mx = cx + f.dx * l * 0.10;
const yE = cy - Math.max(0, f.lado) * l * 0.12, yD = cy - Math.max(0, -f.lado) * l * 0.12;
- const curva = l * (0.18 + f.curva * 0.35);
+ const curva = l * (0.18 + f.curva * 0.35 - ZANGA * 0.3);
let d = "M " + (mx - w) + "," + yE + " Q " + mx + "," + (cy + curva - f.abre * l * 0.12) + " " + (mx + w) + "," + yD;
if (f.abre > 0.04) d += " Q " + mx + "," + (cy + curva + f.abre * l * 0.30) + " " + (mx - w) + "," + yE;
return d;
@@ -224,7 +224,8 @@ function caminhoSobrancelha(i, esc) {
const ab = i === 0 ? r.ab : (r.ab2 === null ? r.ab : r.ab2);
const piscando = 1 - Math.min(1, Math.max(0, ab));
const y = c[1] - metadeOlho(esc) - (EXTRAS.distSobrancelha + 4) * ESC - (f.sob[i] - piscando * 3) * ESC;
- const inc = ((r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : 0) + f.inc[i]) * lado * ESC;
+ const base = r.forma === 'bravo' ? 18 : r.forma === 'triste' ? -14 : ZANGA * 14; // zanga franze a testa
+ const inc = (base + f.inc[i]) * lado * ESC;
const ele = r.esc > 1.1 ? -15 * ESC : 0;
return 'M ' + (c[0] - largura) + ',' + (y - inc + ele) + ' Q ' + c[0] + ',' + (y - 10 * ESC + ele) + ' ' + (c[0] + largura) + ',' + (y + inc + ele);
}
@@ -401,12 +402,17 @@ const LIVRES = ["feliz", "olhar_esq", "olhar_dir", "olhar_baixo",
const LIVRES_HUMOR = {
feliz: ["feliz", "alegre", "piscadinha", "coracao", "olhar_esq", "olhar_dir"],
tranquilo: ["olhar_esq", "olhar_dir", "feliz", "piscadinha", "olhar_baixo", "timido"],
+ bravo: ["bravo", "olhar_baixo", "olhar_esq", "olhar_dir", "confuso"],
entediado: ["olhar_esq", "olhar_dir", "olhar_baixo", "pensar", "bocejar", "confuso"],
carente: ["olhar_baixo", "triste", "olhar_esq", "olhar_dir", "timido"],
triste: ["triste", "olhar_baixo", "olhar_baixo"],
cansado: ["olhar_baixo", "piscar2", "bocejar"],
};
let HUMOR = "normal";
+/* Modo vida: zanga (0 a 1) deixa o rosto emburrado aos poucos quando a alegria cai; sono profundo =
+ dormiu de cansaço, e aí som, movimento e câmera não acordam (como na madrugada). */
+let ZANGA = 0;
+let SONO_PROFUNDO = false;
/* Microexpressões acordado: de vez em quando a boca e as sobrancelhas mexem sozinhas (um canto
da boca sobe, uma sobrancelha arqueia, um suspiro) e voltam devagar ao neutro. O humor do modo
@@ -431,6 +437,7 @@ const MICRO_HUMOR = {
carente: ["preocupado", "preocupado", "suspiro", "meioSorriso"],
triste: ["preocupado", "suspiro", "pensativo"],
cansado: ["suspiro", "suspiro", "ladinho"],
+ bravo: ["pensativo", "pensativo", "ladinho", "suspiro"],
normal: ["meioSorriso", "arquear", "sorrisinho", "ladinho", "interesse", "pensativo", "suspiro"],
};
function atualizarFeicao(s) {
@@ -495,11 +502,11 @@ function passo(dt) { // avanca a fila; true se ha gesto em
/* Atividade em andamento (rosto-motor.js): para onde olhar e que cara fazer. Atualizado por quadro. */
let ATIV = null;
-function atividade(nome) {
+function atividade(nome, segundos) {
if (!window.MotorRosto || !window.MotorRosto.atividade) return;
if (nome === "parar" || !nome) { window.MotorRosto.atividade(null); return; }
if (r.dormindo) { r.dormindo = false; animar("surpreso"); }
- if (window.MotorRosto.atividade(nome) && nome === "cantar") tocarSom("musica");
+ if (window.MotorRosto.atividade(nome, segundos) && nome === "cantar") tocarSom("musica");
}
function repouso(t) { // pose calma: olha devagar, "respira"
@@ -526,6 +533,8 @@ function repouso(t) { // pose calma: olha devagar, "respira
if (ATIV.forma) r.forma = ATIV.forma;
if (ATIV.boca) r.boca = ATIV.boca;
if (ATIV.ab) r.ab = ATIV.ab;
+ } else if (ZANGA >= 0.7) { // alegria caindo: emburrado (a cor escolhida fica)
+ r.forma = "bravo";
}
}
@@ -634,7 +643,13 @@ function trata(ordem) {
} else if (ordem.indexOf("ritmo ") === 0) {
PERFIL = PERFIS[ordem.slice(6).trim()] || PERFIS.normal;
} else if (ordem.indexOf("atividade ") === 0) {
- atividade(ordem.slice(10).trim());
+ const [nome, seg] = ordem.slice(10).trim().split(/\s+/); // "atividade bola 180"
+ atividade(nome, parseFloat(seg) || 0);
+ } else if (ordem.indexOf("zanga ") === 0) {
+ const z = parseFloat(ordem.slice(6));
+ if (!isNaN(z)) ZANGA = Math.max(0, Math.min(1, z));
+ } else if (ordem === "sono profundo" || ordem === "sono leve") {
+ SONO_PROFUNDO = ordem === "sono profundo";
} else if (ordem === "dormir" || ordem === "sono") {
r.dormindo = true;
atividade(null);
@@ -790,6 +805,7 @@ const MIC = {
/* Acordar com som: precisa de N barulhos em 1 minuto (ajuste "Sons para acordar"). Na madrugada
silenciosa só barulho ALTO conta, e movimento/câmera não acordam (alguém se mexendo na cama). */
const DESPERTAR = { sons: 3, noite: true, ini: 23, fim: 7 };
+function sonoRespeitado() { return madrugada() || SONO_PROFUNDO; }
function madrugada() {
if (!DESPERTAR.noite || DESPERTAR.ini === DESPERTAR.fim) return false;
const h = new Date().getHours();
@@ -855,7 +871,7 @@ function som(nivel) {
if (r.dormindo) {
const precisa = Math.max(1, Math.round(DESPERTAR.sons * PERFIL.acordarSom));
- if (contar(CONTA.somAcordar, madrugada() ? alto : temSom, agora, precisa, "ouvido: barulho")) { // acordou
+ if (contar(CONTA.somAcordar, sonoRespeitado() ? alto : temSom, agora, precisa, "ouvido: barulho")) { // acordou
r.dormindo = false;
animar("surpreso");
MIC.proxSurpresa = agora + MIC.cooldown;
@@ -878,6 +894,7 @@ function checarSilencio(agora) {
if (!MIC.ativo) return; // telinha do Pi: sem ouvido
if (agora - MIC.visto > 2) return; // o ouvido parou de mandar dado
if (r.dormindo) return;
+ if (HUMOR !== "normal") return; // modo vida ligado: quem manda dormir é a energia
// movimento tambem conta como "tem alguem aqui": nao adormece com gente passando
const atividade = Math.max(MIC.ultimoSom, MOV.ativo ? MOV.ultimoMov : 0);
if (agora - atividade >= MIC.segSilencio) {
@@ -949,7 +966,7 @@ function rastrear(t) {
MOV.olharY+=((t.y-.5)*2*MOV.olharMax*.6-MOV.olharY)*.65;
if(t.atividade===true) {
MOV.ultimoMov=agora;
- if(r.dormindo && !madrugada() && PERFIL !== PERFIS.calmo) { r.dormindo=false;animar('surpreso'); }
+ if(r.dormindo && !sonoRespeitado() && PERFIL !== PERFIS.calmo) { r.dormindo=false;animar('surpreso'); }
}
}
function movimento(vel, cx, cy) {
@@ -981,7 +998,7 @@ function movimento(vel, cx, cy) {
const forte = v >= limiar(MOV.limiarForte);
if (r.dormindo) {
// acordou: algo se mexeu na frente (de dia; calmo precisa de vários movimentos)
- if (!madrugada() && contar(CONTA.movAcordar, mexeu, agora, PERFIL.acordarMov, "presenca: movimento")) {
+ if (!sonoRespeitado() && contar(CONTA.movAcordar, mexeu, agora, PERFIL.acordarMov, "presenca: movimento")) {
r.dormindo = false;
animar("surpreso");
MOV.proxSurpresa = agora + MOV.cooldown;
diff --git a/kiosk-server.py b/kiosk-server.py
index 50f1cbc..3bfa3f6 100644
--- a/kiosk-server.py
+++ b/kiosk-server.py
@@ -57,7 +57,8 @@ GESTOS = ["piscar", "piscar2", "olhar_esq", "olhar_dir", "olhar_baixo", "feliz",
"esconder", "espiar", "acenar", "pensar", "musica",
"rir", "bocejar", "confuso", "tonto", "timido", "comemorar"]
# Atividades (momentos dele): o bonequinho faz algo embaixo dos olhos (rosto-motor.js).
-ATIVIDADES = ["ler", "cantar", "bola", "videogame", "pintar", "malhar"]
+ATIVIDADES = ["ler", "cantar", "bola", "videogame", "pintar", "malhar", "dormir", "cafe", "cozinhar", "regar",
+ "violao", "dancar", "corda", "meditar", "telescopio", "pipa", "bolhas", "skate", "computador", "pescar"]
# Ordens internas que o rosto entende mas não viram botão.
ORDENS_INTERNAS = {"calma"}
@@ -530,7 +531,7 @@ def main():
if tipo == "pir" and not madrugada(servidor.controle.config):
with servidor.controle.lock:
dormindo = bool(servidor.controle.estado.get("dormindo"))
- if dormindo and vida.pir_acorda():
+ if dormindo and not vida.sono_profundo and vida.pir_acorda():
enfileirar("acordar")
def camera(ligada):
@@ -539,7 +540,7 @@ def main():
def alguem_chegou():
camera(True) # modo presença: o PIR liga a câmera do celular
- if not madrugada(servidor.controle.config):
+ if not madrugada(servidor.controle.config) and not vida.sono_profundo:
enfileirar("acordar")
def ninguem_por_perto():
diff --git a/rosto-motor.js b/rosto-motor.js
index 201b874..d17a361 100644
--- a/rosto-motor.js
+++ b/rosto-motor.js
@@ -524,12 +524,26 @@
// vale para qualquer rosto (SVG ou motor). O carinha.html pergunta para onde olhar
// (olharAtividade) e chama palco() a cada quadro.
const ATIVIDADES = {
- ler: { nome: 'Lendo', emoji: '📖', dur: [25, 45] },
- cantar: { nome: 'Cantando', emoji: '🎤', dur: [18, 30] },
- bola: { nome: 'Jogando bola', emoji: '⚽', dur: [20, 35] },
- videogame: { nome: 'Videogame', emoji: '🎮', dur: [25, 45] },
- pintar: { nome: 'Pintando', emoji: '🎨', dur: [25, 40] },
- malhar: { nome: 'Malhando', emoji: '🏋️', dur: [15, 25] },
+ ler: { nome: 'Lendo', emoji: '📖', dur: [25, 45] },
+ cantar: { nome: 'Cantando', emoji: '🎤', dur: [18, 30] },
+ bola: { nome: 'Jogando bola', emoji: '⚽', dur: [20, 35] },
+ videogame: { nome: 'Videogame', emoji: '🎮', dur: [25, 45] },
+ pintar: { nome: 'Pintando', emoji: '🎨', dur: [25, 40] },
+ malhar: { nome: 'Malhando', emoji: '🏋️', dur: [15, 25] },
+ dormir: { nome: 'Cochilando', emoji: '🛏️', dur: [40, 80] },
+ cafe: { nome: 'Tomando café', emoji: '☕', dur: [20, 35] },
+ cozinhar: { nome: 'Cozinhando', emoji: '🍳', dur: [20, 35] },
+ regar: { nome: 'Regando planta', emoji: '🪴', dur: [20, 30] },
+ violao: { nome: 'Tocando violão', emoji: '🎸', dur: [20, 35] },
+ dancar: { nome: 'Dançando', emoji: '💃', dur: [15, 30] },
+ corda: { nome: 'Pulando corda', emoji: '🪢', dur: [15, 25] },
+ meditar: { nome: 'Meditando', emoji: '🧘', dur: [25, 45] },
+ telescopio: { nome: 'Olhando estrelas', emoji: '🔭', dur: [25, 40] },
+ pipa: { nome: 'Soltando pipa', emoji: '🪁', dur: [20, 35] },
+ bolhas: { nome: 'Bolhas de sabão', emoji: '🫧', dur: [18, 30] },
+ skate: { nome: 'Andando de skate', emoji: '🛹', dur: [18, 30] },
+ computador: { nome: 'No computador', emoji: '💻', dur: [25, 45] },
+ pescar: { nome: 'Pescando', emoji: '🎣', dur: [30, 50] },
};
const ativ = { nome: null, t0: 0, dur: 0, fim: 0 };
let palcoTela = null, pctx = null;
@@ -582,6 +596,38 @@
const p = pincel(k); info.x = 16 + lado * 5 + p.x * 4; info.y = -1 + p.y * 5;
} else if (ativ.nome === 'malhar') {
const f = forca(k); info.x = lado * 14; info.y = 6 - f * 12; info.ab = f > 0.85 ? 0.55 : 1;
+ } else if (ativ.nome === 'dormir') {
+ info.forma = 'dormir'; info.boca = 'sono'; info.x = lado * 6; info.y = 4;
+ } else if (ativ.nome === 'cafe') {
+ const f = (k % 5) / 5, gole = f > 0.55 && f < 0.92;
+ info.x = lado * 12; info.y = gole ? -3 : 3; if (gole) info.forma = 'feliz';
+ } else if (ativ.nome === 'cozinhar') {
+ const cc = (k % 2.6) / 2.6, h = cc < 0.55 ? 4 * (cc / 0.55) * (1 - cc / 0.55) : 0;
+ info.x = lado * 16; info.y = 4 - h * 14;
+ } else if (ativ.nome === 'regar') {
+ info.x = lado * 18; info.y = 8;
+ } else if (ativ.nome === 'violao') {
+ info.x = lado * 8 + 3 * Math.sin(k * 1.1); info.y = 6 + 1.5 * Math.sin(k * 2.2); info.boca = 'sorriso';
+ if (Math.sin(k * 0.7) > 0.3) info.forma = 'feliz';
+ } else if (ativ.nome === 'dancar') {
+ info.x = lado * 10 + 6 * Math.sin(k * 5); info.y = -2 + 4 * Math.abs(Math.sin(k * 5)); info.forma = 'feliz'; info.boca = 'sorriso';
+ } else if (ativ.nome === 'corda') {
+ const th = k * Math.PI * 2 * 0.85; info.x = lado * 12; info.y = 4 - Math.pow(Math.max(0, Math.cos(th)), 0.6) * 10;
+ } else if (ativ.nome === 'meditar') {
+ info.forma = 'dormir'; info.boca = 'sorriso'; info.x = lado * 6; info.y = -2 + 2 * Math.sin(k * 1.2);
+ } else if (ativ.nome === 'telescopio') {
+ info.x = lado * 16; info.y = -11;
+ } else if (ativ.nome === 'pipa') {
+ info.x = lado * 14 + 4 * Math.sin(k * 0.8); info.y = -11 + 2 * Math.cos(k * 1.1);
+ } else if (ativ.nome === 'bolhas') {
+ info.x = lado * 14 + 4 * Math.sin(k * 0.9); info.y = -8 + 3 * Math.sin(k * 0.6);
+ } else if (ativ.nome === 'skate') {
+ info.x = lado * 12 + 7 * Math.sin(k * 0.9); info.y = 6;
+ } else if (ativ.nome === 'computador') {
+ info.x = lado * 18 + Math.sin(k * 20) * 0.6; info.y = 2 + ((k * 2.5) % 1) * 1.5;
+ } else if (ativ.nome === 'pescar') {
+ const cc = k % 8; info.x = lado * 18; info.y = 7;
+ if (cc > 6.2 && cc < 7.6) { info.forma = 'feliz'; info.boca = 'sorriso'; info.y = 0; }
}
return info;
}
@@ -618,8 +664,9 @@
caixa(c, -s * 0.66, -s * 0.98, s * 1.32, s * 0.98, s * 0.28); c.fill(); c.stroke();
const ox = (p.olhoX || 0) * s * 0.12, oy = (p.olhoY || 0) * s * 0.1;
const pisca = (k % 3.7) < 0.12 ? 0.15 : 1;
- c.save(); c.fillStyle = css(cor);
+ c.save(); c.fillStyle = css(cor); c.strokeStyle = css(cor);
for (const lado of [-1, 1]) {
+ if (p.fechados) { c.lineWidth = Math.max(1.5, s * 0.07); c.beginPath(); c.arc(lado * s * 0.27, -s * 0.52, s * 0.12, 0.2, Math.PI - 0.2); c.stroke(); continue; }
c.beginPath(); c.ellipse(lado * s * 0.27 + ox, -s * 0.5 + oy, s * 0.11, s * 0.16 * pisca, 0, 0, Math.PI * 2); c.fill();
}
c.restore(); c.restore();
@@ -644,6 +691,56 @@
c.beginPath(); c.moveTo(x + s * 0.2, y - s * 0.7); c.quadraticCurveTo(x + s * 0.5, y - s * 0.55, x + s * 0.42, y - s * 0.3); c.stroke();
}
+ const ROSA_ = [255, 112, 176], AMARELO_ = [255, 218, 96], VERDE_ = [120, 255, 160], LARANJA_ = [255, 150, 70];
+ const SENTADO = { sentado: true, pE: [-1.35, 1.35], pD: [1.35, -1.35] };
+ const rnd = (i, sem) => { const a = Math.sin(i * 12.9898 + sem * 78.233) * 43758.5453; return a - Math.floor(a); };
+ function estrela(c, x, y, r) {
+ c.beginPath();
+ for (let i = 0; i < 10; i++) { const a = -Math.PI / 2 + i * Math.PI / 5, rr = i % 2 ? r * 0.45 : r; c.lineTo(x + Math.cos(a) * rr, y + Math.sin(a) * rr); }
+ c.closePath(); c.fill();
+ }
+ function vapor(c, x, y, s, k, cor) { // três fios subindo e ondulando
+ c.save(); c.strokeStyle = css(cor, 0.7); c.lineWidth = Math.max(1, s * 0.04);
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 3; i++) {
+ const f = (k * 0.5 + i / 3) % 1, bx = x + (i - 1) * s * 0.1, by = y - f * s * 0.7;
+ c.globalAlpha = alfa * (1 - f) * Math.min(1, f / 0.2);
+ c.beginPath(); c.moveTo(bx, by); c.bezierCurveTo(bx + s * 0.1, by - s * 0.12, bx - s * 0.1, by - s * 0.24, bx, by - s * 0.36); c.stroke();
+ }
+ c.restore();
+ }
+ function xicara(c, x, y, s, cor) {
+ c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.moveTo(x - s * 0.18, y - s * 0.2); c.lineTo(x + s * 0.18, y - s * 0.2); c.lineTo(x + s * 0.14, y + s * 0.08); c.lineTo(x - s * 0.14, y + s * 0.08); c.closePath(); c.fill(); c.stroke();
+ c.beginPath(); c.arc(x + s * 0.2, y - s * 0.07, s * 0.08, -1.4, 1.4); c.stroke();
+ }
+ function chama(c, x, y, alt, s) {
+ c.fillStyle = css(LARANJA_);
+ c.beginPath(); c.moveTo(x, y - alt); c.quadraticCurveTo(x + s * 0.13, y - alt * 0.3, x, y); c.quadraticCurveTo(x - s * 0.13, y - alt * 0.3, x, y - alt); c.fill();
+ c.fillStyle = css(AMARELO_);
+ c.beginPath(); c.moveTo(x, y - alt * 0.6); c.quadraticCurveTo(x + s * 0.06, y - alt * 0.2, x, y); c.quadraticCurveTo(x - s * 0.06, y - alt * 0.2, x, y - alt * 0.6); c.fill();
+ }
+ function zzz(c, x, y, s, k, cor) { // Zs subindo
+ c.save(); c.strokeStyle = css(cor); c.lineCap = 'round'; c.lineJoin = 'round';
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 3; i++) {
+ const f = (k * 0.3 + i / 3) % 1, tam = s * (0.18 + 0.2 * f), zx = x - s * 0.5 * f, zy = y - s * 1.3 * f;
+ c.globalAlpha = alfa * Math.min(1, f / 0.15) * (1 - f); c.lineWidth = Math.max(1.5, tam * 0.2);
+ c.beginPath(); c.moveTo(zx - tam / 2, zy - tam / 2); c.lineTo(zx + tam / 2, zy - tam / 2); c.lineTo(zx - tam / 2, zy + tam / 2); c.lineTo(zx + tam / 2, zy + tam / 2); c.stroke();
+ }
+ c.restore();
+ }
+ function notas(c, x, y, s, k, cor, dir) {
+ c.fillStyle = css(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 3; i++) {
+ const f = (k * 0.45 + i / 3) % 1;
+ c.globalAlpha = alfa * Math.min(1, f / 0.15) * (1 - f);
+ nota(c, x + dir * s * (0.3 + 0.5 * i) + Math.sin(f * 6 + i) * s * 0.2, y - f * s * 1.6, s * 0.7);
+ }
+ c.globalAlpha = alfa;
+ }
+
const CENAS = {
ler(c, x, y, s, k, cor) {
const b = boneco(c, x, y, s, {
@@ -786,6 +883,321 @@
c.beginPath(); c.arc(b.cabeca.x + s * 0.72, b.cabeca.y + s * 0.3 + g * s * 0.5, s * 0.07, 0, Math.PI * 2); c.fill();
}
},
+ dormir(c, x, y, s, k, cor) {
+ // cama com cabeceira, travesseiro e cobertor que sobe e desce com a respiração
+ const bx0 = x - s * 1.7, bx1 = x + s * 1.9, topo = y - s * 0.75;
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07);
+ c.beginPath(); c.moveTo(bx0 + s * 0.12, y); c.lineTo(bx0 + s * 0.12, topo); c.moveTo(bx1 - s * 0.12, y); c.lineTo(bx1 - s * 0.12, topo); c.stroke();
+ caixa(c, bx0 - s * 0.08, topo - s * 1.25, s * 0.24, s * 1.3, s * 0.1); c.fill(); c.stroke();
+ caixa(c, bx0, topo - s * 0.3, bx1 - bx0, s * 0.32, s * 0.12); c.fill(); c.stroke();
+ c.fillStyle = claro(cor, 0.9);
+ c.beginPath(); c.ellipse(bx0 + s * 0.85, topo - s * 0.5, s * 0.6, s * 0.24, 0, 0, Math.PI * 2); c.fill(); c.stroke();
+ // bonequinho deitado: desenhado em pé e girado 90°, pés à direita, cabeça no travesseiro
+ const e = 0.72, linha = topo - s * 0.3 - s * e * 0.5;
+ c.save(); c.translate(x + s * 1.5, linha); c.rotate(-Math.PI / 2);
+ boneco(c, 0, 0, s * e, { fechados: true, bE: [-0.1, 0], bD: [0.1, 0] }, cor, k);
+ c.restore();
+ const resp = Math.sin(k * 1.4) * s * 0.04;
+ c.fillStyle = css(cor.map((v) => v * 0.45)); c.strokeStyle = css(cor);
+ caixa(c, x - s * 0.3, linha - s * 0.42 - resp, s * 2.15, topo - s * 0.3 - (linha - s * 0.42 - resp) + s * 0.06, s * 0.18); c.fill(); c.stroke();
+ c.strokeStyle = css(cor, 0.5); c.beginPath(); c.moveTo(x - s * 0.1, linha - s * 0.2 - resp); c.lineTo(x + s * 1.7, linha - s * 0.2 - resp); c.stroke();
+ zzz(c, x - s * 0.9, linha - s * 0.9, s, k, cor);
+ },
+ cafe(c, x, y, s, k, cor) {
+ const f = (k % 5) / 5, gole = suave(faixaDe(f, 0.5, 0.64)) * (1 - suave(faixaDe(f, 0.84, 0.97)));
+ const b = boneco(c, x, y, s, Object.assign({}, SENTADO, {
+ olhoX: 0.2, olhoY: 0.3 - gole * 0.8, fechados: gole > 0.85, cabeca: -0.04,
+ bE: [0.15, 0.95], bD: [-0.15 + gole * 1.75, -0.95 - gole * 3.05],
+ }), cor, k);
+ xicara(c, b.mD.x, b.mD.y, s, cor);
+ if (gole < 0.3) vapor(c, b.mD.x, b.mD.y - s * 0.3, s, k, cor);
+ },
+ cozinhar(c, x, y, s, k, cor) {
+ const ciclo = (k % 2.6) / 2.6, voo = ciclo < 0.55 ? ciclo / 0.55 : -1, h = voo >= 0 ? 4 * voo * (1 - voo) : 0;
+ const fx = x + s * 1.0, fw = s * 1.7, fh = s * 1.2;
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07);
+ caixa(c, fx, y - fh, fw, fh, s * 0.08); c.fill(); c.stroke();
+ caixa(c, fx + fw * 0.15, y - fh * 0.72, fw * 0.7, fh * 0.5, s * 0.06); c.stroke();
+ for (let i = 0; i < 3; i++) chama(c, fx + fw * (0.3 + 0.2 * i), y - fh, s * (0.22 + 0.07 * Math.sin(k * 17 + i * 2)), s);
+ const b = boneco(c, x - s * 0.45, y, s, {
+ olhoX: 1, olhoY: 0.3 - h, cabeca: -0.05, bE: [-0.4, 0.3],
+ bD: [1.2 - (voo >= 0 && voo < 0.15 ? 0.25 : 0), 0.35],
+ }, cor, k);
+ const px = b.mD.x + s * 0.62, py = b.mD.y + s * 0.02;
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(2, s * 0.09);
+ c.beginPath(); c.moveTo(b.mD.x, b.mD.y); c.lineTo(px - s * 0.42, py); c.stroke();
+ c.fillStyle = escuro(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ c.beginPath(); c.ellipse(px, py, s * 0.46, s * 0.13, 0, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.save(); c.translate(px, py - s * 0.1 - h * s * 1.6); c.rotate(voo >= 0 ? voo * Math.PI * 2 : 0);
+ c.fillStyle = css(AMARELO_); c.beginPath(); c.ellipse(0, 0, s * 0.32, s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.restore();
+ },
+ regar(c, x, y, s, k, cor) {
+ const cresce = Math.min(1, k / 18), px = x + s * 2.0;
+ // vaso
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.07);
+ c.beginPath(); c.moveTo(px - s * 0.42, y - s * 0.6); c.lineTo(px + s * 0.42, y - s * 0.6); c.lineTo(px + s * 0.3, y); c.lineTo(px - s * 0.3, y); c.closePath(); c.fill(); c.stroke();
+ // planta crescendo: caule, folhas e, no fim, a flor
+ const alt = s * (0.3 + 1.25 * cresce), base = y - s * 0.6, balanca = Math.sin(k * 1.5) * s * 0.06;
+ c.strokeStyle = css(VERDE_); c.lineWidth = Math.max(1.5, s * 0.07);
+ c.beginPath(); c.moveTo(px, base); c.quadraticCurveTo(px + balanca, base - alt / 2, px + balanca * 2, base - alt); c.stroke();
+ c.fillStyle = css(VERDE_);
+ for (let i = 1; i <= 3; i++) {
+ if (cresce < i * 0.22) continue;
+ const yy = base - alt * i * 0.25, lado = i % 2 ? 1 : -1;
+ c.beginPath(); c.ellipse(px + balanca * i * 0.5 + lado * s * 0.18, yy, s * 0.2, s * 0.08, lado * -0.5, 0, Math.PI * 2); c.fill();
+ }
+ if (cresce > 0.75) {
+ const f = (cresce - 0.75) / 0.25, fx = px + balanca * 2, fy = base - alt;
+ c.fillStyle = css(ROSA_);
+ for (let i = 0; i < 5; i++) { const a = i * 1.2566 + k * 0.3; c.beginPath(); c.arc(fx + Math.cos(a) * s * 0.16 * f, fy + Math.sin(a) * s * 0.16 * f, s * 0.11 * f, 0, Math.PI * 2); c.fill(); }
+ c.fillStyle = css(AMARELO_); c.beginPath(); c.arc(fx, fy, s * 0.09 * f, 0, Math.PI * 2); c.fill();
+ }
+ const b = boneco(c, x - s * 0.7, y, s, { olhoX: 1, olhoY: 0.8, cabeca: -0.1, inclina: -0.06, bD: [1.35, 0.35], bE: [-0.3, 0.2] }, cor, k);
+ // regador inclinado e as gotas caindo na planta
+ const ang = 0.38, rx = b.mD.x + s * 0.05, ry = b.mD.y;
+ c.save(); c.translate(rx, ry); c.rotate(ang);
+ c.fillStyle = claro(cor, 0.95); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ caixa(c, 0, -s * 0.22, s * 0.6, s * 0.42, s * 0.08); c.fill(); c.stroke();
+ c.beginPath(); c.moveTo(s * 0.55, -s * 0.05); c.lineTo(s * 1.0, -s * 0.32); c.stroke();
+ c.restore();
+ const ex = rx + Math.cos(ang) * s * 1.0 + Math.sin(ang) * s * 0.32, ey = ry + Math.sin(ang) * s * 1.0 - Math.cos(ang) * s * 0.32;
+ c.fillStyle = claro(cor, 1);
+ for (let i = 0; i < 6; i++) {
+ const f = (k * 1.6 + i / 6) % 1;
+ c.beginPath(); c.ellipse(ex + (px - ex) * f * 0.8, ey + (base - alt - ey + s * 0.2) * f * f, s * 0.035, s * 0.07, 0, 0, Math.PI * 2); c.fill();
+ }
+ },
+ violao(c, x, y, s, k, cor) {
+ const toque = Math.sin(k * 11);
+ const b = boneco(c, x, y, s, Object.assign({}, SENTADO, {
+ olhoX: -0.4, olhoY: 0.7, cabeca: 0.08 * Math.sin(k * 1.1),
+ bE: [-1.1, -0.9], bD: [-0.2 + toque * 0.12, -1.0],
+ }), cor, k);
+ // violão atravessado no colo: corpo perto da mão que toca, braço até a outra mão
+ const gx = b.quadril.x + s * 0.3, gy = b.quadril.y - s * 0.35, nx = b.mE.x, ny = b.mE.y;
+ const a = Math.atan2(ny - gy, nx - gx);
+ c.save(); c.translate(gx, gy); c.rotate(a);
+ const L = Math.hypot(nx - gx, ny - gy);
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ caixa(c, s * 0.2, -s * 0.08, L - s * 0.1, s * 0.16, s * 0.04); c.fill(); c.stroke();
+ caixa(c, L - s * 0.05, -s * 0.12, s * 0.3, s * 0.24, s * 0.06); c.fill(); c.stroke();
+ c.fillStyle = claro(cor, 0.95);
+ c.beginPath(); c.arc(-s * 0.28, 0, s * 0.42, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.beginPath(); c.arc(s * 0.22, 0, s * 0.32, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.fillStyle = escuro(cor); c.beginPath(); c.arc(s * 0.05, 0, s * 0.11, 0, Math.PI * 2); c.fill();
+ c.strokeStyle = css(cor, 0.7); c.lineWidth = Math.max(1, s * 0.015);
+ for (const d of [-0.04, 0, 0.04]) { c.beginPath(); c.moveTo(-s * 0.5, d * s); c.lineTo(L, d * s); c.stroke(); }
+ c.restore();
+ c.fillStyle = css(cor); c.beginPath(); c.arc(b.mD.x, b.mD.y, s * 0.13, 0, Math.PI * 2); c.fill();
+ notas(c, b.cabeca.x - s * 1.6, b.cabeca.y + s * 0.2, s, k, cor, -1);
+ },
+ dancar(c, x, y, s, k, cor) {
+ const u = (Math.sin(k * 5) + 1) / 2, mexe = Math.sin(k * 5) * 0.18;
+ // globo espelhado e luzinhas no chão
+ const gx = x, gy = y - s * 4.5;
+ c.strokeStyle = css(cor, 0.6); c.lineWidth = Math.max(1, s * 0.03);
+ c.beginPath(); c.moveTo(gx, gy - s * 0.9); c.lineTo(gx, gy - s * 0.35); c.stroke();
+ c.fillStyle = claro(cor, 0.9); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.arc(gx, gy, s * 0.35, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.strokeStyle = css(cor, 0.5);
+ for (let i = -1; i <= 1; i++) { c.beginPath(); c.ellipse(gx, gy, s * 0.35 * Math.abs(Math.cos(k + i)), s * 0.35, 0, 0, Math.PI * 2); c.stroke(); }
+ const cores = [ROSA_, AMARELO_, VERDE_, cor];
+ for (let i = 0; i < 6; i++) {
+ const lx = x + Math.sin(k * 1.3 + i * 1.7) * s * 2.4, alfa = c.globalAlpha;
+ c.globalAlpha = alfa * 0.35; c.fillStyle = css(cores[i % 4]);
+ c.beginPath(); c.ellipse(lx, y, s * 0.35, s * 0.07, 0, 0, Math.PI * 2); c.fill();
+ c.globalAlpha = alfa;
+ }
+ boneco(c, x, y, s, {
+ dx: mexe, inclina: mexe * 0.5, cabeca: -mexe * 0.6, olhoY: -0.2, olhoX: mexe * 3,
+ bE: [-0.7 - 2.1 * u, 0.5], bD: [0.7 + 2.1 * (1 - u), -0.5],
+ pE: [-0.1 - u * 0.25, u * 0.4], pD: [0.1 + (1 - u) * 0.25, -(1 - u) * 0.4],
+ }, cor, k);
+ for (let i = 0; i < 4; i++) {
+ const f = (k * 0.7 + i / 4) % 1, alfa = c.globalAlpha;
+ c.globalAlpha = alfa * Math.sin(f * Math.PI); c.fillStyle = css(cores[i]);
+ estrela(c, x + Math.cos(i * 1.9) * s * 1.6, y - s * (2 + i * 0.5) - f * s * 0.4, s * 0.12); c.globalAlpha = alfa;
+ }
+ },
+ corda(c, x, y, s, k, cor) {
+ const th = k * Math.PI * 2 * 0.85, pulo = Math.pow(Math.max(0, Math.cos(th)), 0.6), sobe = pulo * s * 0.45;
+ c.save(); c.globalAlpha *= 0.3 * (1 - pulo * 0.5); c.fillStyle = css(cor);
+ c.beginPath(); c.ellipse(x, y, s * 0.6 * (1 - pulo * 0.3), s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.restore();
+ const b = boneco(c, x, y - sobe, s, {
+ olhoY: -0.2, bE: [-0.75 + 0.15 * Math.sin(th), 0.6], bD: [0.75 - 0.15 * Math.sin(th), -0.6],
+ pE: [-0.06, pulo * 0.3], pD: [0.06, -pulo * 0.3],
+ }, cor, k);
+ // a corda passa por baixo dos pés quando ele está no alto do pulo
+ const my = (b.mE.y + b.mD.y) / 2, embaixo = 2 * (y + s * 0.05) - my, emcima = 2 * (b.cabeca.y - s * 1.0) - my;
+ const cy = (embaixo + emcima) / 2 + Math.cos(th) * (embaixo - emcima) / 2;
+ c.strokeStyle = claro(cor, 1); c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.moveTo(b.mE.x, b.mE.y); c.quadraticCurveTo(x, cy, b.mD.x, b.mD.y); c.stroke();
+ },
+ meditar(c, x, y, s, k, cor) {
+ const flutua = (Math.sin(k * 1.2) + 1) / 2 * s * 0.35, cy = y - s * 1.7 - flutua;
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 3; i++) { // aura
+ const f = (k * 0.35 + i / 3) % 1;
+ c.globalAlpha = alfa * 0.35 * (1 - f); c.strokeStyle = css(cor); c.lineWidth = Math.max(1, s * 0.04);
+ c.beginPath(); c.ellipse(x, cy, s * (1.2 + f * 1.3), s * (1.4 + f * 1.3), 0, 0, Math.PI * 2); c.stroke();
+ }
+ c.globalAlpha = alfa * 0.3; c.fillStyle = css(cor);
+ c.beginPath(); c.ellipse(x, y, s * 0.8 * (1 - flutua / s), s * 0.09, 0, 0, Math.PI * 2); c.fill();
+ c.globalAlpha = alfa;
+ boneco(c, x, y - s * 0.3 - flutua, s, {
+ sentado: true, fechados: true, pE: [-1.45, 2.75], pD: [1.45, -2.75], bE: [-0.75, 0.25], bD: [0.75, -0.25],
+ }, cor, k);
+ },
+ telescopio(c, x, y, s, k, cor) {
+ // céu estrelado com uma estrela cadente de vez em quando
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 16; i++) {
+ c.globalAlpha = alfa * (0.35 + 0.65 * Math.abs(Math.sin(k * (1 + rnd(i, 3)) + i)));
+ c.fillStyle = i % 5 ? claro(cor, 1) : css(AMARELO_);
+ estrela(c, x + s * (0.2 + rnd(i, 1) * 3.4), y - s * (3.4 + rnd(i, 2) * 1.5), s * (0.05 + 0.05 * rnd(i, 4)));
+ }
+ const f = (k % 6) / 1.1;
+ if (f < 1) {
+ const sx = x + s * (3.6 - f * 2.2), sy = y - s * (4.9 - f * 0.8);
+ const g = c.createLinearGradient(sx, sy, sx + s * 0.9, sy - s * 0.33);
+ g.addColorStop(0, claro(cor, 1)); g.addColorStop(1, css(cor, 0));
+ c.globalAlpha = alfa * (1 - f); c.strokeStyle = g; c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.moveTo(sx, sy); c.lineTo(sx + s * 0.9, sy - s * 0.33); c.stroke();
+ }
+ c.globalAlpha = alfa;
+ const b = boneco(c, x - s * 0.7, y, s, { olhoX: 1, olhoY: -1, cabeca: 0.15, bD: [2.1, 0.5], bE: [-0.3, 0.2] }, cor, k);
+ const ox = b.cabeca.x + s * 0.78, oy = b.cabeca.y + s * 0.3, tx = ox + s * 1.6, ty = oy - s * 0.95;
+ const mx = (ox + tx) / 2, my = (oy + ty) / 2;
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ c.beginPath(); c.moveTo(mx, my); c.lineTo(mx - s * 0.5, y); c.moveTo(mx, my); c.lineTo(mx + s * 0.5, y); c.moveTo(mx, my); c.lineTo(mx, y); c.stroke();
+ c.fillStyle = escuro(cor);
+ c.beginPath(); capsula(c, ox, oy, tx, ty, s * 0.15); c.fill(); c.stroke();
+ c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(tx, ty, s * 0.17, 0, Math.PI * 2); c.fill(); c.stroke();
+ },
+ pipa(c, x, y, s, k, cor) {
+ const kx = x + s * (2.3 + 0.45 * Math.sin(k * 0.8)), ky = y - s * (4.2 + 0.35 * Math.cos(k * 1.1)), ang = 0.25 * Math.sin(k * 1.6);
+ const b = boneco(c, x - s * 0.9, y, s, { olhoX: 1, olhoY: -1, cabeca: 0.12, bD: [2.55, 0.35], bE: [-0.4, 0.3] }, cor, k);
+ c.strokeStyle = css(cor, 0.8); c.lineWidth = Math.max(1, s * 0.025);
+ c.beginPath(); c.moveTo(b.mD.x, b.mD.y);
+ c.quadraticCurveTo((b.mD.x + kx) / 2 + s * 0.4, (b.mD.y + ky) / 2 + s * 0.7, kx, ky + s * 0.45); c.stroke();
+ // rabiola com lacinhos
+ c.save(); c.translate(kx, ky); c.rotate(ang);
+ c.strokeStyle = css(cor, 0.8); c.beginPath(); c.moveTo(0, s * 0.5);
+ for (let i = 1; i <= 6; i++) c.lineTo(Math.sin(k * 3 + i) * s * 0.18, s * (0.5 + i * 0.22));
+ c.stroke();
+ for (let i = 2; i <= 6; i += 2) {
+ const lx = Math.sin(k * 3 + i) * s * 0.18, ly = s * (0.5 + i * 0.22);
+ c.fillStyle = css(i % 4 ? ROSA_ : AMARELO_);
+ c.beginPath(); c.moveTo(lx, ly); c.lineTo(lx - s * 0.1, ly - s * 0.06); c.lineTo(lx - s * 0.1, ly + s * 0.06); c.closePath(); c.fill();
+ c.beginPath(); c.moveTo(lx, ly); c.lineTo(lx + s * 0.1, ly - s * 0.06); c.lineTo(lx + s * 0.1, ly + s * 0.06); c.closePath(); c.fill();
+ }
+ const pts = [[0, -0.5], [0.38, 0], [0, 0.5], [-0.38, 0]];
+ const cores = [ROSA_, AMARELO_, cor, VERDE_];
+ for (let i = 0; i < 4; i++) {
+ const [ax, ay] = pts[i], [bx, by] = pts[(i + 1) % 4];
+ c.fillStyle = css(cores[i]); c.beginPath(); c.moveTo(0, 0); c.lineTo(ax * s, ay * s); c.lineTo(bx * s, by * s); c.closePath(); c.fill();
+ }
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.04);
+ c.beginPath(); pts.forEach(([px, py], i) => (i ? c.lineTo(px * s, py * s) : c.moveTo(px * s, py * s))); c.closePath(); c.stroke();
+ c.restore();
+ },
+ bolhas(c, x, y, s, k, cor) {
+ const b = boneco(c, x - s * 0.6, y, s, { olhoX: 0.8, olhoY: -0.6, bD: [1.6, -4.0], bE: [-0.3, 0.2] }, cor, k);
+ const wx = b.mD.x + s * 0.35, wy = b.mD.y - s * 0.25;
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ c.beginPath(); c.moveTo(b.mD.x, b.mD.y); c.lineTo(wx - s * 0.1, wy + s * 0.08); c.stroke();
+ c.beginPath(); c.arc(wx, wy, s * 0.12, 0, Math.PI * 2); c.stroke();
+ const alfa = c.globalAlpha;
+ for (let i = 0; i < 9; i++) {
+ const f = (k * 0.22 + i / 9) % 1, r = s * (0.12 + 0.18 * rnd(i, 4));
+ const bx = wx + f * s * (1.6 + 1.4 * rnd(i, 5)) + Math.sin(k * 2 + i) * s * 0.15, by = wy - f * s * (1.6 + rnd(i, 6));
+ c.globalAlpha = alfa * Math.min(1, f / 0.08) * (f > 0.9 ? (1 - f) / 0.1 : 1);
+ c.fillStyle = css(cor, 0.1); c.strokeStyle = css(cor, 0.9); c.lineWidth = Math.max(1, s * 0.03);
+ c.beginPath(); c.arc(bx, by, r, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.strokeStyle = claro(cor, 1); c.beginPath(); c.arc(bx, by, r * 0.65, -2.4, -1.6); c.stroke();
+ }
+ c.globalAlpha = alfa;
+ },
+ skate(c, x, y, s, k, cor) {
+ const pos = Math.sin(k * 0.9) * 1.3, vel = Math.cos(k * 0.9), ciclo = k % 4;
+ const ollie = ciclo < 0.7 ? Math.sin(ciclo / 0.7 * Math.PI) : 0, sobe = ollie * s * 0.6;
+ const bx = x + pos * s, pe = y - s * 0.3 - sobe;
+ c.save(); c.globalAlpha *= 0.3; c.fillStyle = css(cor);
+ c.beginPath(); c.ellipse(bx, y, s * 0.9, s * 0.08, 0, 0, Math.PI * 2); c.fill(); c.restore();
+ c.strokeStyle = css(cor, 0.4); c.lineWidth = Math.max(1, s * 0.03); // risquinhos de velocidade
+ for (let i = 0; i < 3; i++) { const lx = bx - Math.sign(vel) * s * (1.1 + i * 0.25); c.beginPath(); c.moveTo(lx, pe - s * (0.6 + i * 0.4)); c.lineTo(lx - Math.sign(vel) * s * 0.5 * Math.abs(vel), pe - s * (0.6 + i * 0.4)); c.stroke(); }
+ boneco(c, bx, pe, s, {
+ olhoX: vel * 0.8, inclina: -vel * 0.1, bE: [-1.35, 0.25 + 0.2 * Math.sin(k * 3)], bD: [1.35, -0.25 - 0.2 * Math.sin(k * 3)],
+ pE: [-0.25, 0.35], pD: [0.25, -0.35],
+ }, cor, k);
+ c.save(); c.translate(bx, pe); c.rotate(-ollie * 0.25);
+ c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.05);
+ caixa(c, -s * 0.85, 0, s * 1.7, s * 0.12, s * 0.06); c.fill(); c.stroke();
+ c.fillStyle = escuro(cor);
+ for (const lado of [-1, 1]) { c.beginPath(); c.arc(lado * s * 0.55, s * 0.2, s * 0.1, 0, Math.PI * 2); c.fill(); c.stroke(); }
+ c.restore();
+ },
+ computador(c, x, y, s, k, cor) {
+ const mx0 = x + s * 0.3, mx1 = x + s * 2.7, mt = y - s * 1.45;
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ c.beginPath(); c.moveTo(mx0 + s * 0.15, mt); c.lineTo(mx0 + s * 0.15, y); c.moveTo(mx1 - s * 0.15, mt); c.lineTo(mx1 - s * 0.15, y); c.stroke();
+ caixa(c, mx0, mt, mx1 - mx0, s * 0.12, s * 0.04); c.fill(); c.stroke();
+ const nx = mx0 + s * 0.6, nw = s * 1.4, nh = s * 0.95, ny = mt - s * 0.08 - nh;
+ caixa(c, nx - s * 0.12, mt - s * 0.08, nw + s * 0.24, s * 0.08, s * 0.03); c.fill(); c.stroke();
+ caixa(c, nx, ny, nw, nh, s * 0.06); c.fill(); c.stroke();
+ c.save(); caixa(c, nx + s * 0.06, ny + s * 0.06, nw - s * 0.12, nh - s * 0.12, s * 0.03); c.clip();
+ const linhas = Math.floor(k * 2.5), visiveis = 6, ini = Math.max(0, linhas - visiveis);
+ const tintas = [cor, VERDE_, AMARELO_, ROSA_];
+ for (let i = ini; i < linhas; i++) {
+ const yy = ny + s * 0.14 + (i - ini) * s * 0.13, recuo = (rnd(i, 7) < 0.4 ? 1 : 0) * s * 0.15;
+ c.fillStyle = css(tintas[i % 4], 0.85); c.fillRect(nx + s * 0.12 + recuo, yy, (nw - s * 0.4) * (0.3 + 0.6 * rnd(i, 8)), s * 0.05);
+ }
+ if (Math.floor(k * 2) % 2) { c.fillStyle = claro(cor, 1); c.fillRect(nx + s * 0.12, ny + s * 0.14 + Math.min(linhas, visiveis) * s * 0.13, s * 0.04, s * 0.08); }
+ c.restore();
+ const tecla = Math.sin(k * 20) * 0.05;
+ boneco(c, x - s * 0.55, y, s, Object.assign({}, SENTADO, {
+ olhoX: 1, olhoY: 0.2, cabeca: -0.05, bE: [0.9 + tecla, 0.9], bD: [1.35 - tecla, 0.45],
+ }), cor, k);
+ },
+ pescar(c, x, y, s, k, cor) {
+ const ciclo = k % 8, fisgou = ciclo > 6.2 && ciclo < 7.6, puxa = fisgou ? suave((ciclo - 6.2) / 0.5) : 0;
+ // água: ondinhas à direita do píer
+ c.strokeStyle = css(cor, 0.55); c.lineWidth = Math.max(1, s * 0.035);
+ for (let fila = 0; fila < 3; fila++) {
+ c.beginPath();
+ for (let i = 0; i <= 24; i++) {
+ const wx = x + s * (0.5 + i * 0.14), wy = y - s * 0.05 + fila * s * 0.16 + Math.sin(k * 2 + i * 0.8 + fila) * s * 0.04;
+ if (i) c.lineTo(wx, wy); else c.moveTo(wx, wy);
+ }
+ c.stroke();
+ }
+ c.fillStyle = escuro(cor); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ caixa(c, x - s * 2.0, y - s * 0.22, s * 2.5, s * 0.22, s * 0.05); c.fill(); c.stroke();
+ const b = boneco(c, x - s * 0.8, y - s * 0.22, s, Object.assign({}, SENTADO, {
+ olhoX: 1, olhoY: 0.6 - puxa * 1.4, bD: [2.0 - puxa * 0.4, -0.9], bE: [0.5, 1.2],
+ }), cor, k);
+ const vx = b.mD.x + s * (1.4 - puxa * 0.3), vy = b.mD.y - s * (1.6 + puxa * 0.3);
+ c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.06);
+ c.beginPath(); c.moveTo(b.mE.x, b.mE.y); c.lineTo(b.mD.x, b.mD.y); c.lineTo(vx, vy); c.stroke();
+ const belisca = ciclo > 5.5 && ciclo < 6.2 ? Math.sin(k * 30) * s * 0.06 : 0;
+ let lx = x + s * 2.6, ly = y - s * 0.05 + Math.sin(k * 2) * s * 0.04 + belisca;
+ if (fisgou) { // o peixe pula preso na linha
+ lx = x + s * (2.6 - puxa * 0.6); ly = y - s * (0.2 + Math.sin(puxa * Math.PI) * 1.3 + puxa * 0.3);
+ c.save(); c.translate(lx, ly + s * 0.25); c.rotate(Math.sin(k * 14) * 0.4);
+ c.fillStyle = claro(cor, 1); c.strokeStyle = css(cor); c.lineWidth = Math.max(1.5, s * 0.04);
+ c.beginPath(); c.ellipse(0, 0, s * 0.12, s * 0.28, 0, 0, Math.PI * 2); c.fill(); c.stroke();
+ c.beginPath(); c.moveTo(0, s * 0.25); c.lineTo(-s * 0.14, s * 0.45); c.lineTo(s * 0.14, s * 0.45); c.closePath(); c.fill(); c.stroke();
+ c.fillStyle = escuro(cor); c.beginPath(); c.arc(s * 0.04, -s * 0.12, s * 0.03, 0, Math.PI * 2); c.fill();
+ c.restore();
+ }
+ c.strokeStyle = css(cor, 0.85); c.lineWidth = Math.max(1, s * 0.02);
+ c.beginPath(); c.moveTo(vx, vy); c.quadraticCurveTo((vx + lx) / 2, Math.max(vy, ly) - s * (fisgou ? 0.1 : 0.4), lx, ly); c.stroke();
+ if (!fisgou) {
+ c.fillStyle = css(ROSA_); c.beginPath(); c.arc(lx, ly, s * 0.09, Math.PI, 0); c.fill();
+ c.fillStyle = claro(cor, 1); c.beginPath(); c.arc(lx, ly, s * 0.09, 0, Math.PI); c.fill();
+ }
+ },
};
/** Desenha a cena da atividade (camada atrás dos olhos). o = { t, cor }. */
diff --git a/tests/test_vida.py b/tests/test_vida.py
index f951d3f..b86ed1f 100644
--- a/tests/test_vida.py
+++ b/tests/test_vida.py
@@ -100,11 +100,14 @@ class VidaTest(unittest.TestCase):
self.v.celular_online = lambda: True
self.v.necessidades.update(energia=80, alegria=70, social=70, diversao=10) # entediado
self.v.rng.random = lambda: 0.0 # sorteio sempre "sim"
- ato = self.v.agir(DIA)
+ ato = self.v.agir(DIA) # entediado: vai fazer algo
self.assertTrue(ato.startswith('atividade '))
- self.assertIn(ato.split()[1], ('videogame', 'bola', 'pintar', 'ler'))
- self.assertEqual(self.ordens[-1], ato)
+ nome, segundos = self.ordens[-1].split()[1:]
+ self.assertEqual(ato, 'atividade ' + nome)
+ self.assertTrue(120 <= float(segundos) <= 300) # por alguns minutos
self.assertEqual(self.v.diario[0]['tipo'], 'atividade')
+ self.v.celular_atividade = lambda: nome # fazendo: diversão sobe
+ self.v.passar_tempo(DIA + 180)
self.assertGreater(self.v.necessidades['diversao'], 10)
self.v.celular_atividade = lambda: 'videogame' # ocupado: não interrompe
self.assertIsNone(self.v.agir(DIA + 60))
@@ -113,6 +116,49 @@ class VidaTest(unittest.TestCase):
self.v.config['noite'] = False
self.assertFalse(str(self.v.agir(noite)).startswith('atividade')) # de madrugada, nunca
+ def test_exausto_dorme_e_o_sono_e_respeitado(self):
+ self.v.necessidades['energia'] = 15
+ self.assertEqual(self.v.agir(DIA), 'dormir')
+ self.assertTrue(self.v.sono_profundo)
+ self.assertIn('sono profundo', self.ordens)
+ self.v.necessidades['energia'] = 60
+ self.assertIsNone(self.v.agir(DIA + 60)) # ainda não descansou
+ self.v.necessidades['energia'] = 96
+ self.assertEqual(self.v.agir(DIA + 120), 'acordar')
+ self.assertFalse(self.v.sono_profundo)
+ self.assertIn('sono leve', self.ordens)
+
+ def test_acordado_de_proposito_encerra_o_sono_pesado(self):
+ self.v.necessidades['energia'] = 15
+ self.v.agir(DIA)
+ self.v.celular_dormindo = lambda: False # tocaram na tela
+ self.v.tick(DIA + 30)
+ self.assertFalse(self.v.sono_profundo)
+ self.assertEqual(self.v.diario[0]['texto'], 'Acordou antes de descansar')
+
+ def test_cochilo_quando_cansado(self):
+ self.v.celular_online = lambda: True
+ self.v.rng.random = lambda: 0.0
+ self.v.necessidades.update(energia=35, diversao=80)
+ self.assertEqual(self.v.agir(DIA), 'atividade dormir')
+ self.v.celular_atividade = lambda: 'dormir' # na caminha: recupera
+ antes = self.v.necessidades['energia']
+ self.v.passar_tempo(DIA + 300)
+ self.assertGreater(self.v.necessidades['energia'], antes)
+
+ def test_alegria_caindo_deixa_bravo(self):
+ self.v.necessidades.update(energia=80, social=80, diversao=80, alegria=100)
+ self.assertEqual(self.v.zanga(), 0.0)
+ self.v.necessidades['alegria'] = 42
+ self.assertEqual(self.v.zanga(), 0.4)
+ self.v.tick(DIA + 5)
+ self.assertIn('zanga 0.4', self.ordens)
+ self.v.necessidades['alegria'] = 20
+ self.assertEqual(self.v.humor(), 'bravo')
+ self.assertEqual(self.v.zanga(), 0.9)
+ self.v.dormindo_proprio = True # dormindo, sem cara feia
+ self.assertEqual(self.v.zanga(), 0.0)
+
def test_horario_da_madrugada(self):
from controle_pc import madrugada
h = lambda hora: time.mktime((2026, 10, 6, hora, 10, 0, 0, 0, -1))
diff --git a/vida.py b/vida.py
index aa0c337..463822f 100644
--- a/vida.py
+++ b/vida.py
@@ -48,7 +48,7 @@ CUIDADOS = {'carinho': 'Recebeu carinho', 'cutucar': 'Levou uma cutucada', 'brin
HUMORES = {
'dormindo': ('😴', 'Dormindo'), 'cansado': ('🥱', 'Cansado'), 'carente': ('🥺', 'Carente'),
- 'entediado': ('😐', 'Entediado'), 'triste': ('😢', 'Triste'), 'feliz': ('😄', 'Feliz'),
+ 'entediado': ('😐', 'Entediado'), 'triste': ('😢', 'Triste'), 'bravo': ('😠', 'Mal-humorado'), 'feliz': ('😄', 'Feliz'),
'tranquilo': ('🙂', 'Tranquilo'),
}
@@ -68,18 +68,30 @@ ATOS = {
'carente_perto': [('acenar', 3, 'Acenou chamando atenção'), ('espiar', 2, 'Espiou quem está aí'),
('coracao', 2, 'Pediu carinho'), ('surpreso', 1, 'Fez cara de "oi!"'), ('timido', 1, 'Ficou tímido')],
'triste': [('triste', 3, 'Ficou tristinho'), ('olhar_baixo', 2, 'Olhou para baixo'), ('pensar', 1, 'Ficou pensativo')],
+ 'bravo': [('bravo', 3, 'Ficou emburrado'), ('olhar_baixo', 2, 'Bufou de mau humor'), ('confuso', 1, 'Resmungou sozinho')],
'cansado': [('bocejar', 3, 'Bocejou'), ('piscar2', 1, 'Piscou devagar'), ('olhar_baixo', 2, 'Cochilou de olho aberto'),
('tonto', 1, 'Ficou meio tonto de sono')],
}
-# Atividades (momentos dele): o bonequinho faz algo na tela. Por humor: (atividade, peso, diário, efeito)
-ATIVIDADES = {
- 'feliz': [('cantar', 3, 'Foi cantar'), ('bola', 2, 'Foi jogar bola'), ('malhar', 1, 'Foi malhar')],
- 'tranquilo': [('ler', 3, 'Foi ler um livro'), ('pintar', 2, 'Foi pintar um quadro'), ('cantar', 1, 'Foi cantar')],
- 'entediado': [('videogame', 3, 'Foi jogar videogame'), ('bola', 2, 'Foi jogar bola'),
- ('pintar', 1, 'Foi pintar um quadro'), ('ler', 1, 'Foi ler um livro')],
+# Como no The Sims, cada necessidade baixa puxa um comportamento:
+# energia < COCHILO: às vezes tira um cochilo (cena da cama); < SONO: dorme de verdade e o sono é
+# respeitado (som, movimento e PIR não acordam) até DESCANSADO; alegria caindo: o rosto vai ficando
+# bravo (zanga); diversão < TEDIO: vai fazer uma atividade por alguns minutos.
+COCHILO, SONO, DESCANSADO, TEDIO = 40, 20, 95, 45
+ATIVAS = ['bola', 'malhar', 'corda', 'skate', 'dancar', 'pipa'] # gastam energia
+CALMAS = ['ler', 'cafe', 'meditar', 'telescopio', 'pescar', 'computador', 'regar'] # quase não gastam
+CRIATIVAS = ['pintar', 'violao', 'cantar', 'cozinhar', 'bolhas', 'videogame']
+NOME_ATIVIDADE = {
+ 'ler': 'Foi ler um livro', 'cantar': 'Foi cantar', 'bola': 'Foi jogar bola', 'videogame': 'Foi jogar videogame',
+ 'pintar': 'Foi pintar um quadro', 'malhar': 'Foi malhar', 'dormir': 'Tirou um cochilo', 'cafe': 'Foi tomar um café',
+ 'cozinhar': 'Foi cozinhar', 'regar': 'Foi regar a plantinha', 'violao': 'Foi tocar violão', 'dancar': 'Foi dançar',
+ 'corda': 'Foi pular corda', 'meditar': 'Foi meditar', 'telescopio': 'Foi olhar as estrelas', 'pipa': 'Foi soltar pipa',
+ 'bolhas': 'Foi fazer bolhas de sabão', 'skate': 'Foi andar de skate', 'computador': 'Foi mexer no computador',
+ 'pescar': 'Foi pescar',
}
-GASTO_ATIVIDADE = {'malhar': 8, 'bola': 5, 'cantar': 3} # energia gasta
+# Efeito por minuto enquanto faz a atividade
+POR_MINUTO_DIVERSAO, POR_MINUTO_ALEGRIA = 6.0, 1.5
+ENERGIA_POR_MINUTO = {**{a: -2.0 for a in ATIVAS}, **{a: -0.2 for a in CALMAS}, 'cafe': 3.0} # café dá pique
# Assuntos das figurinhas que ele inventa sozinho, por humor e por hora do dia.
TEMAS = {
@@ -88,6 +100,7 @@ TEMAS = {
'entediado': ['um foguete', 'um planeta com anel', 'uma pipa', 'um dado', 'um controle de videogame', 'um pião'],
'carente': ['um coração com curativo', 'um ursinho de pelúcia', 'uma carta de amor', 'dois corações juntos'],
'triste': ['uma nuvem de chuva', 'um guarda-chuva', 'uma gota d\'água'],
+ 'bravo': ['uma nuvem de tempestade', 'um vulcão', 'um raio', 'uma pimenta'],
'cansado': ['uma lua com touca de dormir', 'uma xícara de café', 'um travesseiro', 'uma bateria fraca'],
}
PERIODOS = [(5, 12, 'de manhã', ['um sol nascendo', 'um pãozinho', 'uma xícara de café']),
@@ -115,6 +128,11 @@ class Vida:
self.celular_online = lambda: False
self.alguem_perto = lambda: False # PIR/movimento recente
self.celular_atividade = lambda: None # atividade que o celular está fazendo agora
+ self.sono_profundo = False # dormindo de cansaço: sensores não acordam
+ self.dormiu_em = 0.0
+ self.atividade_em = 0.0 # quando começou a última atividade
+ self.ultima_atividade = None
+ self.zanga_enviada = None
self.bloqueado = lambda: False # E-Stop ou modo presença com ninguém em casa
self.pode_gemini = lambda: bool(os.environ.get('GEMINI_API_KEY'))
self.madrugada = self.noite_padrao # o main troca pelo horário dos ajustes
@@ -178,6 +196,10 @@ class Vida:
if not limpo['ligado']:
self.enfileirar('humor normal') # o celular volta aos gestos soltos de sempre
self.enfileirar('ritmo normal') # e à sensibilidade dos Ajustes
+ self.enfileirar('zanga 0.0')
+ if self.sono_profundo:
+ self.sono_profundo = False
+ self.enfileirar('sono leve')
self.proximo_ato = time.time() + 5
if self.config['ligado'] and 'ritmo' in limpo:
self.enfileirar('ritmo ' + self.config['ritmo'])
@@ -212,11 +234,17 @@ class Vida:
if n['diversao'] < 25:
return 'entediado'
if n['alegria'] < 30:
- return 'triste'
+ return 'bravo'
if n['alegria'] >= 65 and n['social'] >= 40 and n['diversao'] >= 40:
return 'feliz'
return 'tranquilo'
+ def zanga(self):
+ """0 a 1: o quanto o rosto está emburrado. Começa abaixo de 60 de alegria e piora devagar."""
+ if self.dormindo():
+ return 0.0
+ return round(max(0.0, min(1.0, (60 - self.necessidades['alegria']) / 45)), 1)
+
def anotar(self, tipo, texto, agora=None):
with self.lock:
self.diario.insert(0, dict(horario=agora or time.time(), tipo=tipo, texto=texto))
@@ -234,6 +262,7 @@ class Vida:
humor=h, emoji=HUMORES[h][0], nome_humor=HUMORES[h][1], dormindo=self.dormindo(),
nascimento=self.nascimento, idade_dias=int((agora - self.nascimento) // 86400),
emojis_hoje=self.emojis_hoje(agora), ultimo_emoji=self.emojis.get('ultimo') or None,
+ zanga=self.zanga(), sono_profundo=self.sono_profundo,
pode_criar=self.motivo_sem_emoji(agora) or '', diario=self.diario[:30])
def avisar(self):
@@ -244,13 +273,18 @@ class Vida:
def passar_tempo(self, agora):
minutos = max(0.0, min(10.0, (agora - self.ultimo_tick) / 60)) # pausa longa (placa desligada) não zera tudo
self.ultimo_tick = agora
- dormindo = self.dormindo()
+ atividade = self.celular_atividade()
+ dormindo = self.dormindo() or atividade == 'dormir'
with self.lock:
n = self.necessidades
for k, q in QUEDA.items():
n[k] = _limitar(n[k] - q * minutos * (SONO_LENTO if dormindo else 1))
if dormindo:
n['energia'] = _limitar(n['energia'] + RECUPERA_SONO * minutos)
+ elif atividade:
+ n['diversao'] = _limitar(n['diversao'] + (QUEDA['diversao'] + POR_MINUTO_DIVERSAO) * minutos)
+ n['alegria'] = _limitar(n['alegria'] + POR_MINUTO_ALEGRIA * minutos)
+ n['energia'] = _limitar(n['energia'] + ENERGIA_POR_MINUTO.get(atividade, -0.5) * minutos)
# alegria acompanha devagar a média de companhia e diversão
alvo = (n['social'] + n['diversao']) / 2 + 10
n['alegria'] = _limitar(n['alegria'] + (alvo - n['alegria']) * min(1.0, 0.05 * minutos))
@@ -284,18 +318,46 @@ class Vida:
self.avisar()
return self.status(agora)
- def dormir(self, agora, motivo):
+ def dormir(self, agora, motivo, profundo=False):
self.dormindo_proprio = True
+ self.dormiu_em = agora
self.enfileirar('dormir')
+ if profundo: # cansado de verdade: sono respeitado até descansar
+ self.sono_profundo = True
+ self.enfileirar('sono profundo')
if motivo:
self.anotar('sono', motivo, agora)
def acordar(self, agora, motivo):
self.dormindo_proprio = False
+ if self.sono_profundo:
+ self.sono_profundo = False
+ self.enfileirar('sono leve')
self.enfileirar('acordar')
if motivo:
self.anotar('sono', motivo, agora)
+ def escolher_atividade(self):
+ """Pelo pique: cansado prefere coisas calmas (e às vezes um café); cheio de energia, coisas agitadas."""
+ e = self.necessidades['energia']
+ if 35 <= e < 55 and self.rng.random() < 0.35:
+ return 'cafe'
+ if e < 50:
+ lista = CALMAS * 2 + CRIATIVAS
+ elif e > 70:
+ lista = ATIVAS * 2 + CRIATIVAS + CALMAS
+ else:
+ lista = ATIVAS + CRIATIVAS * 2 + CALMAS
+ opcoes = [a for a in lista if a != self.ultima_atividade] or lista
+ return self.rng.choice(opcoes)
+
+ def comecar_atividade(self, agora, nome, segundos):
+ self.enfileirar('atividade %s %d' % (nome, segundos))
+ self.atividade_em = agora
+ self.ultima_atividade = nome
+ self.anotar('atividade', NOME_ATIVIDADE.get(nome, 'Foi fazer algo'), agora)
+ return 'atividade ' + nome
+
def pir_acorda(self, agora=None):
"""Disparo do PIR com o rosto dormindo: acorda? No ritmo calmo precisa de vários em 2 min."""
agora = agora or time.time()
@@ -376,30 +438,31 @@ class Vida:
n = self.necessidades
perto = self.alguem_perto()
if self.dormindo():
- # de madrugada nunca acorda sozinho (nem com gente se mexendo na cama); de dia, quando descansou
- if n['energia'] >= 95 and not self.madrugada(agora):
+ # de madrugada nunca acorda sozinho (nem com gente se mexendo na cama); de dia, só descansado
+ if n['energia'] >= DESCANSADO and not self.madrugada(agora):
self.acordar(agora, 'Acordou descansado')
return 'acordar'
return None
if self.config['noite'] and self.madrugada(agora) and agora - self.interagiu_em > PEDIU_ATENCAO:
self.dormir(agora, 'Ficou tarde, foi dormir')
return 'dormir'
- if n['energia'] < 20:
- self.dormir(agora, 'Ficou cansado e foi tirar um cochilo')
+ if n['energia'] < SONO:
+ self.dormir(agora, 'Ficou exausto e foi dormir', profundo=True)
return 'dormir'
humor = self.humor()
if self.celular_atividade():
return None # ocupado com uma atividade: deixa terminar
- # momentos dele: ler, cantar, jogar bola, videogame... (de dia, com o celular online)
- if humor in ATIVIDADES and self.celular_online() and not self.madrugada(agora) and self.rng.random() < 0.18:
- nome, texto = self.escolher(ATIVIDADES[humor])
- self.enfileirar('atividade ' + nome)
- with self.lock:
- n['diversao'] = _limitar(n['diversao'] + 18)
- n['alegria'] = _limitar(n['alegria'] + 5)
- n['energia'] = _limitar(n['energia'] - GASTO_ATIVIDADE.get(nome, 2))
- self.anotar('atividade', texto, agora)
- return 'atividade ' + nome
+ dia = self.celular_online() and not self.madrugada(agora)
+ folga = agora - self.atividade_em > 90 # um respiro entre uma atividade e outra
+ # cansado: um cochilo na caminha de vez em quando (recupera energia sem o sono pesado)
+ if dia and folga and n['energia'] < COCHILO and self.rng.random() < 0.35:
+ return self.comecar_atividade(agora, 'dormir', self.rng.uniform(120, 240))
+ # entediado: vai fazer alguma coisa por alguns minutos (pode ser interrompido)
+ if dia and folga and n['diversao'] < TEDIO and self.rng.random() < 0.75:
+ return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(120, 300))
+ # bem: de vez em quando, uma atividade curtinha por vontade própria
+ if dia and folga and humor in ('feliz', 'tranquilo') and self.rng.random() < 0.1:
+ return self.comecar_atividade(agora, self.escolher_atividade(), self.rng.uniform(40, 90))
# habilidade: inventar uma figurinha nova (raro, com limite por dia e intervalo)
if not self.motivo_sem_emoji(agora) and self.celular_online() and self.rng.random() < 0.08:
if self.criar_figurinha(agora, humor):
@@ -426,12 +489,22 @@ class Vida:
return None
ato = None
humor = self.humor()
+ # acordaram ele de propósito (toque, painel) antes de descansar: o sono pesado acaba aqui
+ if self.sono_profundo and not self.dormindo() and agora - self.dormiu_em > 15:
+ self.sono_profundo = False
+ self.enfileirar('sono leve')
+ self.anotar('sono', 'Acordou antes de descansar', agora)
if not self.bloqueado():
# o celular escolhe os gestos soltos pelo humor; reenvia de tempos em tempos (pode ter reiniciado)
- if humor != self.humor_enviado[0] or agora - self.humor_enviado[1] > 60:
+ zanga = self.zanga()
+ if humor != self.humor_enviado[0] or agora - self.humor_enviado[1] > 60 or zanga != self.zanga_enviada:
self.enfileirar('humor ' + humor)
self.enfileirar('ritmo ' + self.config['ritmo'])
+ self.enfileirar('zanga %.1f' % zanga)
+ if self.sono_profundo:
+ self.enfileirar('sono profundo')
self.humor_enviado = (humor, agora)
+ self.zanga_enviada = zanga
if agora >= self.proximo_ato:
ato = self.agir(agora)
lo, hi = RITMOS[self.config['ritmo']]
diff --git a/web/src/lib/comandos.svelte.ts b/web/src/lib/comandos.svelte.ts
index 883f74c..969a4bc 100644
--- a/web/src/lib/comandos.svelte.ts
+++ b/web/src/lib/comandos.svelte.ts
@@ -41,6 +41,20 @@ export const ATIVIDADES_INFO: Record =
videogame: { nome: 'Videogame', emoji: '🎮' },
pintar: { nome: 'Pintar', emoji: '🎨' },
malhar: { nome: 'Malhar', emoji: '🏋️' },
+ dormir: { nome: 'Cochilar', emoji: '🛏️' },
+ cafe: { nome: 'Café', emoji: '☕' },
+ cozinhar: { nome: 'Cozinhar', emoji: '🍳' },
+ regar: { nome: 'Regar planta', emoji: '🪴' },
+ violao: { nome: 'Violão', emoji: '🎸' },
+ dancar: { nome: 'Dançar', emoji: '💃' },
+ corda: { nome: 'Pular corda', emoji: '🪢' },
+ meditar: { nome: 'Meditar', emoji: '🧘' },
+ telescopio: { nome: 'Telescópio', emoji: '🔭' },
+ pipa: { nome: 'Pipa', emoji: '🪁' },
+ bolhas: { nome: 'Bolhas', emoji: '🫧' },
+ skate: { nome: 'Skate', emoji: '🛹' },
+ computador: { nome: 'Computador', emoji: '💻' },
+ pescar: { nome: 'Pescar', emoji: '🎣' },
};
export const atividade = (nome: string) => tentar(() => postar('/comando', { tipo: 'atividade', valor: nome }));
diff --git a/web/src/lib/componentes/VidaCartao.svelte b/web/src/lib/componentes/VidaCartao.svelte
index 01b2976..2f2d7fb 100644
--- a/web/src/lib/componentes/VidaCartao.svelte
+++ b/web/src/lib/componentes/VidaCartao.svelte
@@ -57,7 +57,9 @@
{v.config.ligado ? v.emoji : '🤖'}
{v.config.ligado ? v.nome_humor : 'Modo autônomo desligado'}{#if v.config.ligado}{/if}
-
{idade(v.idade_dias)}{v.config.ligado && v.diario[0] ? ' · ' + v.diario[0].texto : ''}
+
+ {idade(v.idade_dias)}{v.config.ligado && v.sono_profundo ? ' · sono pesado: só acorda descansado' : v.config.ligado && v.diario[0] ? ' · ' + v.diario[0].texto : ''}
+
configurarVida({ ligado: l })} />
diff --git a/web/src/lib/guia.ts b/web/src/lib/guia.ts
index d6675a0..974759f 100644
--- a/web/src/lib/guia.ts
+++ b/web/src/lib/guia.ts
@@ -42,12 +42,13 @@ export const TOPICOS: Record = {
emoji: '🐣',
onde: 'Ao vivo › Vida',
resumo:
- 'Com o modo vida ligado, o Carinha funciona como um bichinho virtual: tem necessidades que mudam com o tempo e com o que acontece em volta, e decide sozinho o que fazer pelo humor.',
+ 'Com o modo vida ligado, o Carinha funciona como um Sim: tem necessidades que caem com o tempo, e cada necessidade baixa puxa um comportamento. Ele decide sozinho, mas você sempre pode interromper.',
itens: [
- { emoji: '⚡', nome: 'Energia', texto: 'Cai enquanto ele está acordado e enche dormindo. Muito baixa, ele vai tirar um cochilo.' },
- { emoji: '😊', nome: 'Alegria', texto: 'Acompanha devagar a companhia e a diversão.' },
+ { emoji: '⚡', nome: 'Energia', texto: 'Cai acordado e mais rápido nas atividades agitadas; o café dá um pique. Abaixo de 40%, às vezes tira um cochilo na caminha. Abaixo de 20%, dorme de verdade e o sono é respeitado: som, movimento e PIR não acordam até ele chegar a 95%.' },
+ { emoji: '😊', nome: 'Alegria', texto: 'Acompanha a companhia e a diversão. Abaixo de 60%, o rosto vai ficando emburrado aos poucos (testa franzida, boca para baixo e, no fim, olhos bravos). Abaixo de 30%, fica mal-humorado.' },
{ emoji: '💬', nome: 'Companhia', texto: 'Sobe quando alguém aparece (PIR), conversa com ele ou faz carinho. Baixa, ele fica carente.' },
- { emoji: '🎈', nome: 'Diversão', texto: 'Sobe com movimento em volta, brincadeiras e quando ele inventa algo. Baixa, ele fica entediado.' },
+ { emoji: '🎈', nome: 'Diversão', texto: 'Abaixo de 45%, ele vai fazer uma atividade por 2 a 5 minutos (cansado escolhe coisas calmas; cheio de pique, coisas agitadas). Enquanto faz, a diversão sobe.' },
+ { emoji: '🛌', nome: 'Sono do celular', texto: 'Com o modo vida ligado, quem decide dormir é a energia; o celular não adormece mais só porque ficou 20 s em silêncio.' },
{ emoji: '🎨', nome: 'Inventar figurinhas sozinho', texto: 'De vez em quando ele tem uma ideia e desenha uma figurinha nova com o Gemini, que vai para a biblioteca de Reações. No máximo 3 por dia, com 90 min entre elas.' },
{ emoji: '🌙', nome: 'Dormir à noite', texto: 'Na madrugada ele vai dormir mesmo com gente se mexendo por perto. Carinho ou conversa seguram ele acordado por 10 minutos.' },
{ emoji: '📖', nome: 'Diário', texto: 'Lista do que ele fez sozinho e dos cuidados que recebeu.' },
@@ -77,7 +78,7 @@ export const TOPICOS: Record = {
{ emoji: '🙂', nome: 'Tranquilo', texto: 'Normal. Olha em volta, pisca devagar, meio sorriso.' },
{ emoji: '😐', nome: 'Entediado', texto: 'Diversão baixa. Fica pensativo, boca de lado, procura o que fazer e às vezes toca uma reação da biblioteca.' },
{ emoji: '🥺', nome: 'Carente', texto: 'Companhia baixa. Com alguém perto, acena e pede carinho; sozinho, fica cabisbaixo.' },
- { emoji: '😢', nome: 'Triste', texto: 'Alegria baixa. Fica tristinho, olha para baixo e mexe menos.' },
+ { emoji: '😠', nome: 'Mal-humorado', texto: 'Alegria abaixo de 30%. Testa franzida, olhos bravos, resmunga. Carinho, conversa e brincadeira resolvem.' },
{ emoji: '🥱', nome: 'Cansado', texto: 'Energia baixa. Boceja, suspira e logo vai dormir.' },
{ emoji: '😴', nome: 'Dormindo', texto: 'Olhos fechados, respira devagar e recupera energia.' },
],
@@ -168,15 +169,13 @@ export const TOPICOS: Record = {
emoji: '🎮',
onde: 'Ao vivo › Gestos › Atividades',
resumo:
- 'De vez em quando ele faz alguma coisa: os olhos sobem e encolhem, e embaixo aparece um bonequinho dele, na cor dos olhos, fazendo a atividade. Os olhos grandes acompanham (seguem a bola, leem as linhas, olham a TV).',
+ 'São 20 atividades. A tela se divide: o rosto vai para a esquerda e à direita aparece um bonequinho dele, na cor dos olhos, fazendo a atividade. Os olhos grandes acompanham (seguem a bola, leem as linhas, olham a TV, a pipa, o peixe).',
itens: [
- { emoji: '📖', nome: 'Ler', texto: 'Sentado com um livro; vira a página de vez em quando e os olhos leem linha a linha.' },
- { emoji: '🎤', nome: 'Cantar', texto: 'Com microfone, balançando, notas subindo e boca cantando.' },
- { emoji: '⚽', nome: 'Jogar bola', texto: 'Faz embaixadinha; os olhos seguem a bola.' },
- { emoji: '🎮', nome: 'Videogame', texto: 'Sentado com o controle na frente de uma TV com um joguinho; comemora quando ganha.' },
- { emoji: '🎨', nome: 'Pintar', texto: 'No cavalete; o quadro vai ganhando pinceladas coloridas.' },
- { emoji: '🏋️', nome: 'Malhar', texto: 'Levanta halteres e sua.' },
- { emoji: '🐣', nome: 'No modo vida', texto: 'Ele escolhe sozinho pelo humor: feliz canta e joga bola; tranquilo lê e pinta; entediado joga videogame. Nunca de madrugada.' },
+ { emoji: '⚽', nome: 'Agitadas (gastam energia)', texto: 'Jogar bola, malhar, pular corda, skate, dançar (com globo espelhado) e soltar pipa.' },
+ { emoji: '📖', nome: 'Calmas', texto: 'Ler, tomar café (dá energia), meditar flutuando, olhar estrelas no telescópio, pescar, computador e regar a plantinha (que cresce e dá flor).' },
+ { emoji: '🎨', nome: 'Criativas', texto: 'Pintar, tocar violão, cantar, cozinhar (vira a panqueca), bolhas de sabão e videogame.' },
+ { emoji: '🛏️', nome: 'Cochilar', texto: 'Deita na caminha com cobertor e Zzz; recupera energia como dormir.' },
+ { emoji: '🐣', nome: 'No modo vida', texto: 'Com a diversão baixa, ele escolhe uma pelo pique e fica nela de 2 a 5 minutos; cansado, cochila. Nunca de madrugada.' },
{ emoji: '👆', nome: 'Parar', texto: 'Botão Parar no painel ou um toque na tela do celular. Dormir também encerra a atividade.' },
],
},
diff --git a/web/src/lib/tipos.ts b/web/src/lib/tipos.ts
index 8bb872e..7be1c45 100644
--- a/web/src/lib/tipos.ts
+++ b/web/src/lib/tipos.ts
@@ -97,6 +97,10 @@ export interface Vida {
idade_dias: number;
emojis_hoje: number;
ultimo_emoji: number | null;
+ /** 0 a 1: o quanto está emburrado (alegria baixa). */
+ zanga?: number;
+ /** Dormindo de cansaço: som, movimento e PIR não acordam. */
+ sono_profundo?: boolean;
/** '' quando pode inventar figurinha agora; senão, o motivo. */
pode_criar: string;
diario: { horario: number; tipo: 'acao' | 'sono' | 'emoji' | 'cuidado' | 'vida' | 'atividade'; texto: string }[];