Community project

ESP32 Project Ideas (copy) (copy)

Photo of ESP32 Project Ideas (copy) (copy)
Generated with AI

Lucas Soares

Published November 11, 2026

This Raspberry Pi Pico project brings an interactive space exploration game to life with a 128x64 OLED display, a 5x5 NeoPixel matrix for visual feedback, and physical buttons for gameplay control. Players navigate campaigns, complete missions, manage crew, and explore procedurally generated frontiers while managing fuel, shield, and hull integrity across multiple game modes.

The guide provides a complete parts list, wiring diagram showing connections between the Pico, display, pixel matrix, and input buttons, and the full firmware implementation. Builders will learn how to integrate display graphics, addressable LEDs, debounced button input, and game state management on the Pico platform.

Wiring diagram

Wiring diagram for ESP32 Project Ideas (copy) (copy)
Loading diagram…

Assemble it in 15 steps

1. Prepare a placa

Use a BitDogLab v6.3 com os recursos integrados. Nao adicione fios. Conecte o USB e pressione Deploy. Deixe o joystick solto durante a inicializacao para calibrar o centro.

  • Nao aplique 5 V aos GPIOs.

2. Aprenda no tutorial

O sistema inicia no menu. Use B para percorrer as opcoes e A para confirmar. O menu principal tem Decolar, Central, Diario e Tutorial. Central abre as missoes anteriores e o Hangar. Escolha Tutorial quando quiser treinar. Mova o joystick ate aparecer a explicacao do freio automatico. Pressione A para continuar. No treino nao ha danos nem consumo de combustivel.

3. Faca a primeira entrega

Siga a seta na tela ate o alvo. A distancia ALVO deve diminuir. Solte o joystick para frear. Na entrega livre ha duas paradas: pressione A em cada destino e siga a seta para o proximo. Depois volte a BASE e pressione A. O tutorial e a campanha mantem uma unica parada. Nao e necessario atingir uma velocidade exata.

  • A nave fica no centro e o cenario se move. O alvo tem aneis; a estacao tem paineis laterais; pequenas rochas sao asteroides. A nave gira na direcao do movimento e mostra chama ao acelerar.
  • B pausa e abre a ajuda. B novamente continua. No painel, A liga ou desliga o som. Ha sons de motor, botoes, partida, entrega, impacto e conclusao. O motor usa pulsos curtos para nao encobrir os avisos.
  • No radar, branco representa a nave, verde o alvo, azul a base e vermelho os asteroides. Uma celula pode conter varios objetos; nave, base e alvo tem prioridade sobre rochas. Use principalmente a seta da tela.

4. Jogue as missoes

Use B para escolher e A para confirmar. Central abre os contratos e o Hangar; Hangar reune setor e melhorias; Voltar retorna ao menu principal. No setor inicial, entrega rende 20 creditos, exploracao 30 e resgate 40, alem de experiencia. Patentes sobem a cada 60 XP; o nivel 2 libera o segundo setor e o nivel 4 libera o terceiro. Trocar setor alterna entre os setores liberados. Setores avancados rendem mais e causam mais dano em impactos. Tutorial e uma opcao independente no menu, nunca obrigatoria. As missoes usam freio automatico, mas os asteroides causam dano. Escudo custa 30 creditos por melhoria, motor 40 e tanque 35, com tres melhorias de cada. O diario mostra patente, XP e missoes concluidas. O tutorial nao da recompensas.

  • Creditos, XP, contagens, setor, melhorias e preferencia de som sao salvos automaticamente ao concluir missoes ou alterar melhorias, setor ou som. Ao ligar, voce retorna a base; um voo interrompido nao e retomado. Nao desligue durante o salvamento. A primeira gravacao usa a area EEPROM e pode substituir dados antigos do bichinho. Regravar ou apagar a flash pode remover o progresso. O tutorial continua opcional.

5. Campanha Ecos de Aurora

Em Central, escolha Campanha. Leia a transmissao e pressione A para continuar ou B para voltar. Na escolha, B alterna entre duas respostas e A inicia o voo. Siga a seta, interaja com A no alvo e volte a base. Cada capitulo tem uma viagem, uma escolha e uma conversa final. Sao seis capitulos com Lia, Ivo, NOX e Nara. Entrega, exploracao e resgate livres continuam disponiveis em Missoes.

  • As escolhas mudam os relatos e o desfecho, nao a dificuldade nem os atributos da nave. A campanha visita Io e Lira sem exigir patente; isso nao desbloqueia esses setores no hangar.
  • Cada capitulo concluido paga a recompensa normal e um bonus de 25 creditos e 15 XP. O progresso e as escolhas sao salvos ao voltar a base. Uma derrota ou reinicio permite tentar o mesmo capitulo novamente.
  • Depois dos seis capitulos, Campanha permite reler o desfecho escolhido. As missoes livres continuam disponiveis. O progresso anterior e mantido quando sua gravacao e valida.
  • Nao desligue durante uma gravacao. Um voo interrompido nao e salvo. O jogo informa erro na tela se a gravacao falhar.

6. Explore os encontros opcionais

Cada voo fora do tutorial tem um sinal opcional em uma posicao variavel, representado por interrogacao no OLED e amarelo no radar quando proximo. A seta continua apontando para a missao principal. Aproxime-se do sinal e pressione A. O voo pausa para uma decisao; B escolhe e A confirma. Pode recuperar pecas ou reparar o casco, ajudar uma sonda com combustivel ou registrar um eco antigo. Ha um encontro por voo; voce pode ignora-lo.

  • Os encontros variam entre tres tipos. Bonus em creditos e descobertas so sao guardados ao concluir a missao e voltar a base; perder a nave ou desligar antes disso descarta esses ganhos. Reparos e combustivel afetam apenas o voo atual.
  • Entrega paga ate 10 creditos extras pela integridade da carga; cada colisao antes da entrega reduz esse extra. A recompensa normal nao diminui. Exploracao livre exige leituras em tres pontos: segure A em cada um ate completar a barra. NOX reduz o tempo de cada leitura; soltar A pausa o progresso. No resgate livre, segure A por dois segundos seguidos no SOS para reparar; soltar ou afastar reinicia o reparo. Depois solte e pressione A para rebocar. O reboque reduz a aceleracao em 20 por cento na volta. Campanha preserva o objetivo unico e as regras anteriores. O tutorial mantem as acoes simples originais.
  • O diario mostra quantas das tres reliquias foram registradas e se ja encontrou a do setor atual. Registre um sinal antigo em cada setor e volte a base para completar a colecao. Os cenarios e os desenhos dos alvos diferem entre entrega, exploracao e resgate.
  • O voo nao e salvo. Nao desligue durante a gravacao na base. O progresso anterior valido continua compativel; a colecao usa um registro separado.

7. Experimente os contratos especiais

