No description
Find a file
2026-08-01 09:30:29 +02:00
src feat: create first mvp 2026-08-01 09:30:29 +02:00
.gitignore Initial commit 2026-08-01 07:29:38 +00:00
Cargo.toml feat: create first mvp 2026-08-01 09:30:29 +02:00
LICENSE Initial commit 2026-08-01 07:29:38 +00:00
README.md feat: create first mvp 2026-08-01 09:30:29 +02:00

PoC3: Rotinas de NPCs (Utility AI)

Objetivo

Demonstrar AI de NPCs com rotinas diárias (Shopkeeper, TavernKeeper, Villager) usando Utility AI customizada com Thinker/Scorer/Action. Cada NPC escolhe a ação de maior score com base no horário simulado e em flags do Blackboard. Quando a flag shop_ruined é ativada (tecla R), o Shopkeeper troca permanentemente seu comportamento para ficar na taverna.

Como rodar

cargo run -p poc3

Controles

  • R — ativa a flag shop_ruined, fazendo o Shopkeeper trocar permanentemente para comportamento de luto (StayAt taverna)
  • Output de log mostra a ação escolhida por cada NPC a cada frame

HUD Dia/Noite

Um Text2d no canto superior-esquerdo mostra o período atual do ciclo simulado: "☀ Dia" (Morning/Noon, 06h18h) ou "🌙 Noite" (18h06h), junto com a hora simulada (ex.: ☀ Dia (14h)). Implementado em src/hud.rs (TimeHudPlugin), reutilizando thinker::current_hour — a mesma fórmula do relógio usada pelas rotinas dos NPCs (10s reais = 24h). Sua posição é derivada do box de conteúdo da câmera (ver seção seguinte), não de um canto fixo, pra nunca sair da área garantida visível.

Câmera Responsiva

Os 3 NPCs só andam entre 3 pontos fixos — shop (80,350), tavern (400,200), home (720,400) — então a "cena" tem um bounding box conhecido e fixo (640×200, centro em (400,300)). ResponsiveCameraPlugin (src/camera.rs) recentraliza a câmera nesse ponto todo frame e ajusta o scale da projeção ortográfica para que o box (com 30% de margem) sempre caiba dentro da janela atual:

  • Janela grande: zoom mais próximo (scale menor, mas nunca abaixo de 1.0 — não amplia além de 1 unidade de mundo = 1 pixel), cena permanece centralizada.
  • Janela pequena: zoom mais afastado (scale maior) pra nada ficar cortado, cena continua centralizada.
  • Recalcula a cada frame lendo o tamanho da janela primária — não depende de eventos de resize, então também corrige o zoom inicial no primeiro frame.

O que aparece na tela

  • Fundo: cinza/preto padrão do Bevy (sem mapa desenhado)
  • 3 quadrados coloridos 24×24 representando os NPCs, que se movem entre 3 localizações:
    • 🔴 Vermelho — Shopkeeper (começa na shop @ (80, 350))
    • 🔵 Azul — TavernKeeper (começa na tavern @ (400, 200))
    • 🟡 Amarelo — Villager (começa em casa @ (720, 400))
  • Movimento visível: os NPCs andam para suas posições-alvo baseado na rotina ativa. O ciclo simulado é de 10s = 24h:
    • Manhã (0-10s): Shopkeeper→shop, TavernKeeper→tavern, Villager→home
    • Noite (10-20s): Shopkeeper→tavern, TavernKeeper→home, Villager→home
    • Após 20s volta para manhã
  • Velocidade: 60 px/s (chega em ~5-10s dependendo da distância)
  • Anti-sobreposição: à noite, TavernKeeper e Villager convergem para o mesmo destino (home); quem chegar primeiro ocupa o ponto, e o segundo para a pelo menos 24px de distância (tamanho do sprite) em vez de empilhar exatamente em cima.
  • Ao pressionar R: Shopkeeper troca para "luto" e fica parado na tavern. Os outros 2 NPCs continuam rotinas normais.
  • Saída: log mostra cada decisão de ação por frame.

Fluxo principal

  1. Três NPCs spawnam ao iniciar: Shopkeeper, TavernKeeper, Villager
  2. Cada NPC tem um Thinker com múltiplos Scorer + Action pareados
  3. A cada frame, o Thinker avalia todos os scorers e escolhe a ação com maior score (Utility = score do Scorer)
  4. O horário simulado (3 períodos: Morning, Noon, Night) dita as rotinas normais
  5. Se shop_ruined=true, o GriefScorer (score 1.0) supera todos os outros, e o Shopkeeper troca para StayAt taverna permanentemente via ThinkerSwapPlugin

Arquitetura

  • poc3/src/main.rs — entry point, setup de plugins, input handling
  • poc3/src/blackboard.rs — Resource Blackboard com HashMap<String, Belief>, crenças com decaimento exponencial
  • poc3/src/thinker.rsThinker Component, Scorer trait, Action enum, NpcKind enum, TimeOfDay com 3 períodos
  • poc3/src/consideration.rsConsideration trait, CompositeScorer (gates outro Scorer por considerações), BlackboardConsideration, TimeOfDayConsideration
  • poc3/src/npcs/mod.rs — 3 NPCs com rotinas (MorningScorer, NoonScorer, NightScorer, GriefScorer, WorkScorer), NpcPlugin com sistema update_npcs
  • poc3/src/thinker_swap.rsThinkerSwapPlugin que substitui o Thinker do Shopkeeper quando shop_ruined é detectado
  • poc3/src/visuals.rsattach_npc_sprites, sprite 24×24 colorido por NpcKind (registrado direto em Update, sem plugin próprio)
  • poc3/src/movement.rsMovementPlugin/move_npcs_toward_target, anda o NPC em direção ao alvo de Action::GoTo via thinker::location_pos
  • poc3/src/camera.rsResponsiveCameraPlugin/fit_camera_to_window, recentraliza e reescala a câmera pro box shop/tavern/home caber na janela atual

Contratos com outros PoCs

  • Lê do Blackboard (compartilhado via Resource): flag shop_ruined
  • AI roda independente do mapa, mas os NPCs são entidades no overworld (gerenciado por PoC7)
  • Thinker + Scorer traits são compatíveis com a interface do big_brain (fork codeberg.org/zkat/big-brain), permitindo migração futura

Detalhes de implementação

  • O dia simulado dura 10 segundos reais: hour = (elapsed / 10.0 * 24.0) % 24.0
  • Períodos: Morning (6h-12h), Noon (12h-18h), Night (18h-6h)
  • Crenças no Blackboard têm decay_rate para decaimento exponencial: effective = value * (1 - decay)^t
  • CompositeScorer permite compor scorers com gates lógicos (ex: "só executa se flag X for true")
  • GriefScorer ignora horário e retorna 1.0 se shop_ruined > 0.5, 0.0 caso contrário

Status

Roda em loop | 3 NPCs com rotinas diárias | Utility AI funcional | Troca de comportamento via flag | Testes unitários em todos os módulos