- Rust 100%
| src | ||
| .gitignore | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
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, 06h–18h) ou "🌙 Noite" (18h–06h), 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
- Três NPCs spawnam ao iniciar: Shopkeeper, TavernKeeper, Villager
- Cada NPC tem um
Thinkercom múltiplosScorer+Actionpareados - A cada frame, o Thinker avalia todos os scorers e escolhe a ação com maior score (Utility = score do Scorer)
- O horário simulado (3 períodos: Morning, Noon, Night) dita as rotinas normais
- Se
shop_ruined=true, oGriefScorer(score 1.0) supera todos os outros, e o Shopkeeper troca paraStayAt tavernapermanentemente viaThinkerSwapPlugin
Arquitetura
poc3/src/main.rs— entry point, setup de plugins, input handlingpoc3/src/blackboard.rs— ResourceBlackboardcomHashMap<String, Belief>, crenças com decaimento exponencialpoc3/src/thinker.rs—ThinkerComponent,Scorertrait,Actionenum,NpcKindenum,TimeOfDaycom 3 períodospoc3/src/consideration.rs—Considerationtrait,CompositeScorer(gates outro Scorer por considerações),BlackboardConsideration,TimeOfDayConsiderationpoc3/src/npcs/mod.rs— 3 NPCs com rotinas (MorningScorer, NoonScorer, NightScorer, GriefScorer, WorkScorer),NpcPlugincom sistemaupdate_npcspoc3/src/thinker_swap.rs—ThinkerSwapPluginque substitui o Thinker do Shopkeeper quandoshop_ruinedé detectadopoc3/src/visuals.rs—attach_npc_sprites, sprite 24×24 colorido porNpcKind(registrado direto emUpdate, sem plugin próprio)poc3/src/movement.rs—MovementPlugin/move_npcs_toward_target, anda o NPC em direção ao alvo deAction::GoToviathinker::location_pospoc3/src/camera.rs—ResponsiveCameraPlugin/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): flagshop_ruined - AI roda independente do mapa, mas os NPCs são entidades no overworld (gerenciado por PoC7)
Thinker+Scorertraits são compatíveis com a interface dobig_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_ratepara decaimento exponencial:effective = value * (1 - decay)^t CompositeScorerpermite compor scorers com gates lógicos (ex: "só executa se flag X for true")GriefScorerignora horário e retorna 1.0 seshop_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