Ao escolher uma missao livre, aparece um resumo. B alterna entre normal e especial. A abre uma explicacao curta das etapas; nessa tela A decola e B volta ao contrato. Entrega especial exige voltar sem impactos; exploracao especial exige voltar com pelo menos 85 de combustivel; resgate especial exige concluir a ida e a volta em ate 150 segundos de voo. Pausas e conversas nao contam. O painel B mostra o progresso do desafio. Falhar no desafio nao tira a recompensa normal da missao.

  • Cada contrato especial cumprido rende mais 40 creditos e 20 XP. A primeira conquista de cada tipo rende mais 20 creditos e uma medalha permanente. Consulte Diario e pressione B para alternar entre resumo e medalhas; A volta ao menu.
  • Medalhas e recompensas sao salvas ao voltar a base. Campanha e tutorial nao recebem esses desafios. O progresso anterior continua compativel; um voo interrompido continua sem ser retomado.

8. Conheca a nova interface visual

Todas as telas usam uma faixa de titulo e comandos na parte inferior. O menu mantem as quatro opcoes, agora com icones e marcador lateral de posicao. Durante o voo, F indica combustivel, E escudo e C casco; cada valor tem uma pequena barra. O painel de pausa apresenta barras maiores. Cantos ao redor do destino indicam proximidade para interagir e a exploracao mostra uma barra de leitura. O circulo temporario ao redor da nave indica impacto, nao um escudo extra.

  • O Diario apresenta progresso de patente, capitulos e reliquias com marcadores. As medalhas continuam na segunda pagina. Os encontros mostram retratos diferentes e ambas as escolhas, com a atual destacada.
  • Os controles, as regras, o som discreto do motor e o formato do salvamento foram mantidos. O planeta continua fixo no mapa e o radar mantem a orientacao corrigida, com asteroides vermelhos.

9. Desenvolva sua tripulacao

Cada missao recebe apoio remoto automatico de um especialista. Ivo acompanha entregas e reduz o dano de cada impacto em 2 pontos por grau de habilidade. NOX acompanha exploracao e reduz o tempo de leitura. Nara acompanha resgates e reduz o consumo de combustivel durante todo o voo. A preparacao das missoes livres mostra quem acompanha voce. Nao ha novos botoes nem opcoes no menu principal.

  • Cada tres missoes concluidas do mesmo tipo aumentam um nivel de vinculo, ate tres niveis. Cada especialista inicia com uma habilidade basica. Ivo reduz entre 2 e 8 pontos de dano, NOX reduz a leitura de 2 segundos para entre 1,8 e 1,2 segundo, Nara economiza entre 6 e 24 por cento de combustivel. Ivo nao impede impactos nem evita danos a carga.
  • No Diario, B percorre resumo, medalhas, Ivo, NOX e Nara. As fichas mostram o efeito atual e tres marcadores de vinculo. A volta ao menu. Campanha e missoes livres desenvolvem vinculos; o tutorial nao concede progresso.
  • Vinculos sao gravados ao concluir missoes junto ao progresso anterior. Uma derrota nao aumenta vinculos. O apoio remoto esta disponivel mesmo antes de conhecer os personagens na campanha. Nao desligue durante o salvamento.

10. Entenda as novas rotas livres

Entrega tem duas colonias; exploracao tem tres pontos de leitura; resgate tem reparo antes do reboque. Siga sempre a seta e o contador ENT 1/2 ou SCAN 1/3. Ao terminar as etapas, a seta aponta para a base. Todas as recompensas continuam sendo recebidas e salvas somente ao voltar e pressionar A. Campanha e escola preservam os objetivos anteriores.

  • Nao ha novos menus principais. Leia os detalhes antes de decolar. B continua pausando o voo e A continua sendo a unica acao.
  • No Chromebook, baixe firmware.uf2 pelo Deploy, entre no modo BOOTSEL e copie o arquivo para a unidade RPI-RP2 no aplicativo Arquivos. Nao e necessario usar Connect. Deixe o joystick solto ao reiniciar.

11. Explore Aurora em voo livre

Escolha Decolar no menu. Voce inicia atracado na ultima estacao visitada. B escolhe e A confirma. A opcao Decolar sai sem missao obrigatoria. B durante o voo pausa; A no painel abre o mapa, B retoma. No mapa, B escolhe um dos tres locais e A volta ao voo. A seta aponta para o local escolhido. Perto dele, A atraca e reabastece e repara gratuitamente. Na estacao, Central / sair retorna ao menu anterior.

  • Aurora oferece uma carga opcional para o Farol. Aceite a carga, decole, escolha Farol no mapa e aceite Entregar a carga. Volte a Aurora e atraque para receber 35 creditos e 20 XP. O contrato nao impede desvios e fica salvo entre partidas.
  • No ferro-velho, recupere ate tres lotes de pecas, de 15 creditos cada. Os lotes nao reaparecem ao reiniciar. Locais visitados aparecem como VISTO no mapa.
  • O comboio percorre uma rota triangular e aparece amarelo no radar. Aproxime-se e pressione A para doar 10 de combustivel e ganhar 25 creditos, recebidos na proxima atracacao. Essa ajuda e unica neste primeiro capitulo do mundo aberto.
  • Radar tem asteroides vermelhos, locais azuis, destino escolhido verde e nave branca. Pontos sobrepostos tem prioridade visual; o radar nao substitui a tela.
  • O mundo aberto inicial abrange apenas Aurora. Campanha e missoes anteriores continuam na Central; Io e Lira ainda usam o formato anterior. Nao ha combate ou troca de equipamento nesta etapa.
  • O checkpoint salva contrato, locais visitados, lotes recuperados, ajuda ao comboio e ultima atracacao. O voo nao e retomado. Reiniciar volta a ultima estacao salva, com recursos restaurados. Nao desligue durante a gravacao. Uma derrota descarta recompensas ainda nao recebidas na estacao.

12. Navegue pelo clima de Aurora

Na carta, B tambem seleciona COMBOIO e ABRIGO. A seta acompanha o comboio em movimento. A nuvem pontilhada representa ions: dentro dela a nave fica mais lenta, sem dano adicional. Estacoes e o abrigo protegem desse efeito. No abrigo, pressione A para ler a ajuda e A novamente para receber ate 20 de combustivel, limitado a capacidade do tanque. A recarga pode ser usada uma vez por decolagem. B sai sem recarregar. O clima e o comboio param durante os menus. Na carta, o circulo sem marcador indica a nuvem. Desvie dela para manter a velocidade. Progresso e recompensas continuam sendo salvos nas atracacoes.

13. Use as novas rotas e o piloto

No voo livre, B pausa e A abre a carta. Use o joystick nos dois sentidos ou B para selecionar destinos. Sinais lidos saem da carta; apenas o proximo sinal aparece. A abre uma tela separada: A inicia o piloto automatico e B retoma manualmente. Solte o joystick ao iniciar. O piloto consome combustivel, tenta contornar asteroides e para perto do destino; use A para interagir ou atracar. Mover o joystick cancela o piloto. Impactos tambem cancelam; nao e garantia de viagem sem danos.

  • Depois do primeiro capitulo, IVO aparece na carta enquanto o capitulo 2 estiver pendente. A perto do SOS oferece carona. Aceite e atraque em Aurora para concluir, receber 45 creditos e 25 XP e acender boias de rota. Sem prazo oculto.
  • As cargas variam por viagem. Oficina paga no Farol o premio base, 15 XP e 2 apoio; jardim paga 20 XP e 3 apoio; baterias pagam 25 XP e 1 apoio. Todas acendem boias e ajudam a ampliacao visual existente do Farol. Sucata paga o premio base mais 20 creditos e 5 XP, sem apoio. Confira os valores antes de confirmar. A escolha e feita antes de conectar o cabo. Outras estacoes nao encerram o reboque.
  • O piloto nao atraca, nao resolve conversas e nao torna a nave invulneravel. Carona de Ivo, carga e posicao do voo nao sao retomadas ao reiniciar; recompensas e capitulo sao salvos na atracacao.

