feat(vida): 20 atividades e necessidades no estilo The Sims

Quatorze cenas novas para o bonequinho: cochilar na caminha (cobertor
respirando e Zzz), café (vapor e gole), cozinhar (vira a panqueca no
fogão), regar a plantinha (cresce e dá flor), violão, dançar (globo
espelhado), pular corda, meditar flutuando, telescópio (céu estrelado e
estrela cadente), pipa com rabiola, bolhas de sabão, skate (com ollie),
computador (código aparecendo) e pescar (o peixe pula preso na linha).

Necessidades puxam comportamento, como no The Sims:
- energia < 40%: às vezes cochila; < 20%: dorme de verdade com sono
  respeitado (som comum, movimento e PIR não acordam) até 95%; acordar
  de propósito (toque, painel) encerra o sono pesado;
- alegria abaixo de 60%: o rosto vai ficando emburrado (zanga 0 a 1:
  testa franzida, boca para baixo, olhos bravos); < 30%: mal-humorado;
- diversão < 45%: faz uma atividade de 2 a 5 min escolhida pelo pique
  (cansado prefere calmas, café dá energia); pode ser interrompido;
- durante a atividade a diversão sobe e a energia cai conforme o tipo.

Com o modo vida ligado o celular não adormece mais por 20 s de silêncio:
quem decide o sono é a energia. A cor escolhida nunca muda com a zanga.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
julio
2026-10-06 21:28:09 -03:00
co-authored by Claude Opus 5.5
parent d7a17b5683
commit e73897d9c1
11 changed files with 1073 additions and 76 deletions
+99 -26
View File
@@ -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']]