No description
  • Rust 99%
  • Lua 1%
Find a file
2026-08-04 08:07:28 +02:00
assets feat: add more events/quest and clearly system to creation new events 2026-08-04 08:07:28 +02:00
src feat: add more events/quest and clearly system to creation new events 2026-08-04 08:07:28 +02:00
.gitignore Initial commit 2026-08-01 07:27:59 +00:00
Cargo.toml feat: create first mvp 2026-08-01 09:28:31 +02:00
LICENSE Initial commit 2026-08-01 07:27:59 +00:00
README.md feat: add more events/quest and clearly system to creation new events 2026-08-04 08:07:28 +02:00

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 feature lua-editor): executa o .lua num sandbox com bindings de captura — cada chamada vira um CompiledAction gravado em ordem; o resultado é serializado em .ron ao lado do fonte.
  • Runtime (src/compiled_script.rs, sempre compilado): lê o .ron, aplica SetFlag direto nas flags e enfileira MoveNpc/OpenDoor/CloseDoor como PendingAction — a mesma fila que action_consumer já drena.
  • get_flag no compilador lê as initial_flags do config.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

  1. ScriptPipelinePlugin (Startup) insere MutableState como Resource
  2. Com lua-editor: lua_compiler::compile_lua_scripts compila level_1.lua e cada .lua referenciado em config.triggers (ver seção de triggers abaixo), (re)escrevendo o .ron de cada um
  3. compiled_script::load_compiled_script seeda as flags de config.initial_flags, lê level_1.ron e aplica: flags direto, ações na fila de PendingAction
  4. action_consumer::process_actions (Update) drena a fila e aplica no ECS: move NPCs, abre portas, etc.
  5. player::move_player (Update) lê WASD/setas e move o player
  6. trigger::check_proximity_triggers (Update) mede a distância do player até cada TriggerConfig; ao entrar no raio, carrega e aplica o .ron daquele 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 quando open_door dispara — o mesmo frame que recolore a porta pra vermelho (recolor_open_doors em main.rs) também remove o Obstacle, 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.ronWorldState 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, sizesize.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:

  1. Escreva o script: crie assets/scripts/triggers/meu_evento.lua usando os bindings existentes — add_quest("Nome da Quest"), set_flag("minha_flag", true), move_npc(...), open_door(...), close_door(...). Quantas linhas quiser.
  2. Registre o trigger: adicione um bloco em assets/config.rontriggers:
    (
        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",
    )
    
  3. (Opcional) NPC visual: se o evento é "perto de alguém", adicione também uma entrada em config.ronnpcs na mesma posição, só por estética — o trigger funciona mesmo sem NPC ali.
  4. Compile: rode cargo run -p poc2 uma vez (feature lua-editor, ligada por padrão) — lua_compiler::compile_lua_scripts acha o novo trigger em config.triggers automaticamente e gera meu_evento.ron do lado do .lua. Sem isso o build console (--no-default-features) não acha o .ron.
  5. Commit o .ron gerado — é 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 id fazem o segundo nunca disparar (o "já disparei" é rastreado por id).
  • .ron faltando — o script aponta pra um arquivo cujo .ron ainda 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ós move_npc("guide", 10, 20) no Lua, vai para (10, 20) — mesmo ponto do trigger accept_quest.
  • NPC Guard — quadrado vermelho 40×40 px em world (100, 300). Após move_npc("Guard", 120, 340), vai para (120, 340) — mesmo ponto do trigger battle_zone.
  • NPC King — quadrado dourado 40×40 px em world (300, -150) — mesmo ponto do trigger king_zone. Não se move.
  • Door throne_room — retângulo verde 60×120 px em world (200, 0). Após open_door("throne_room") no Lua, vira vermelho (perde a colisão via marcador RemoveCollider).
  • HUD — texto Text2d no 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 quando true, cinza quando false — visível em tempo real assim que um script chama set_flag, inclusive para chaves novas que ainda não existiam. Quests ativas aparecem em dourado (⚔ nome da quest) assim que um trigger chama add_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 do config.ron
  • poc2/src/compiled_script.rsCompiledAction/CompiledScript (serde), applier RON→WorldState, ScriptPipelinePlugin
  • poc2/src/lua_compiler.rs — (feature lua-editor) sandbox + bindings de captura + compile no startup (script inicial + scripts de trigger)
  • poc2/src/world_state.rsWorldState (HashMap<String, bool> flags + VecDeque<PendingAction> + Vec<String> quests) e MutableState (Arc<Mutex<WorldState>>)
  • poc2/src/action_consumer.rs — sistema Update que drena actions e aplica no ECS
  • poc2/src/collision.rsObstacle, checagem circular de colisão (is_blocked_by_obstacle), obstacle_half_size
  • poc2/src/player.rsPlayer, PlayerPlugin, movimento WASD/setas contínuo, bloqueado por Obstacle
  • poc2/src/trigger.rsTriggerPlugin, checagem de proximidade circular + aplica o .ron do trigger no WorldState; validate_triggers (Startup) avisa sobre id duplicado / .ron faltando
  • poc2/assets/scripts/level_1.lua — script de autoria (fonte)
  • poc2/assets/scripts/level_1.ron — script compilado (runtime; regenerado pelo lua-editor)
  • poc2/assets/scripts/triggers/{accept_quest,battle_zone,king_zone}.lua — scripts de autoria dos 3 triggers de exemplo
  • poc2/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 WorldState Resource (flag store + action queue + quest log) para poc4, poc6, poc7
  • Fornece MutableState Resource (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