14. Aceite contratos locais renovaveis

Depois da primeira entrega de Lia e do retorno a Aurora, escolha Contratos locais na estacao. No ferro-velho, recupere primeiro os tres lotes antigos. Joystick ou B percorrem tres ofertas e Voltar; A confirma. Suprimentos paga 30Cr e 12XP. Rota economica paga 35Cr e 12XP, mais 15Cr se chegar com pelo menos 50 de combustivel. Circuito de pecas exige atracar na escala indicada antes do destino e paga 55Cr e 20XP. O pagamento acontece automaticamente ao atracar no destino. Entregar no Farol tambem concede 1 apoio. Nao ha prazo nem multa.

  • Apenas um contrato local fica ativo. Na estacao, Contrato / acompanhar permite restaurar o alvo com A ou abrir o cancelamento com B; confirme o cancelamento com A ou mantenha com B.
  • Contrato e escala ficam salvos; reiniciar nao retoma o voo. A carta pode acompanhar outros destinos sem apagar o contrato. Reabastecimento nas estacoes continua gratuito.
  • Nao desligue durante o salvamento. Nao instale firmware antigo com contrato novo ativo; o formato antigo nao reconhece os novos codigos de tarefa.

15. Use o clique central como A

Pressione o centro do joystick para fazer a mesma acao do botao A em qualquer tela. Segurar o centro tambem equivale a segurar A. O botao A continua funcionando. Nao precisa adicionar fios. Onde a tela indicar A, voce pode usar qualquer um dos dois. Solte ambos antes de confirmar novamente.

  • Teste uma confirmacao no menu, uma interacao no voo e uma acao de segurar no tutorial. Manter pressionado ao mudar de tela nao deve confirmar a tela seguinte.
  • Pressionar A e o centro juntos conta como uma unica acao. B e os movimentos do joystick continuam iguais.

Deploy the firmware

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#include <Adafruit_NeoPixel.h>
#include <math.h>
#include <EEPROM.h>
#include <stddef.h>
#include <hardware/pwm.h>
#include <hardware/clocks.h>
Adafruit_SSD1306 screen(128,64,&Wire1,-1);
Adafruit_NeoPixel matrix(25,7,NEO_GRB+NEO_KHZ800);
struct Button {
 int pin;
 bool raw=false,down=false,pressed=false;
 uint32_t at=0;
 void begin(){pinMode(pin,INPUT_PULLUP);}
 void tick(uint32_t t){
  pressed=false;
  bool v=digitalRead(pin)==LOW;
  if(v!=raw){raw=v;at=t;}
  if(t-at>=25&&down!=raw){down=raw;pressed=down;}
 }
};
// Debounce each physical switch independently, then merge into action A.
Button physicalA{5},stickClick{22},a{5},b{6};
#include "campaign.h"
#include "expedition.h"
#include "contracts.h"
#include "crew.h"
#include "frontier.h"
#include "openworld.h"
enum Mode{BASE,FLIGHT,PANEL,LOST,LESSON,JOURNAL,STORY,ENCOUNTER,CONTRACT,REPORT,BRIEF,WORLDMAP,WORLDPORT,ECHOSIGNAL,ACTIVITY,SETTINGS,DOCKREPORT};Mode mode=BASE;
Mode settingsReturn=BASE;
int settingRow=0,volumeLevel=2,lightLevel=5;
bool settingsChanged=false;
void openSettings(Mode back){settingsReturn=back;settingRow=0;settingsChanged=false;mode=SETTINGS;}
int lesson=0; bool training=false; float moved=0; bool actionPending=false;
float x=50,y=150,vx=0,vy=0,fuel=100,shield=100,hull=100;
int cx=2048,cy=2048,selection=0,mission=0,credits=0,completed=0,level=0;
bool contractWon=false;
bool cargo=false,soundOn=true,oled=false,dirty=true;
uint32_t frameAt=0,navAt=0,hitAt=0,noteUntil=0,msgUntil=0;
void tickButtons(uint32_t now){
 physicalA.tick(now);stickClick.tick(now);b.tick(now);
 bool wasDown=a.down;
 a.raw=physicalA.raw||stickClick.raw;
 a.down=physicalA.down||stickClick.down;
 a.pressed=a.down&&!wasDown;
 // Clicking can slightly tilt the stick: do not navigate during confirmation.
 if(stickClick.raw||stickClick.down)navAt=now;
}
const char* message="";
const char* missions[]={"Entrega","Exploracao","Resgate"};
float tx[]={330,280,350},ty[]={65,240,175};
int routeStep=0; bool rescueFixed=false;
bool variedMission(){return !training&&!campaignFlight;}
void routeTarget(){
 if(!variedMission()||mission==2)return;
 const int paths[2][3][2]={{{170,80},{330,180},{330,180}},{{150,235},{245,70},{345,225}}};
 int i=mission==0?min(routeStep,1):min(routeStep,2);
 tx[mission]=paths[mission][i][0];ty[mission]=paths[mission][i][1];
}

