- Rust 99%
- Lua 1%
| assets | ||
| src | ||
| .gitignore | ||
| Cargo.toml | ||
| LICENSE | ||
| README.md | ||
PoC2: Lua como editor → RON como runtime (console-friendly)
Objetivo
Demonstrar um pipeline de conteúdo onde Lua é ferramenta de autoria e RON é o formato
de runtime. Designers escrevem scripts .lua imperativos (set_flag, get_flag,
move_npc, open_door, close_door); em builds de desenvolvimento (feature
lua-editor, ligada por padrão), o jogo compila o .lua para um .ron declarativo
no startup e em seguida consome só o .ron. Builds "console"
(--no-default-features) não embarcam VM de Lua nenhuma — leem direto o .ron
pré-compilado. Outras pessoas podem criar quests/eventos sem mexer no Rust.
Pipeline
level_1.lua ──(lua-editor: compile no startup)──▶ level_1.ron ──▶ WorldState/ECS
▲
build console lê daqui (sem mlua)
- Compilador (
src/lua_compiler.rs, atrás da featurelua-editor): executa o.luanum sandbox com bindings de captura — cada chamada vira umCompiledActiongravado em ordem; o resultado é serializado em.ronao lado do fonte. - Runtime (
src/compiled_script.rs, sempre compilado): lê o.ron, aplicaSetFlagdireto nas flags e enfileiraMoveNpc/OpenDoor/CloseDoorcomoPendingAction— a mesma fila queaction_consumerjá drena. get_flagno compilador lê asinitial_flagsdoconfig.ron+ as flags que o próprio script setou (semântica determinística de snapshot).
Build de console (sem Lua)
cargo run -p poc2 --no-default-features
Roda o mesmo cenário de demonstração lendo apenas assets/scripts/level_1.ron
(pré-compilado e commitado), sem mlua no binário.
Como rodar
cargo run -p poc2
Aguarde ~30s para build cold (mlua + bevy compilam do zero na primeira vez).
Fluxo principal
ScriptPipelinePlugin(Startup) insereMutableStatecomo Resource- Com
lua-editor:lua_compiler::compile_lua_scriptscompilalevel_1.luae cada.luareferenciado emconfig.triggers(ver seção de triggers abaixo), (re)escrevendo o.ronde cada um compiled_script::load_compiled_scriptseeda as flags deconfig.initial_flags, lêlevel_1.rone aplica: flags direto, ações na fila dePendingActionaction_consumer::process_actions(Update) drena a fila e aplica no ECS: move NPCs, abre portas, etc.player::move_player(Update) lê WASD/setas e move o playertrigger::check_proximity_triggers(Update) mede a distância do player até cadaTriggerConfig; ao entrar no raio, carrega e aplica o.rondaquele trigger (mesmo applier do passo 3), uma única vez
Script de exemplo
poc2/assets/scripts/level_1.lua:
set_flag("intro_shown", true)
move_npc("guide", 10.0, 20.0)
move_npc("Guard", 120.0, 340.0)
open_door("throne_room")
Controles
- WASD / setas: move o player (quadrado branco) pela cena
- O resto (NPCs, porta) continua rodando automaticamente no Startup via
level_1.lua
Colisão
- Player vs NPC/porta fechada: bloqueio circular (raio do player + raio do
obstáculo); o player para na borda em vez de atravessar. Implementado em
collision.rs(Obstacle,is_blocked_by_obstacle), sem wall-sliding — se o passo colide, o player simplesmente não anda naquele frame. Mesmo padrão do PoC4. - Guide, Guard e King são sempre obstáculos (raio = metade do lado do sprite, 20px pros 40×40 atuais).
- Porta
throne_room: obstáculo (raio = metade do maior lado, 60px pros 60×120 atuais) enquanto fechada. Some quandoopen_doordispara — o mesmo frame que recolore a porta pra vermelho (recolor_open_doorsemmain.rs) também remove oObstacle, então a porta aberta deixa passar.
Triggers de Proximidade → Quest Log / Batalha / Morte do Rei
Exemplo prático de eventos disparados por proximidade (não por Startup), usando o
mesmo pipeline .lua→.ron→WorldState do script inicial — só que gatilhado por
distância do player em vez de rodar uma vez no boot. Implementado em
src/trigger.rs (TriggerPlugin, check_proximity_triggers), lendo
config.triggers: Vec<TriggerConfig> (id, position, size — size.0 é o
raio da zona —, script). Cada trigger dispara uma única vez por execução
(marcado internamente assim que aplicado, não reaplica se o player continuar
dentro da zona ou voltar depois).
Trigger (id) |
Perto de | Script (assets/scripts/triggers/) |
Efeito no WorldState |
|---|---|---|---|
accept_quest |
Guide (10, 20) | accept_quest.lua |
add_quest("Find the King") + set_flag("quest_accepted", true) — entra no HUD quest log |
battle_zone |
Guard (120, 340) | battle_zone.lua |
set_flag("battle_triggered", true) — stub do evento "batalha" (sem cena de combate nesta PoC) |
king_zone |
King (300, -150) | king_zone.lua |
set_flag("npc_king_dead", true) — stub do evento "morte do rei" |
Cada .lua em triggers/ é autoria (compilado para .ron pelo lua_compiler no
Startup, igual ao level_1.lua); o .ron compilado e commitado é o que
trigger.rs lê em builds console (--no-default-features). add_quest(name) é
o novo binding Lua (CompiledAction::AddQuest), que empurra pra
WorldState.quests: Vec<String> (dedupado por nome) — renderizado no HUD como
uma quest log separada das flags.
Antes desta mudança, level_1.lua setava npc_king_dead=true incondicionalmente
no Startup; agora essa flag só vira true quando o player efetivamente chega
perto do King (king_zone), como um evento de verdade em vez de um valor
pré-setado.
Adicionando novos eventos e quests (sem mexer no Rust)
O sistema foi feito pra uma pessoa que não mexe em Rust adicionar um evento de
proximidade novo (e a quest/flag que ele dispara) só editando .lua e .ron:
- Escreva o script: crie
assets/scripts/triggers/meu_evento.luausando os bindings existentes —add_quest("Nome da Quest"),set_flag("minha_flag", true),move_npc(...),open_door(...),close_door(...). Quantas linhas quiser. - Registre o trigger: adicione um bloco em
assets/config.ron→triggers:( id: "meu_evento", // precisa ser único — ver validação abaixo position: (x, y), // centro da zona, mesmas coordenadas de mundo dos NPCs size: (raio, raio), // só size.0 é usado, como raio do círculo script: "triggers/meu_evento.lua", ) - (Opcional) NPC visual: se o evento é "perto de alguém", adicione também uma
entrada em
config.ron→npcsna mesma posição, só por estética — o trigger funciona mesmo sem NPC ali. - Compile: rode
cargo run -p poc2uma vez (featurelua-editor, ligada por padrão) —lua_compiler::compile_lua_scriptsacha o novo trigger emconfig.triggersautomaticamente e gerameu_evento.rondo lado do.lua. Sem isso o build console (--no-default-features) não acha o.ron. - Commit o
.rongerado — é ele que builds console leem, sem precisar de Lua.
Quests: não existe lista separada pra registrar — qualquer add_quest("Nome")
em qualquer script (inicial ou trigger) já aparece na quest log do HUD, dedupado
por nome. Não precisa mexer em WorldState nem em hud.rs.
Flags: mesma coisa — set_flag("qualquer_chave", true) cria a flag na hora,
aparece no HUD colorida (verde/cinza), nenhum registro central necessário.
Validação automática: trigger::validate_triggers roda no Startup e loga
warn! no terminal pra dois erros comuns de quem copia-e-cola um bloco de
trigger:
- id duplicado — dois triggers com o mesmo
idfazem o segundo nunca disparar (o "já disparei" é rastreado por id). .ronfaltando — oscriptaponta pra um arquivo cujo.ronainda não foi compilado (rode o passo 4 acima).
Limitação importante: cada script (inicial ou de trigger) é compilado uma
vez, no Startup, contra o snapshot de initial_flags do boot — get_flag
dentro de um trigger não enxerga o que outro trigger fez em runtime, só o
estado inicial. Não dá pra escrever if get_flag("quest_accepted") then ...
num trigger esperando que ele veja o resultado de outro trigger que já
disparou; pra isso seria preciso um consumidor Rust lendo múltiplas flags
(como action_consumer.rs já faz para actions).
O que aparece na tela
Visual (janela 3D aberta)
- Player — quadrado branco 24×24 px, spawna em (-300, -250). Movido com WASD/setas.
- NPC
guide— quadrado azul 40×40 px em world (0, 0). Apósmove_npc("guide", 10, 20)no Lua, vai para (10, 20) — mesmo ponto do triggeraccept_quest. - NPC
Guard— quadrado vermelho 40×40 px em world (100, 300). Apósmove_npc("Guard", 120, 340), vai para (120, 340) — mesmo ponto do triggerbattle_zone. - NPC
King— quadrado dourado 40×40 px em world (300, -150) — mesmo ponto do triggerking_zone. Não se move. - Door
throne_room— retângulo verde 60×120 px em world (200, 0). Apósopen_door("throne_room")no Lua, vira vermelho (perde a colisão via marcadorRemoveCollider). - HUD — texto
Text2dno canto superior esquerdo em world (-550, 320, 1) mostrando flags + quest log + actions queue. Fonte 18pt branca. Cada flag muda de cor conforme o valor: verde quandotrue, cinza quandofalse— visível em tempo real assim que um script chamaset_flag, inclusive para chaves novas que ainda não existiam. Quests ativas aparecem em dourado (⚔ nome da quest) assim que um trigger chamaadd_quest.
Inspector (devtool, F1)
- Painel lateral direito (360 px) com 6 seções colapsáveis: Camera / NPCs / Doors / HUD / Initial Flags / Scripts
- 3 botões no rodapé: Apply (despawn + respawn + reset flags), Save to RON, Reload from RON
- Pressione F1 para mostrar/ocultar
- Mouse interage com widgets (DragValue, TextEdit, Checkbox)
- Sem mouse não dá pra editar valores
Terminal (logs info!)
INFO poc2::script_loader: executing initial script: ".../poc2/assets/scripts/level_1.lua"
INFO poc2::script_loader: initial script executed successfully
INFO poc2::action_consumer: moved NPC guide to (10, 20)
INFO poc2::action_consumer: moved NPC Guard to (120, 340)
INFO poc2::action_consumer: opened door throne_room
INFO poc2::hud: PoC2 HUD is live
Arquitetura
poc2/src/main.rs— entry point, spawns NPCs (guide, Guard, King), Door (throne_room) e Player doconfig.ronpoc2/src/compiled_script.rs—CompiledAction/CompiledScript(serde), applier RON→WorldState,ScriptPipelinePluginpoc2/src/lua_compiler.rs— (featurelua-editor) sandbox + bindings de captura + compile no startup (script inicial + scripts de trigger)poc2/src/world_state.rs—WorldState(HashMap<String, bool>flags +VecDeque<PendingAction>+Vec<String>quests) eMutableState(Arc<Mutex<WorldState>>)poc2/src/action_consumer.rs— sistema Update que drena actions e aplica no ECSpoc2/src/collision.rs—Obstacle, checagem circular de colisão (is_blocked_by_obstacle),obstacle_half_sizepoc2/src/player.rs—Player,PlayerPlugin, movimento WASD/setas contínuo, bloqueado porObstaclepoc2/src/trigger.rs—TriggerPlugin, checagem de proximidade circular + aplica o.rondo trigger noWorldState;validate_triggers(Startup) avisa sobre id duplicado /.ronfaltandopoc2/assets/scripts/level_1.lua— script de autoria (fonte)poc2/assets/scripts/level_1.ron— script compilado (runtime; regenerado pelolua-editor)poc2/assets/scripts/triggers/{accept_quest,battle_zone,king_zone}.lua— scripts de autoria dos 3 triggers de exemplopoc2/assets/scripts/triggers/{accept_quest,battle_zone,king_zone}.ron— compilados (commitados; usados pelo build console)
Segurança
lua_compiler::install_sandbox remove funções perigosas (loadfile, dofile, collectgarbage) do ambiente Lua para evitar que scripts de autoria escapem do sandbox. Em builds de produção o problema nem existe: não há VM.
Contratos com outros PoCs
- Fornece
WorldStateResource (flag store + action queue + quest log) para poc4, poc6, poc7 - Fornece
MutableStateResource (Arc<Mutex>) para acesso concorrente - Define contrato de flags:
bridge_fixed,shop_ruined,quest_accepted,intro_shown,npc_king_dead,battle_triggered, etc. - Define contrato de actions:
MoveNpc { id, x, y },OpenDoor { id },CloseDoor { id },AddQuest { name }
Status
✅ Roda limpo | Lua scripts executam | Actions aplicadas com sucesso | Sandbox ativo | Player WASD + colisão com NPCs/porta fechada | 3 triggers de proximidade (quest / batalha / morte do rei) | Novo evento/quest é 100% .lua+.ron, sem tocar Rust — ver "Adicionando novos eventos e quests" | Validação de id duplicado / .ron faltando no Startup | Testado visualmente (colisão e porta aberta confirmadas) | Testes unitários em bindings, world_state, action_consumer, hud, player, collision, trigger e lua_state