int xp=0,rankLevel=1,sector=0,engineLevel=0,tankLevel=0;
int deliveries=0,scans=0,rescues=0;
const char* sectors[]={"Orbita Aurora","Cintura de Io","Nebulosa Lira"};
const char* ranks[]={"Cadete","Piloto","Explorador","Comandante","Capitao"};
int menuPage=0;
int homeSector=0;
int menuCount(){return menuPage==0?4:menuPage==1?7:5;}
bool saveOK=true;
bool storageReady=false;
uint8_t checkpoint[256];
void rememberCheckpoint(){for(int i=0;i<256;i++)checkpoint[i]=EEPROM.read(i);}
void loadGame();
void restoreCheckpoint(){
 chapterCargo=false;
 credits=xp=completed=level=engineLevel=tankLevel=sector=deliveries=scans=rescues=0;
 rankLevel=1;soundOn=true;volumeLevel=2;lightLevel=5;storyChapter=storyChoices=0;
 discoveries=medals=echoBits=0;visitedSites=1;salvaged=worldJob=dockSite=worldClock=0;convoyHelped=false;
 for(int i=0;i<3;i++){crewFlights[i]=0;sectorAid[i]=0;}
 for(int i=0;i<256;i++)EEPROM.write(i,checkpoint[i]);loadGame();
}
extern int tune;
// Versioned checkpoint: progress only, never transient flight state.
struct SaveData {uint32_t magic,version; int32_t cr,xp,done,esc,engine,tank,sector,ent,exp,res,sound; uint32_t checksum;};
uint32_t checksum(const SaveData& d){const uint8_t* b=(const uint8_t*)&d;uint32_t h=2166136261u;for(size_t i=0;i<offsetof(SaveData,checksum);i++)h=(h^b[i])*16777619u;return h;}
// Settings occupy unused bytes 64..75; existing progress records are unchanged.
void storeSettings(){uint32_t v=volumeLevel|(lightLevel<<8),check=v^0x41564C31u;EEPROM.put(64,uint32_t(0x41564C31u));EEPROM.put(68,v);EEPROM.put(72,check);}
void loadSettings(){uint32_t magic,v,check;EEPROM.get(64,magic);EEPROM.get(68,v);EEPROM.get(72,check);if(magic==0x41564C31u&&check==(v^magic)&&(v&255)<=5&&(v>>8)<=5){volumeLevel=v&255;lightLevel=v>>8;}}
void saveGame(){
 SaveData d={0x4F524254,1,credits,xp,completed,level,engineLevel,tankLevel,(campaignFlight||openFlight)?homeSector:sector,deliveries,scans,rescues,soundOn?1:0,0};d.checksum=checksum(d);
 noTone(21);digitalWrite(21,LOW);noteUntil=0;tune=-1;
 storeSettings();EEPROM.put(0,d);storeStory();storeDiscoveries();storeMedals();storeCrew();storeFrontier();storeWorld();storeEcho();bool changed=false;for(int i=0;i<256;i++)if(EEPROM.read(i)!=checkpoint[i]){changed=true;break;}saveOK=!changed||EEPROM.commit();if(saveOK)rememberCheckpoint();Serial.println(saveOK?"Progresso salvo":"Falha ao salvar");
}
void loadGame(){if(!storageReady){EEPROM.begin(256);storageReady=true;rememberCheckpoint();}SaveData d;EEPROM.get(0,d);loadSettings();loadStory();loadDiscoveries();loadMedals();loadCrew();loadFrontier();loadWorld();loadEcho();
 if(d.magic!=0x4F524254||d.version!=1||d.checksum!=checksum(d))return;
 if(d.cr<0||d.xp<0||d.done<0||d.esc<0||d.esc>3||d.engine<0||d.engine>3||d.tank<0||d.tank>3||d.sector<0||d.sector>2||d.ent<0||d.exp<0||d.res<0||d.sound<0||d.sound>1)return;
 credits=d.cr;xp=d.xp;completed=d.done;level=d.esc;engineLevel=d.engine;tankLevel=d.tank;sector=d.sector;deliveries=d.ent;scans=d.exp;rescues=d.res;soundOn=d.sound;
 rankLevel=1;while(rankLevel<5&&xp>=rankLevel*60)rankLevel++;

}

const char* targetAction(){if(variedMission()&&mission==2)return rescueFixed?"A: REBOCAR":"SEGURE A: REPARAR";return mission==0?"A: ENTREGAR":mission==1?"SEGURE A: ESCANEAR":"A: RESGATAR";}
void locateTargets(){
 const float px[3][3]={{330,280,350},{290,350,220},{360,240,320}};
 const float py[3][3]={{65,240,175},{250,90,50},{240,45,110}};
 for(int i=0;i<3;i++){tx[i]=px[sector][i];ty[i]=py[sector][i];}
}
void reward(){
 int gain=20+mission*10+sector*15+(frontierStage(sector)==2?8:0);credits+=gain;xp+=15+mission*5+sector*10;completed++;
 if(crewFlights[mission]<100000)crewFlights[mission]++;
 if(mission==0)deliveries++;else if(mission==1)scans++;else rescues++;
 contractWon=false;if(special){bool success=contract==0?flightHits==0:contract==1?fuel>=85:flightTicks<=3000;contractWon=success;if(success){bool first=!(medals&(1u<<contract));medals|=1u<<contract;bonus+=40+(first?20:0);xp+=20;}}
 credits+=bonus+(mission==0?integrity/10:0);discoveries|=pendingDiscoveries;
 rankLevel=1;while(rankLevel<5&&xp>=rankLevel*60)rankLevel++;
}

const int rocks[][2]={{125,95},{150,190},{200,140},{230,65},{250,210},{310,120},{100,240},{180,265}};
// Non-blocking melodies: flight and buttons stay responsive.
const int notes[][8]={{660,880,0,0,0,0,0,0},{440,660,880,1100,0,0,0,0},{880,1100,1320,0,0,0,0,0},{660,880,1100,1320,1100,1568,0,0},{180,120,80,0,0,0,0,0}};
float speed();
int tune=-1,noteIndex=0; uint32_t nextNote=0,motorAt=0; bool thrust=false;
void beep(int f,int ms){if(soundOn&&volumeLevel>0&&f>0){
 // Dedicated slice for GPIO21: duty controls loudness without changing RGB PWM.
 pwm_config c=pwm_get_default_config();
 pwm_config_set_clkdiv(&c,float(clock_get_hz(clk_sys))/(4096.0f*f));
 pwm_config_set_wrap(&c,4095);pwm_init(pwm_gpio_to_slice_num(21),&c,true);
 const uint16_t duty[]={0,24,64,160,400,2048};
 gpio_set_function(21,GPIO_FUNC_PWM);pwm_set_gpio_level(21,duty[volumeLevel]);
 noteUntil=millis()+ms;
}}
void music(int id){if(!soundOn)return;tune=id;noteIndex=0;nextNote=millis();}
void audioTick(uint32_t now){
 if(noteUntil&&int32_t(now-noteUntil)>=0){noTone(21);digitalWrite(21,LOW);noteUntil=0;}
 if(tune>=0&&int32_t(now-nextNote)>=0){int n=notes[tune][noteIndex++];if(!n||noteIndex>=8){tune=-1;}else{beep(n,85);nextNote=now+115;}}
 if(soundOn&&tune<0&&!noteUntil&&mode==FLIGHT&&thrust&&now-motorAt>1100){motorAt=now;beep(110,16);}
}
void say(const char* s){message=s;msgUntil=millis()+2200;dirty=true;}
float dist(float ax,float ay,float bx,float by){return hypotf(ax-bx,ay-by);}
float speed(){return hypotf(vx,vy);}
float axis(int pin,int center){float n=(analogRead(pin)-center)/1800.0f;return fabsf(n)<0.18f?0:constrain(n,-1.0f,1.0f);}
void resetShip(){x=50;y=150;vx=vy=0;fuel=100+tankLevel*25;shield=hull=100;cargo=false;}
void launch(){openFlight=false;routeStep=0;rescueFixed=false;flightTicks=0;flightHits=0;locateTargets();resetShip();bonus=0;scanTicks=0;integrity=100;pendingDiscoveries=0;encounterDone=false;encounterKind=random(3);signalX=140+random(45);signalY=110+random(80);mode=FLIGHT;training=false;routeTarget();say(mission==0?"Carga: evite impactos":mission==1?"No alvo: segure A":"Resgate e escolte");music(1);}
// Fixed 6x8 font: never wrap into another UI region.
void text(int x,int y,const char* s){screen.setCursor(x,y);int n=max(0,(128-x)/6);while(*s&&*s!='\n'&&n-->0)screen.write(*s++);}
void prose(int x,int y,int width,int lines,const char* s){
 int cap=constrain(width/6,1,21);for(int row=0;row<lines&&*s;row++){
 char buf[22];int n=0;while(s[n]&&s[n]!='\n'&&n<cap)n++;
 if(n==cap&&s[n]&&s[n]!='\n'){int k=n;while(k>0&&s[k]!=' ')k--;if(k>0)n=k;}
 memcpy(buf,s,n);buf[n]=0;text(x,y+row*8,buf);s+=n;if(*s=='\n'||*s==' ')s++;
 }
}
int radarIndex(int col,int row){
 // Rotate logical radar coordinates 180 degrees for the board's viewing orientation.
 col=4-col;row=4-row;
 return row*5+((row&1)?4-col:col);
}
void radar(){matrix.clear();if(openFlight&&(mode==FLIGHT||mode==PANEL||mode==WORLDMAP)){
 for(auto &r:rocks){int c=lroundf((r[0]-x)/65)+2,rr=lroundf((r[1]-y)/65)+2;if(c>=0&&c<5&&rr>=0&&rr<5)matrix.setPixelColor(radarIndex(c,rr),matrix.Color(18,0,0));}
 for(int i=0;i<3;i++){int c=constrain(int(lroundf((siteX[i]-x)/65))+2,0,4),r=constrain(int(lroundf((siteY[i]-y)/65))+2,0,4);matrix.setPixelColor(radarIndex(c,r),i==worldFocus?matrix.Color(0,14,0):matrix.Color(0,0,12));}
 int en=echoNext();if(en>=0){int ec=constrain(int(lroundf((echoX[en]-x)/65))+2,0,4),er=constrain(int(lroundf((echoY[en]-y)/65))+2,0,4);matrix.setPixelColor(radarIndex(ec,er),matrix.Color(12,0,16));}
 int c=lroundf((convoyX-x)/65)+2,r=lroundf((convoyY-y)/65)+2;if(c>=0&&c<5&&r>=0&&r<5)matrix.setPixelColor(radarIndex(c,r),matrix.Color(14,8,0));int tc=constrain(int(lroundf((focusX()-x)/65))+2,0,4),tr=constrain(int(lroundf((focusY()-y)/65))+2,0,4);matrix.setPixelColor(radarIndex(tc,tr),matrix.Color(0,14,0));matrix.setPixelColor(12,matrix.Color(12,12,12));
 }else if(mode==FLIGHT||mode==PANEL){
 // Same 65-world-unit cells for rocks, target and station. Do not clamp distant rocks to edges.
 for(auto &r:rocks){int c=int(lroundf((r[0]-x)/65))+2,rr=int(lroundf((r[1]-y)/65))+2;if(c>=0&&c<5&&rr>=0&&rr<5)matrix.setPixelColor(radarIndex(c,rr),matrix.Color(18,0,0));}
 if(!training&&!encounterDone){int c=int(lroundf((signalX-x)/65))+2,r=int(lroundf((signalY-y)/65))+2;if(c>=0&&c<5&&r>=0&&r<5)matrix.setPixelColor(radarIndex(c,r),matrix.Color(14,8,0));}
 int col=constrain(int(lroundf((tx[mission]-x)/65))+2,0,4),row=constrain(int(lroundf((ty[mission]-y)/65))+2,0,4);
 if(!cargo)matrix.setPixelColor(radarIndex(col,row),matrix.Color(0,12,0));
 col=constrain(int(lroundf((50-x)/65))+2,0,4);row=constrain(int(lroundf((150-y)/65))+2,0,4);matrix.setPixelColor(radarIndex(col,row),matrix.Color(0,0,12));matrix.setPixelColor(12,matrix.Color(12,12,12));}
 if(mode==SETTINGS){matrix.clear();for(int i=0;i<25;i++)matrix.setPixelColor(i,matrix.Color(i%3==0?18:0,i%3==1?18:0,i%3==2?18:0));}
 // Scale fresh frame colors; never rescale an already dimmed buffer.
 for(int i=0;i<25;i++){uint32_t c=matrix.getPixelColor(i);matrix.setPixelColor(i,((c>>16)&255)*lightLevel/5,((c>>8)&255)*lightLevel/5,(c&255)*lightLevel/5);}
 matrix.show();analogWrite(13,hull<35?40:0);analogWrite(11,hull>=35?18:0);analogWrite(12,mode==BASE?24:0);}
#include "visuals.h"
#include "openworld_ui.h"
void draw(){if(!oled)return;screen.clearDisplay();screen.setTextWrap(false);screen.setTextSize(1);screen.setTextColor(SSD1306_WHITE);
 if(mode==SETTINGS){
 uiHeader("AJUSTES / CONFORTO");
 const char* labels[]={"Volume","Brilho matriz","Salvar e voltar"};
 for(int i=0;i<3;i++){int yy=14+i*13;if(settingRow==i){screen.fillRoundRect(0,yy,128,11,2,1);screen.setTextColor(0);}text(3,yy+2,labels[i]);if(i<2){screen.setCursor(99,yy+2);screen.print((i==0?volumeLevel:lightLevel)*20);screen.print("%");}screen.setTextColor(1);}
 uiFooter(saveOK?"B: item A: + / sair":"ERRO SAVE: A repete");
 }
 else if(mode==ACTIVITY)uiActivity();
 else if(mode==ECHOSIGNAL)uiEcho();
 else if(mode==WORLDMAP)uiWorldMap();
 else if(mode==WORLDPORT)uiWorldPort();
 else if(mode==FLIGHT&&openFlight)uiWorldFlight();
 else if(mode==LESSON)uiLesson();
 else if(mode==STORY)uiStory();
 else if(mode==ENCOUNTER)uiEncounter();
 else if(mode==CONTRACT)uiContract();
 else if(mode==BRIEF)uiBrief();
 else if(mode==DOCKREPORT)uiDockReport();
 else if(mode==REPORT)uiReport();
 else if(mode==BASE)uiBase();
 else if(mode==JOURNAL)uiJournal();
 else if(mode==LOST)uiLost();
 else if(mode==PANEL)uiPanel();
 else { // World scrolls around the ship; HUD occupies the top and bottom rows.
  for(int i=0;i<30;i++){int sx=(i*73+17)%400-int(x)+64,sy=(i*47+11)%300-int(y)+34;if(sx>=0&&sx<128&&sy>9&&sy<55)screen.drawPixel(sx,sy,1);}
  // Sector-specific world details, never an overlay following the player.
  if(sector==1){for(int i=0;i<18;i++){int sx=90+i*14-int(x)+64,sy=40+(i*37)%225-int(y)+34;if(sx>0&&sx<128&&sy>13&&sy<51)screen.drawRect(sx,sy,2+(i%3),2,1);}}
  if(sector==2){for(int i=0;i<12;i++){int sx=70+(i*31)%300-int(x)+64,sy=30+(i*19)%240-int(y)+34;if(sx>3&&sx<125&&sy>14&&sy<50){screen.drawLine(sx-2,sy,sx+2,sy,1);screen.drawPixel(sx,sy-2,1);}}}
  // Fixed scenery in world coordinates, not a ship-following parallax overlay.
  int planetX=365-int(x)+64,planetY=35-int(y)+34;
  if(planetX>=-16&&planetX<144&&planetY>=-16&&planetY<80){
   screen.drawCircle(planetX,planetY,12,1);screen.drawCircle(planetX+4,planetY-2,4,1);screen.drawLine(planetX-16,planetY+5,planetX+16,planetY-3,1);
   for(int py=-9;py<=9;py+=3)for(int px=-10;px<0;px+=3)if(px*px+py*py<125)screen.drawPixel(planetX+px,planetY+py,1);
  }
  auto point=[&](float wx,float wy,int kind){int sx=int(wx-x)+64,sy=int(wy-y)+34;if(sx< -6||sx>133||sy<14||sy>49)return;if(kind==0){screen.drawRoundRect(sx-4,sy-5,9,11,2,1);screen.drawRect(sx-12,sy-4,6,9,1);screen.drawRect(sx+7,sy-4,6,9,1);screen.drawLine(sx-7,sy,sx+7,sy,1);screen.drawPixel(sx,sy-2,1);}else if(kind==1){screen.drawLine(sx-4,sy-2,sx-1,sy-5,1);screen.drawLine(sx-1,sy-5,sx+4,sy-2,1);screen.drawLine(sx+4,sy-2,sx+3,sy+4,1);screen.drawLine(sx+3,sy+4,sx-3,sy+3,1);screen.drawLine(sx-3,sy+3,sx-4,sy-2,1);screen.drawPixel(sx-1,sy-1,1);screen.drawLine(sx+1,sy+1,sx+3,sy+1,1);}else{if(mission==0){screen.drawRect(sx-6,sy-5,13,11,1);screen.drawLine(sx-6,sy-5,sx+6,sy+5,1);screen.drawLine(sx+6,sy-5,sx-6,sy+5,1);}else if(mission==1){screen.drawCircle(sx,sy,7,1);screen.drawCircle(sx,sy,3,1);screen.drawFastHLine(sx-10,sy,4,1);screen.drawFastHLine(sx+7,sy,4,1);}else{screen.drawRoundRect(sx-6,sy-3,13,7,2,1);screen.drawLine(sx,sy-6,sx,sy+6,1);screen.drawPixel(sx-9,sy,1);screen.drawPixel(sx+9,sy,1);}}};
  if(!training&&!encounterDone){int sx=int(signalX-x)+64,sy=int(signalY-y)+34;if(sx>5&&sx<122&&sy>17&&sy<48){int pulse=(millis()/350)%2;screen.drawCircle(sx,sy,4+pulse*2,1);text(sx-2,sy-3,"?");}}
  if(!training&&cargo&&mission==2){screen.drawRoundRect(48,39,7,5,1,1);screen.drawLine(53,40,60,36,1);}
  for(auto &r:rocks)point(r[0],r[1],1);point(50,150,0);if(!cargo)point(tx[mission],ty[mission],2);
  // Rotate the spacecraft toward its movement, with animated exhaust.
  float angle=speed()>0.04f?atan2f(vy,vx):-1.5708f;
  float ux=cosf(angle),uy=sinf(angle);
  int noseX=64+int(ux*7),noseY=34+int(uy*7);
  int leftX=64-int(ux*4+uy*5),leftY=34-int(uy*4-ux*5);
  int rightX=64-int(ux*4-uy*5),rightY=34-int(uy*4+ux*5);
  screen.fillTriangle(noseX,noseY,leftX,leftY,rightX,rightY,1);
  screen.drawLine(64-int(ux*2+uy*2),34-int(uy*2-ux*2),noseX-int(ux*2),noseY-int(uy*2),0);
  // Engine nacelles and shield impact ring do not change the collision radius.
  screen.drawCircle(leftX,leftY,1,1);screen.drawCircle(rightX,rightY,1,1);
  if(hitAt && millis()-hitAt<450)screen.drawCircle(64,34,10,1);
  if(thrust){int tail=8+(millis()/100)%3;screen.drawLine(64-int(ux*5),34-int(uy*5),64-int(ux*tail),34-int(uy*tail),1);}
  screen.drawFastHLine(0,10,128,1);screen.drawFastHLine(0,53,128,1);
  screen.fillRect(0,0,128,9,0);screen.setCursor(0,0);if(training&&lesson==1)screen.print("1/4 Mova o joystick");
  else if(training)screen.print(cargo?"4/4 Volte para BASE":"3/4 Va ate o ALVO");
  else {for(int i=0;i<3;i++){int xx=i*43;float val=i==0?fuel:i==1?shield:hull;text(xx,0,i==0?"F":i==1?"E":"C");screen.setCursor(xx+7,0);screen.print(int(val));uiBar(xx+26,0,16,8,val,i==0?100+tankLevel*25:100);}}
  float gx=cargo?50:tx[mission],gy=cargo?150:ty[mission];
  float dx=gx-x,dy=gy-y,dd=hypotf(dx,dy);
  if(dd>24){float ux=dx/dd,uy=dy/dd;int ex=64+int(ux*20),ey=34+int(uy*17);screen.drawLine(64,34,ex,ey,1);screen.drawLine(ex,ey,ex-int(ux*5-uy*3),ey-int(uy*5+ux*3),1);screen.drawLine(ex,ey,ex-int(ux*5+uy*3),ey-int(uy*5-ux*3),1);}
  if(dd<24){int bx=64+int(dx),by=34+int(dy);if(by>19&&by<46&&bx>8&&bx<120){for(int side=-1;side<=1;side+=2){screen.drawLine(bx+side*9,by-7,bx+side*5,by-7,1);screen.drawLine(bx+side*9,by-7,bx+side*9,by-3,1);screen.drawLine(bx+side*9,by+7,bx+side*5,by+7,1);}}}
  if(!training&&!cargo&&scanTicks>0&&(mission==1||(variedMission()&&mission==2&&!rescueFixed)))uiBar(38,46,52,6,scanTicks,mission==1?scanRequired():40);
  screen.fillRect(0,55,128,9,0);screen.setCursor(0,56);if(training&&lesson==1)screen.print("Mova ate completar");
  else if(!training&&!encounterDone&&dist(x,y,signalX,signalY)<22)screen.print("A: INVESTIGAR SINAL");
  else if(dd<24){if(!training&&!cargo&&(mission==1||(variedMission()&&mission==2&&!rescueFixed))){screen.print(mission==1?"LEITURA ":"REPARO ");screen.print(scanTicks*100/(mission==1?scanRequired():40));screen.print("% segure A");}else screen.print(cargo?"APERTE A: ATRACAR":targetAction());}
  else {if(!cargo&&variedMission()){screen.print(mission==0?"ENT ":mission==1?"SCAN ":"SOS ");if(mission<2){screen.print(routeStep+1);screen.print(mission==0?"/2 ":"/3 ");}}else screen.print(cargo?"BASE ":"ALVO ");screen.print(int(dd));screen.print(" >");}
 }
 if(millis()<msgUntil&&mode==BASE){screen.fillRect(0,54,128,10,0);screen.drawFastHLine(0,54,128,1);text(1,56,message);}
 if(millis()<msgUntil&&mode==FLIGHT){screen.fillRoundRect(0,21,128,23,3,0);screen.drawRoundRect(0,21,128,23,3,1);char line[21];size_t len=strlen(message);size_t cut=min(size_t(20),len);if(len>20){for(size_t i=cut;i>0;i--)if(message[i]==' '){cut=i;break;}}memcpy(line,message,cut);line[cut]=0;text(4,len>20?24:29,line);if(len>20){size_t next=cut+(message[cut]==' '?1:0);strncpy(line,message+next,20);line[20]=0;text(4,34,line);}}screen.display();}
void setup(){Serial.begin(115200);physicalA.begin();stickClick.begin();b.begin();pinMode(21,OUTPUT);digitalWrite(21,LOW);for(int p:{13,11,12}){pinMode(p,OUTPUT);digitalWrite(p,LOW);}analogWriteResolution(8);analogReadResolution(12);loadGame();matrix.begin();matrix.clear();matrix.show();Wire1.setSDA(14);Wire1.setSCL(15);Wire1.begin();Wire1.beginTransmission(0x3C);if(Wire1.endTransmission()==0)oled=screen.begin(SSD1306_SWITCHCAPVCC,0x3C,false,false);
 long sx=0,sy=0;for(int i=0;i<32;i++){sx+=analogRead(26);sy+=analogRead(27);delay(3);}cx=sx/32;cy=sy/32;randomSeed(micros());Serial.println(oled?"Simulador pronto":"OLED nao encontrado");radar();draw();music(0);}
void loop(){static Mode previousMode=BASE;static bool inputGuard=false;Mode inputMode=mode;uint32_t now=millis();tickButtons(now);if(inputGuard){a.pressed=b.pressed=false;if(!a.down&&!b.down&&!a.raw&&!b.raw)inputGuard=false;}audioTick(now);if(a.pressed||b.pressed)beep(700,35);if(msgUntil&&int32_t(now-msgUntil)>=0){msgUntil=0;dirty=true;}
 if(mode==SETTINGS){
 if(b.pressed){settingRow=(settingRow+1)%3;dirty=true;}
 float j=axis(27,cy);
 bool step=fabsf(j)>0.4f&&now-navAt>230;
 if(settingRow<2&&(a.pressed||step)){
 int &v=settingRow==0?volumeLevel:lightLevel;
 v=a.pressed?(v+1)%6:constrain(v+(j>0?1:-1),0,5);navAt=now;
 if(settingRow==0){soundOn=volumeLevel>0;tune=-1;noTone(21);digitalWrite(21,LOW);noteUntil=0;beep(440,100);}
 settingsChanged=true;dirty=true;
 }else if(settingRow==2&&a.pressed){if(settingsChanged)saveGame();if(saveOK){mode=settingsReturn;settingsChanged=false;}dirty=true;}
 }
 else if(mode==ACTIVITY){bool next=b.pressed;if(activityPage==10){float j=axis(27,cy);if(fabsf(j)<.3f)j=-axis(26,cx);if(fabsf(j)>.4f&&now-navAt>280){localOffer=(localOffer+(j>0?1:3))%4;navAt=now;dirty=true;}}tickActivity(a.pressed,next);if(a.pressed||next)dirty=true;}
 else if(mode==ECHOSIGNAL){tickEcho(a.pressed,b.pressed);dirty=true;}
 else if(mode==WORLDMAP){
 if(!focusAvailable(worldFocus)||(worldFocus==9&&storyChapter!=1))moveWorldFocus(1);
 float j=axis(27,cy);if(fabsf(j)<.3f)j=-axis(26,cx);
 static int heldDir=0;static uint32_t repeatAt=0;int dir=!(stickClick.raw||stickClick.down)&&fabsf(j)>.4f?(j>0?1:-1):0;bool navStep=dir&&(dir!=heldDir||int32_t(now-repeatAt)>=0);if(navStep){repeatAt=now+(dir!=heldDir?380:170);}heldDir=dir;
 if(b.pressed||navStep){moveWorldFocus(b.pressed||j>0?1:-1);navAt=now;dirty=true;}
 if(a.pressed){activityPage=8;mode=ACTIVITY;dirty=true;}}

 else if(mode==WORLDPORT){float j=axis(27,cy);if(fabsf(j)<.3f)j=-axis(26,cx);if(b.pressed||(fabsf(j)>.4f&&now-navAt>280)){portChoice=(portChoice+(b.pressed||j>0?1:3))%4;navAt=now;dirty=true;}if(a.pressed){worldPortAction();dirty=true;}}
 else if(mode==LESSON){if(a.pressed){if(lesson==0){resetShip();mission=0;locateTargets();training=true;lesson=1;moved=0;mode=FLIGHT;}else if(lesson==2){resetShip();lesson=3;mode=FLIGHT;}else if(lesson==5){training=false;mode=BASE;}dirty=true;}}
 else if(mode==STORY){
 if(storyStep==0){if(b.pressed){mode=BASE;dirty=true;}else if(a.pressed){storyStep=1;storyOption=0;dirty=true;}}
 else if(storyStep==1){if(b.pressed){storyOption=1-storyOption;dirty=true;}if(a.pressed){
 storyChoices=(storyChoices&~(1<<storyShown))|(storyOption<<storyShown);
 special=false;homeSector=sector;mission=chapters[storyShown].job;campaignFlight=true;launch();sector=chapters[storyShown].region;locateTargets();say("Siga a seta ao ALVO");dirty=true;
 }}else if(a.pressed||b.pressed){mode=BASE;menuPage=0;selection=0;dirty=true;}
 }
 else if(mode==ENCOUNTER){if(b.pressed){encounterChoice=1-encounterChoice;dirty=true;}if(a.pressed){
 if(encounterKind==0){if(encounterChoice==0)bonus+=20;else hull=fminf(100,hull+30);}
 else if(encounterKind==1){if(encounterChoice==0&&fuel>=10){fuel-=10;bonus+=30;}else if(encounterChoice==0){say("Combustivel insuf.");}}
 else {if(encounterChoice==0){pendingDiscoveries|=1u<<sector;bonus+=10;say(relics[sector]);}else{shield=fmaxf(0,shield-15);bonus+=35;}}
 encounterDone=true;mode=FLIGHT;music(2);dirty=true;
 }}
 else if(mode==CONTRACT){if(b.pressed){special=!special;dirty=true;}if(a.pressed){mode=BRIEF;msgUntil=0;dirty=true;}}
 else if(mode==BRIEF){if(b.pressed){mode=CONTRACT;dirty=true;}else if(a.pressed){launch();say("Objetivo: siga a seta");}}
 else if(mode==DOCKREPORT){if(b.pressed){dockPage=(dockPage+1)%3;dirty=true;}else if(a.pressed){if(!saveOK)saveGame();else if(dockPage<2)dockPage++;else mode=WORLDPORT;dirty=true;}}
 else if(mode==REPORT){if(a.pressed&&!saveOK){saveGame();dirty=true;}else if(a.pressed){mode=campaignFlight?STORY:BASE;if(campaignFlight){storyStep=2;campaignFlight=false;}dirty=true;}}
 else if(mode==JOURNAL){if(b.pressed){journalPage=(journalPage+1)%6;dirty=true;}if(a.pressed){mode=BASE;dirty=true;}}
 else if(mode==BASE){float j=axis(26,cx);if(fabsf(j)<0.2f)j=axis(27,cy);if(fabsf(j)>0.3f&&now-navAt>250){selection=(selection+(j>0?1:menuCount()-1))%menuCount();navAt=now;beep(600,30);dirty=true;}
 if(b.pressed){selection=(selection+1)%menuCount();dirty=true;}
 if(a.pressed){dirty=true;
 if(menuPage==0){if(selection==0){startWorld();}else if(selection==1){menuPage=1;selection=0;}else if(selection==2)mode=JOURNAL;else{openFlight=false;campaignFlight=false;lesson=0;special=false;training=true;mode=LESSON;}}
 else if(menuPage==1){if(selection==0){storyShown=storyChapter<chapterCount?storyChapter:chapterCount-1;storyStep=storyChapter<chapterCount?0:2;mode=STORY;}
 else if(selection<4){campaignFlight=false;mission=selection-1;contract=mission;special=false;mode=CONTRACT;}else if(selection==4){menuPage=2;selection=0;}else if(selection==5){openSettings(BASE);}else{menuPage=0;selection=0;}}
 else if(selection==4){menuPage=0;selection=0;}
 else if(selection==0){int unlocked=rankLevel>=4?3:rankLevel>=2?2:1;if(unlocked==1)say("Setor 2: nivel 2");else{sector=(sector+1)%unlocked;saveGame();say(saveOK?sectors[sector]:"Erro ao salvar!");}}
 else {int* upgrade=selection==1?&level:selection==2?&engineLevel:&tankLevel;int cost=selection==1?30:selection==2?40:35;if(*upgrade>=3)say("Melhoria no maximo");else if(credits<cost)say("Faltam creditos");else{credits-=cost;(*upgrade)++;saveGame();say(saveOK?"Nave melhorada!":"Erro ao salvar!");music(2);}}
 }}
 else if(mode==LOST){if(a.pressed){openFlight=false;campaignFlight=false;restoreCheckpoint();bonus=0;escortAidPending=0;towing=false;escortActive=false;rescuingIvo=false;autoPilot=false;actionPending=false;training=false;special=false;resetShip();mode=BASE;menuPage=0;selection=0;dirty=true;}}
 else if(mode==PANEL){if(openFlight){if(b.pressed){mode=FLIGHT;dirty=true;}if(a.pressed){mode=WORLDMAP;dirty=true;}}else {if(a.pressed){soundOn=!soundOn;saveGame();if(!soundOn){tune=-1;noteUntil=0;noTone(21);digitalWrite(21,LOW);}else music(0);dirty=true;}if(b.pressed){mode=FLIGHT;dirty=true;}}}
 else if(b.pressed){mode=PANEL;dirty=true;}
 if(mode!=previousMode){inputGuard=true;navAt=now;previousMode=mode;}
 // A menu confirmation must never become a flight interaction in the same loop.
 if(mode==FLIGHT && inputMode==FLIGHT && a.pressed)actionPending=true;
 if(mode!=FLIGHT)actionPending=false;
 if(now-frameAt>=50){frameAt=now;if(mode==FLIGHT&&openFlight){tickWorld(now);dirty=true;}else if(mode==FLIGHT){if(!training)flightTicks++;float ax=axis(27,cy),ay=-axis(26,cx);thrust=(fabsf(ax)+fabsf(ay)>0.1f)&&fuel>0;if(fuel>0){float power=(0.055f+engineLevel*0.009f)*(!training&&cargo&&mission==2?0.8f:1.0f);vx+=ax*power;vy+=ay*power;if(!training)fuel=fmaxf(0,fuel-(fabsf(ax)+fabsf(ay))*0.013f*(mission==2?1.0f-0.06f*crewPower(2):1.0f));}vx*=0.88f;vy*=0.88f;float s=speed();if(s>2){vx*=2/s;vy*=2/s;}x+=vx;y+=vy;if(x<8||x>392){x=constrain(x,8.0f,392.0f);vx*=-0.4f;}if(y<8||y>292){y=constrain(y,8.0f,292.0f);vy*=-0.4f;}
 for(auto &r:rocks)if(!training&&dist(x,y,r[0],r[1])<9&&now-hitAt>1200){hitAt=now;flightHits++;if(!cargo&&mission==0)integrity=max(0,integrity-20);float damage=18+sector*4-level*3-(frontierStage(sector)>=1?2:0);if(mission==0)damage=fmaxf(1,damage-2*crewPower(0));if(shield>=damage)shield-=damage;else{hull-=damage-shield;shield=0;}vx*=-0.7f;vy*=-0.7f;beep(160,140);say("Impacto!");}
 if(training&&lesson==1){moved+=speed();if(moved>12){vx=vy=0;lesson=2;mode=LESSON;dirty=true;}}
 if(!training&&!cargo&&mission==1&&dist(x,y,tx[mission],ty[mission])<24&&a.down&&!inputGuard&&inputMode==FLIGHT){scanTicks++;if(scanTicks>=scanRequired()){vx=vy=0;scanTicks=0;if(variedMission()&&routeStep<2){routeStep++;routeTarget();say("Leitura OK! Proximo");}else{cargo=true;say("Mapa OK! Volte BASE");}music(2);}}
 if(variedMission()&&mission==2&&!cargo&&!rescueFixed){
 if(dist(x,y,tx[mission],ty[mission])<24&&a.down&&!inputGuard&&inputMode==FLIGHT){scanTicks++;if(scanTicks>=40){rescueFixed=true;scanTicks=0;vx=vy=0;say("Reparo OK! A: rebocar");music(2);}}
 else scanTicks=0;
 }
 if(actionPending){actionPending=false;
 if(training&&lesson==1)say("Mova o joystick");
 else if(!training&&!encounterDone&&dist(x,y,signalX,signalY)<22){mode=ENCOUNTER;encounterChoice=0;vx=vy=0;msgUntil=0;}
 else if(!training&&!cargo&&mission==1&&dist(x,y,tx[mission],ty[mission])<24){}
 else if(variedMission()&&!cargo&&mission==2&&!rescueFixed&&dist(x,y,tx[mission],ty[mission])<24){}
 else if(!cargo&&dist(x,y,tx[mission],ty[mission])<24){vx=vy=0;if(variedMission()&&mission==0&&routeStep==0){routeStep++;routeTarget();say("Entrega 1/2! Proxima");}else{cargo=true;if(training)lesson=4;say("Agora volte a BASE");}music(2);}
 else if(cargo&&dist(x,y,50,150)<24){if(!training){int oldCredits=credits,oldXP=xp;reportSector=sector;int oldAid=sectorAid[sector];reward();sectorAid[sector]=min(12u,sectorAid[sector]+uint32_t(mission==0?2:mission==1?1:3));reportAid=sectorAid[sector]-oldAid;reportOnline=oldAid<12&&sectorAid[sector]>=12;if(campaignFlight){storyChapter++;sector=homeSector;credits+=25;xp+=15;rankLevel=1;while(rankLevel<5&&xp>=rankLevel*60)rankLevel++;}reportCredits=credits-oldCredits;reportXP=xp-oldXP;saveGame();}resetShip();if(training){lesson=5;mode=LESSON;}else{mode=REPORT;say(saveOK?(contractWon?"Contrato especial OK!":"Concluida e salva!"):"Erro ao salvar!");}music(3);}
 else say(cargo?"Siga seta para BASE":"Siga seta para ALVO");}

 if(hull<=0||fuel<=0){mode=LOST;music(4);}dirty=true;}
 if(dirty){draw();radar();dirty=false;}}
}

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik