Questa versione viene buildata, ma ha dei minor issue (inspect-output.txt) notati dal platformio inspector (pio check) che sarebbe meglio sistemare e controllare. Dovrebbe Funzionare ma va testata, nei docs ci sono le info su come funziona ora il codice per fare troubleshooting.

This commit is contained in:
Jasssbo 2025-08-30 15:03:07 +02:00
parent eadf2d7679
commit 194fd2afdc
39 changed files with 4067 additions and 1852 deletions

26
.gitignore vendored
View file

@ -1,21 +1,5 @@
[Ll]ibrary/
[Tt]emp/
[Oo]bj/
[Bb]uild/
# Autogenerated VS/MD solution and project files
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
# Unity3D generated meta files
*.pidb.meta
# Unity3D Generated File On Crash Reports
sysinfo.txt
.pio
.vscode/.browse.c_cpp.db*
.vscode/c_cpp_properties.json
.vscode/launch.json
.vscode/ipch

10
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,10 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"platformio.platformio-ide"
],
"unwantedRecommendations": [
"ms-vscode.cpptools-extension-pack"
]
}

42
Boss.h
View file

@ -1,42 +0,0 @@
#include "Arduino.h"
class Boss
{
public:
void Spawn();
void Hit();
void Kill();
bool Alive();
int _pos;
int _lives;
int _ticks;
private:
bool _alive;
};
void Boss::Spawn(){
_pos = 800;
_lives = 3;
_alive = 1;
}
void Boss::Hit(){
_lives --;
if(_lives == 0) {
Kill();
return;
}
if(_lives == 2){
_pos = 200;
}else if(_lives == 1){
_pos = 600;
}
}
bool Boss::Alive(){
return _alive;
}
void Boss::Kill(){
_alive = 0;
}

View file

@ -1,23 +0,0 @@
#include "Arduino.h"
class Conveyor
{
public:
void Spawn(int startPoint, int endPoint, int dir);
void Kill();
int _startPoint;
int _endPoint;
int _dir;
bool _alive;
};
void Conveyor::Spawn(int startPoint, int endPoint, int dir){
_startPoint = startPoint;
_endPoint = endPoint;
_dir = dir;
_alive = true;
}
void Conveyor::Kill(){
_alive = false;
}

55
Enemy.h
View file

@ -1,55 +0,0 @@
#include "Arduino.h"
class Enemy
{
public:
void Spawn(int pos, int dir, int sp, int wobble);
void Tick();
void Kill();
bool Alive();
int _pos;
int _wobble;
int playerSide;
private:
int _dir;
int _sp;
int _alive;
int _origin;
};
void Enemy::Spawn(int pos, int dir, int sp, int wobble){
_pos = pos;
_dir = dir; // 0 = left, 1 = right
_wobble = wobble; // 0 = no, >0 = yes, value is width of wobble
_origin = pos;
_sp = sp;
_alive = 1;
}
void Enemy::Tick(){
if(_alive){
if(_wobble > 0){
_pos = _origin + (sin((millis()/3000.0)*_sp)*_wobble);
}else{
if(_dir == 0){
_pos -= _sp;
}else{
_pos += _sp;
}
if(_pos > 1000) {
Kill();
}
if(_pos <= 0) {
Kill();
}
}
}
}
bool Enemy::Alive(){
return _alive;
}
void Enemy::Kill(){
_alive = 0;
}

22
LICENSE
View file

@ -1,22 +0,0 @@
The MIT License (MIT)
Copyright (c) 2015 Critters
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

37
Lava.h
View file

@ -1,37 +0,0 @@
#include "Arduino.h"
class Lava
{
public:
void Spawn(int left, int right, int ontime, int offtime, int offset, char* state);
void Kill();
int Alive();
int _left;
int _right;
int _ontime;
int _offtime;
int _offset;
long _lastOn;
char* _state;
private:
int _alive;
};
void Lava::Spawn(int left, int right, int ontime, int offtime, int offset, char* state){
_left = left;
_right = right;
_ontime = ontime;
_offtime = offtime;
_offset = offset;
_alive = 1;
_lastOn = millis()-offset;
_state = state;
}
void Lava::Kill(){
_alive = 0;
}
int Lava::Alive(){
return _alive;
}

View file

@ -1,59 +0,0 @@
#include "Arduino.h"
#define FRICTION 1
class Particle
{
public:
void Spawn(int pos);
void Tick(int USE_GRAVITY);
void Kill();
bool Alive();
int _pos;
int _power;
private:
int _life;
int _alive;
int _sp;
};
void Particle::Spawn(int pos){
_pos = pos;
_sp = random(-200, 200);
_power = 255;
_alive = 1;
_life = 220 - abs(_sp);
}
void Particle::Tick(int USE_GRAVITY){
if(_alive){
_life ++;
if(_sp > 0){
_sp -= _life/10;
}else{
_sp += _life/10;
}
if(USE_GRAVITY && _pos > 500) _sp -= 10;
_power = 100 - _life;
if(_power <= 0){
Kill();
}else{
_pos += _sp/7.0;
if(_pos > 1000){
_pos = 1000;
_sp = 0-(_sp/2);
}
else if(_pos < 0){
_pos = 0;
_sp = 0-(_sp/2);
}
}
}
}
bool Particle::Alive(){
return _alive;
}
void Particle::Kill(){
_alive = 0;
}

144
README.md
View file

@ -1,78 +1,108 @@
# TWANG
A Arduino-based, 1D, LED loving, dungeon crawler. inspired by Line Wobbler by Robin B
README - TWANG ESP32
=====================
## Video playlist
A playlist that shows the development of TWANG and the game in both a desktop and house-sized form can be found here: https://www.youtube.com/watch?v=9yf_VINmbTE&list=PL1_Z89_x_Dff-XhOxlx6sQ38wJqe1X2M0
Scopo
-----
## Required libraries:
* FastLED: https://github.com/FastLED/FastLED/files/4608545/FastLED.zip
* I2Cdev
* MPU6050: https://github.com/jrowberg/i2cdevlib/tree/master/Arduino/MPU6050
* RunningMedian: http://playground.arduino.cc/Main/RunningMedian
Repository per il gioco TWANG su ESP32. Ho migrato il controllo LED da FastLED a NeoPixelBus per evitare problemi specifici su ESP32. Questo README spiega come verificare che la build funzioni e come testare la striscia LED e gli input.
## Hardware used:
* Arduino MEGA/NANO
* 3 LEDs for life indicator
* APA102-C LED light strip. The more the better, maximum of 1000. Tested with 2x 144/meter and 12x 60/meter strips. The FastLED lib works with the less expensive WS2812 LEDs, i've not tried them but should be fine.
* 5v power supply, assume around 40mW per LED to calculate size
* MPU6050 accelerometer
* Spring doorstop, I used these: http://smile.amazon.com/gp/product/B00J4Y5BU2
Checklist rapida (stato)
------------------------
## Enclosure
Files to print your own enclosure can be found here: http://www.thingiverse.com/thing:1116899
- Migrazione FastLED -> NeoPixelBus: Done
- Rimozione build flags FastLED: Done
- `LedController` aggiornato per NeoPixelBus: Done
- `GameEngine` aggiornato per usare `RgbColor`: Done
- Correzione `Level::reset` signature: Done
- Build PlatformIO: PASS (firmware generato)
## Overview
TWANG was developed quickly to make my Halloween lights interactive, the code is fairly well commmented but could be improved. The following is a quick overview of the code to help you understand and tweak the game to your needs.
File modificati / aggiunti
-------------------------
The game is played on a 1000 unit line, the position of enemies, the player, lava etc range from 0 to 1000 and the LEDs that represent them are derived using the getLED() function. You don't need to worry about this but it's good to know for things like the width of the attack and player max move speed. Regardles of the number of LEDs, everything takes place in this 1000 unit wide line.
- `platformio.ini` — dipendenza aggiornata a `makuna/NeoPixelBus` e rimozione flags FastLED
- `src/hardware/LedController.h` — nuova implementazione con NeoPixelBus
- `src/game/GameEngine.cpp` — sostituiti i riferimenti `CRGB` -> `RgbColor`
- `src/game/Level.h``reset(Player &p)` ora chiama `p.reset()` senza argomenti
- `docs/CONTROLS.md` — piccola correzione heading
- `README.md` — questo file
**ATMEGA4809**
The TWANG4809 sketch is intended for use with the Arduino Nano Every and the Uno Wifi REV2 both of which use the Atmega 4809 processor.
At the time of writing, these boards do not have out of the box support for FastLED or ToneAC.
This sketch removes the ToneAC functionality.
For FastLED to function, install [this](https://github.com/FastLED/FastLED/files/4608545/FastLED.zip) library instead of the one listed in Arduino IDE.
Come verificare la build (locale)
--------------------------------
**//LED SETUP** Defines the quantity of LEDs as well as the data and clock pins used. I've tested several APA102-C strips and the color order sometimes changes from BGR to GBR or GRB, if the player is not blue, the exit green and the enemies red, this is the bit you want to change. Brightness should range from 50 to 255, use a lower number if playing at night or wanting to use a smaller power supply. "DIRECTION" can be set to 0 or 1 to flip the game orientation. In setup() there is a "FastLED.addLeds()" line, in there you could change it to another brand of LED strip like the cheaper WS2812.
Apri una PowerShell (pwsh) nella cartella del progetto (root dove c'è `platformio.ini`) e lancia:
The game also has 3 regular LEDs for life indicators (the player gets 3 lives which reset each time they level up). The pins for these LEDs are stored in lifeLEDs[] and are updated in the updateLives() function
```powershell
# compilazione
platformio run
**//JOYSTICK SETUP** All parameters are commented in the code, you can set it to work in both forward/backward as well as side-to-side mode by changing JOYSTICK_ORIENTATION. Adjust the ATTACK_THRESHOLD if the "Twanging" is overly sensitive and the JOYSTICK_DEADZONE if the player slowly drifts when there is no input (because it's hard to get the MPU6050 dead level).
# upload (se la board è collegata e vuoi flashare)
platformio run --target upload
**//WOBBLE ATTACK** Sets the width, duration (ms) of the attack.
# apertura seriale per test realtime (115200)
platformio device monitor --baud 115200
```
**//POOLS** These are the object pools for enemies, particles, lava, conveyors etc. You can modify the quantity of any of them if your levels use more or if you want to save some memory, just remember to update the respective counts to avoid errors.
Cosa controllare dopo la build
-----------------------------
**//USE_GRAVITY** 0/1 to set if particles created by the player getting killed should fall towards the start point, the BEND_POINT variable can be set to mark the point at which the strip of LEDs goes from been horizontal to vertical. The game is 1000 units wide (regardless of number of LED's) so 500 would be the mid point. If this is confusing just set USE_GRAVITY to 0.
1. `platformio run` termina con "SUCCESS" e viene generato `firmware.bin`.
2. Non devono comparire errori di link o simboli non definiti.
3. Se vuoi verificare comportamento LED prima di caricare, puoi comunque aprire il serial monitor e inviare comandi (vedi sotto).
## Modifying / Creating levels
Find the loadLevel() function, in there you can see a switch statment with the 10 levels I created. They all call different functions and variables to setup the level. Each one is described below:
Test runtime (veloce)
---------------------
**playerPosition;** Where the player starts on the 0 to 1000 line. If not set it defaults to 0. I set it to 200 in the first level so the player can see movement even if the first action they take is to push the joystick left
1. Collega l'ESP32 al PC via USB.
2. Apri il monitor seriale:
**spawnEnemy(position, direction, speed, wobble);**
* position: 0 to 1000
* direction: 0/1, initial direction of travel
* speed: >=0, speed of the enemy, remember the game is 1000 wide and runs at 60fps. I recommend between 1 and 4
* wobble: 0=regular moving enemy, 1=sine wave enemy, in this case speed sets the width of the wave
```powershell
platformio device monitor --baud 115200
```
**spawnPool[poolNumber].Spawn(position, rate, speed, direction);**
* A spawn pool is a point which spawns enemies forever
* position: 0 to 1000
* rate: milliseconds between spawns, 1000 = 1 second
* speed: speed of the enemis it spawns
* direction: 0=towards start, 1=away from start
1. Invia il comando `debug` seguito da invio: questo attiva la modalità debug nel gioco e dovrebbe mostrare un pixel bianco che si muove lungo la striscia.
**spawnLava(startPoint, endPoint, ontime, offtime, offset);**
* startPoint: 0 to 1000
* endPoint: 0 to 1000, combined with startPoint this sets the location and size of the lava
* ontime: How lomg (ms) the lava is ON for
* offset: How long (ms) after the level starts before the lava turns on, use this to create patterns with multiple lavas
Comandi seriali disponibili
--------------------------
**spawnConveyor(startPoint, endPoint, direction);**
* startPoint, endPoint: Same as lava
* direction: the direction of travel 0/1
- `debug` — entra in modalità debug (pattern di prova)
- `restart` — riavvia la board
- `a`, `d`, `w`, `s` — movimenti/azioni (utili per test da seriale)
- `status` — stampa stato (placeholder)
**spawnBoss()**
* There are no parramaters for a boss, they always spawn in the same place and have 3 lives. Tweak the values of Boss.h to modify
Controlli fisici
----------------
Feel free to edit, comment on the YouTube video (link at top) if you have any questions.
- `BUTTON_LEFT` -> GPIO 15
- `BUTTON_RIGHT` -> GPIO 2
- `BUTTON_ATTACK`-> GPIO 4
- `BUTTON_START` -> GPIO 5
Nota hardware sui LED
---------------------
- Usa resistenza 330-470 Ω sul line DATA tra ESP32 e prima LED consigliata.
- Usa condensatore 1000 µF vicino alla strip tra +V e GND per stabilizzare alimentazione.
- Assicurati che l'alimentazione della strip supporti il numero di LED (circa 60mA per LED a piena intensità RGB).
Cose da verificare se qualcosa va storto
--------------------------------------
- Errori in compilazione: controlla l'output di `platformio run` e cerca file/righe menzionate.
- Problemi con LED che non accendono: verifica pin `DATA_PIN` in `src/config.h` e il cablaggio fisico.
- Striscia parzialmente corretta o colori sbagliati: prova a cambiare `NeoGrbFeature` in `LedController.h` (alcuni LED usano GRB o RGB) — apertura possibile modifica: `NeoRgbFeature`/`NeoGrbFeature`.
- Problemi di crash o reset: verifica consumo memoria (PlatformIO log mostra RAM/Flash) e alimentazione.
Note tecniche rilevanti
----------------------
- `LedController` usa `new NeoPixelBus<>` dinamico per evitare static init order problems; non ci sono delete espliciti perché la durata è quella della app.
- `fadeAll` è implementato manualmente leggendo `GetPixelColor` e scalando componenti.
- `Player::reset()` non accetta parametri: ogni chiamata nel progetto è stata adattata per usare la versione senza argomenti (correzioni in `Level.h`).
Miglioramenti possibili
----------------------
- Aggiungere un semplice pattern di test automatico all'avvio per diagnosticare la strip senza interazione seriale.
- Implementare un piccolo unit-test harness (non comune per Arduino, ma possibile con framework) per la logica core.
Se vuoi che proceda con l'upload automatico o ad aggiungere il pattern di test all'avvio, dimmelo e lo aggiungo subito.

View file

@ -1,35 +0,0 @@
#include "Arduino.h"
class Spawner
{
public:
void Spawn(int pos, int rate, int sp, int dir, long activate);
void Kill();
int Alive();
int _pos;
int _rate;
int _sp;
int _dir;
long _lastSpawned;
long _activate;
private:
int _alive;
};
void Spawner::Spawn(int pos, int rate, int sp, int dir, long activate){
_pos = pos;
_rate = rate;
_sp = sp;
_dir = dir;
_activate = millis()+activate;
_alive = 1;
}
void Spawner::Kill(){
_alive = 0;
_lastSpawned = 0;
}
int Spawner::Alive(){
return _alive;
}

734
TWANG.ino
View file

@ -1,734 +0,0 @@
// Required libs
#include "FastLED.h"
#include "I2Cdev.h"
#include "MPU6050.h"
#include "Wire.h"
#include "toneAC.h"
#include "iSin.h"
#include "RunningMedian.h"
// Included libs
#include "Enemy.h"
#include "Particle.h"
#include "Spawner.h"
#include "Lava.h"
#include "Boss.h"
#include "Conveyor.h"
// MPU
MPU6050 accelgyro;
int16_t ax, ay, az;
int16_t gx, gy, gz;
// LED setup
#define NUM_LEDS 475
#define DATA_PIN 3
#define CLOCK_PIN 4
#define LED_COLOR_ORDER BGR //if colours aren't working, try GRB or GBR
#define BRIGHTNESS 150 //Use a lower value for lower current power supplies(<2 amps)
#define DIRECTION 1 // 0 = right to left, 1 = left to right
#define MIN_REDRAW_INTERVAL 16 // Min redraw interval (ms) 33 = 30fps / 16 = 63fps
#define USE_GRAVITY 1 // 0/1 use gravity (LED strip going up wall)
#define BEND_POINT 550 // 0/1000 point at which the LED strip goes up the wall
#define LED_TYPE APA102//type of LED strip to use(APA102 - DotStar, WS2811 - NeoPixel) For Neopixels, uncomment line #108 and comment out line #106
// GAME
long previousMillis = 0; // Time of the last redraw
int levelNumber = 0;
long lastInputTime = 0;
#define TIMEOUT 30000
#define LEVEL_COUNT 9
#define MAX_VOLUME 10
iSin isin = iSin();
// JOYSTICK
#define JOYSTICK_ORIENTATION 1 // 0, 1 or 2 to set the angle of the joystick
#define JOYSTICK_DIRECTION 1 // 0/1 to flip joystick direction
#define ATTACK_THRESHOLD 30000 // The threshold that triggers an attack
#define JOYSTICK_DEADZONE 5 // Angle to ignore
int joystickTilt = 0; // Stores the angle of the joystick
int joystickWobble = 0; // Stores the max amount of acceleration (wobble)
// WOBBLE ATTACK
#define ATTACK_WIDTH 70 // Width of the wobble attack, world is 1000 wide
#define ATTACK_DURATION 500 // Duration of a wobble attack (ms)
long attackMillis = 0; // Time the attack started
bool attacking = 0; // Is the attack in progress?
#define BOSS_WIDTH 40
// PLAYER
#define MAX_PLAYER_SPEED 10 // Max move speed of the player
char* stage; // what stage the game is at (PLAY/DEAD/WIN/GAMEOVER)
long stageStartTime; // Stores the time the stage changed for stages that are time based
int playerPosition; // Stores the player position
int playerPositionModifier; // +/- adjustment to player position
bool playerAlive;
long killTime;
int lives = 3;
// POOLS
int lifeLEDs[3] = {52, 50, 40};
Enemy enemyPool[10] = {
Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy()
};
int const enemyCount = 10;
Particle particlePool[40] = {
Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle()
};
int const particleCount = 40;
Spawner spawnPool[2] = {
Spawner(), Spawner()
};
int const spawnCount = 2;
Lava lavaPool[4] = {
Lava(), Lava(), Lava(), Lava()
};
int const lavaCount = 4;
Conveyor conveyorPool[2] = {
Conveyor(), Conveyor()
};
int const conveyorCount = 2;
Boss boss = Boss();
CRGB leds[NUM_LEDS];
RunningMedian MPUAngleSamples = RunningMedian(5);
RunningMedian MPUWobbleSamples = RunningMedian(5);
void setup() {
Serial.begin(9600);
while (!Serial);
// MPU
Wire.begin();
accelgyro.initialize();
// Fast LED
FastLED.addLeds<LED_TYPE, DATA_PIN, CLOCK_PIN, LED_COLOR_ORDER>(leds, NUM_LEDS);
//If using Neopixels, use
//FastLED.addLeds<LED_TYPE, DATA_PIN, LED_COLOR_ORDER>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
FastLED.setDither(1);
// Life LEDs
for(int i = 0; i<3; i++){
pinMode(lifeLEDs[i], OUTPUT);
digitalWrite(lifeLEDs[i], HIGH);
}
loadLevel();
}
void loop() {
long mm = millis();
int brightness = 0;
if(stage == "PLAY"){
if(attacking){
SFXattacking();
}else{
SFXtilt(joystickTilt);
}
}else if(stage == "DEAD"){
SFXdead();
}
if (mm - previousMillis >= MIN_REDRAW_INTERVAL) {
getInput();
long frameTimer = mm;
previousMillis = mm;
if(abs(joystickTilt) > JOYSTICK_DEADZONE){
lastInputTime = mm;
if(stage == "SCREENSAVER"){
levelNumber = -1;
stageStartTime = mm;
stage = "WIN";
}
}else{
if(lastInputTime+TIMEOUT < mm){
stage = "SCREENSAVER";
}
}
if(stage == "SCREENSAVER"){
screenSaverTick();
}else if(stage == "PLAY"){
// PLAYING
if(attacking && attackMillis+ATTACK_DURATION < mm) attacking = 0;
// If not attacking, check if they should be
if(!attacking && joystickWobble > ATTACK_THRESHOLD){
attackMillis = mm;
attacking = 1;
}
// If still not attacking, move!
playerPosition += playerPositionModifier;
if(!attacking){
int moveAmount = (joystickTilt/6.0);
if(DIRECTION) moveAmount = -moveAmount;
moveAmount = constrain(moveAmount, -MAX_PLAYER_SPEED, MAX_PLAYER_SPEED);
playerPosition -= moveAmount;
if(playerPosition < 0) playerPosition = 0;
if(playerPosition >= 1000 && !boss.Alive()) {
// Reached exit!
levelComplete();
return;
}
}
if(inLava(playerPosition)){
die();
}
// Ticks and draw calls
FastLED.clear();
tickConveyors();
tickSpawners();
tickBoss();
tickLava();
tickEnemies();
drawPlayer();
drawAttack();
drawExit();
}else if(stage == "DEAD"){
// DEAD
FastLED.clear();
if(!tickParticles()){
loadLevel();
}
}else if(stage == "WIN"){
// LEVEL COMPLETE
FastLED.clear();
if(stageStartTime+500 > mm){
int n = max(map(((mm-stageStartTime)), 0, 500, NUM_LEDS, 0), 0);
for(int i = NUM_LEDS; i>= n; i--){
brightness = 255;
leds[i] = CRGB(0, brightness, 0);
}
SFXwin();
}else if(stageStartTime+1000 > mm){
int n = max(map(((mm-stageStartTime)), 500, 1000, NUM_LEDS, 0), 0);
for(int i = 0; i< n; i++){
brightness = 255;
leds[i] = CRGB(0, brightness, 0);
}
SFXwin();
}else if(stageStartTime+1200 > mm){
leds[0] = CRGB(0, 255, 0);
}else{
nextLevel();
}
}else if(stage == "COMPLETE"){
FastLED.clear();
SFXcomplete();
if(stageStartTime+500 > mm){
int n = max(map(((mm-stageStartTime)), 0, 500, NUM_LEDS, 0), 0);
for(int i = NUM_LEDS; i>= n; i--){
brightness = (sin(((i*10)+mm)/500.0)+1)*255;
leds[i].setHSV(brightness, 255, 50);
}
}else if(stageStartTime+5000 > mm){
for(int i = NUM_LEDS; i>= 0; i--){
brightness = (sin(((i*10)+mm)/500.0)+1)*255;
leds[i].setHSV(brightness, 255, 50);
}
}else if(stageStartTime+5500 > mm){
int n = max(map(((mm-stageStartTime)), 5000, 5500, NUM_LEDS, 0), 0);
for(int i = 0; i< n; i++){
brightness = (sin(((i*10)+mm)/500.0)+1)*255;
leds[i].setHSV(brightness, 255, 50);
}
}else{
nextLevel();
}
}else if(stage == "GAMEOVER"){
// GAME OVER!
FastLED.clear();
stageStartTime = 0;
}
Serial.print(millis()-mm);
Serial.print(" - ");
FastLED.show();
Serial.println(millis()-mm);
}
}
// ---------------------------------
// ------------ LEVELS -------------
// ---------------------------------
void loadLevel(){
updateLives();
cleanupLevel();
playerPosition = 0;
playerAlive = 1;
switch(levelNumber){
case 0:
// Left or right?
playerPosition = 200;
spawnEnemy(1, 0, 0, 0);
break;
case 1:
// Slow moving enemy
spawnEnemy(900, 0, 1, 0);
break;
case 2:
// Spawning enemies at exit every 2 seconds
spawnPool[0].Spawn(1000, 3000, 2, 0, 0);
break;
case 3:
// Lava intro
spawnLava(400, 490, 2000, 2000, 0, "OFF");
spawnPool[0].Spawn(1000, 5500, 3, 0, 0);
break;
case 4:
// Sin enemy
spawnEnemy(700, 1, 7, 275);
spawnEnemy(500, 1, 5, 250);
break;
case 5:
// Conveyor
spawnConveyor(100, 600, -1);
spawnEnemy(800, 0, 0, 0);
break;
case 6:
// Conveyor of enemies
spawnConveyor(50, 1000, 1);
spawnEnemy(300, 0, 0, 0);
spawnEnemy(400, 0, 0, 0);
spawnEnemy(500, 0, 0, 0);
spawnEnemy(600, 0, 0, 0);
spawnEnemy(700, 0, 0, 0);
spawnEnemy(800, 0, 0, 0);
spawnEnemy(900, 0, 0, 0);
break;
case 7:
// Lava run
spawnLava(195, 300, 2000, 2000, 0, "OFF");
spawnLava(350, 455, 2000, 2000, 0, "OFF");
spawnLava(510, 610, 2000, 2000, 0, "OFF");
spawnLava(660, 760, 2000, 2000, 0, "OFF");
spawnPool[0].Spawn(1000, 3800, 4, 0, 0);
break;
case 8:
// Sin enemy #2
spawnEnemy(700, 1, 7, 275);
spawnEnemy(500, 1, 5, 250);
spawnPool[0].Spawn(1000, 5500, 4, 0, 3000);
spawnPool[1].Spawn(0, 5500, 5, 1, 10000);
spawnConveyor(100, 900, -1);
break;
case 9:
// Boss
spawnBoss();
break;
}
stageStartTime = millis();
stage = "PLAY";
}
void spawnBoss(){
boss.Spawn();
moveBoss();
}
void moveBoss(){
int spawnSpeed = 2500;
if(boss._lives == 2) spawnSpeed = 2000;
if(boss._lives == 1) spawnSpeed = 1500;
spawnPool[0].Spawn(boss._pos, spawnSpeed, 3, 0, 0);
spawnPool[1].Spawn(boss._pos, spawnSpeed, 3, 1, 0);
}
void spawnEnemy(int pos, int dir, int sp, int wobble){
for(int e = 0; e<enemyCount; e++){
if(!enemyPool[e].Alive()){
enemyPool[e].Spawn(pos, dir, sp, wobble);
enemyPool[e].playerSide = pos > playerPosition?1:-1;
return;
}
}
}
void spawnLava(int left, int right, int ontime, int offtime, int offset, char* state){
for(int i = 0; i<lavaCount; i++){
if(!lavaPool[i].Alive()){
lavaPool[i].Spawn(left, right, ontime, offtime, offset, state);
return;
}
}
}
void spawnConveyor(int startPoint, int endPoint, int dir){
for(int i = 0; i<conveyorCount; i++){
if(!conveyorPool[i]._alive){
conveyorPool[i].Spawn(startPoint, endPoint, dir);
return;
}
}
}
void cleanupLevel(){
for(int i = 0; i<enemyCount; i++){
enemyPool[i].Kill();
}
for(int i = 0; i<particleCount; i++){
particlePool[i].Kill();
}
for(int i = 0; i<spawnCount; i++){
spawnPool[i].Kill();
}
for(int i = 0; i<lavaCount; i++){
lavaPool[i].Kill();
}
for(int i = 0; i<conveyorCount; i++){
conveyorPool[i].Kill();
}
boss.Kill();
}
void levelComplete(){
stageStartTime = millis();
stage = "WIN";
if(levelNumber == LEVEL_COUNT) stage = "COMPLETE";
lives = 3;
updateLives();
}
void nextLevel(){
levelNumber ++;
if(levelNumber > LEVEL_COUNT) levelNumber = 0;
loadLevel();
}
void gameOver(){
levelNumber = 0;
loadLevel();
}
void die(){
playerAlive = 0;
if(levelNumber > 0) lives --;
updateLives();
if(lives == 0){
levelNumber = 0;
lives = 3;
}
for(int p = 0; p < particleCount; p++){
particlePool[p].Spawn(playerPosition);
}
stageStartTime = millis();
stage = "DEAD";
killTime = millis();
}
// ----------------------------------
// -------- TICKS & RENDERS ---------
// ----------------------------------
void tickEnemies(){
for(int i = 0; i<enemyCount; i++){
if(enemyPool[i].Alive()){
enemyPool[i].Tick();
// Hit attack?
if(attacking){
if(enemyPool[i]._pos > playerPosition-(ATTACK_WIDTH/2) && enemyPool[i]._pos < playerPosition+(ATTACK_WIDTH/2)){
enemyPool[i].Kill();
SFXkill();
}
}
if(inLava(enemyPool[i]._pos)){
enemyPool[i].Kill();
SFXkill();
}
// Draw (if still alive)
if(enemyPool[i].Alive()) {
leds[getLED(enemyPool[i]._pos)] = CRGB(255, 0, 0);
}
// Hit player?
if(
(enemyPool[i].playerSide == 1 && enemyPool[i]._pos <= playerPosition) ||
(enemyPool[i].playerSide == -1 && enemyPool[i]._pos >= playerPosition)
){
die();
return;
}
}
}
}
void tickBoss(){
// DRAW
if(boss.Alive()){
boss._ticks ++;
for(int i = getLED(boss._pos-BOSS_WIDTH/2); i<=getLED(boss._pos+BOSS_WIDTH/2); i++){
leds[i] = CRGB::DarkRed;
leds[i] %= 100;
}
// CHECK COLLISION
if(getLED(playerPosition) > getLED(boss._pos - BOSS_WIDTH/2) && getLED(playerPosition) < getLED(boss._pos + BOSS_WIDTH)){
die();
return;
}
// CHECK FOR ATTACK
if(attacking){
if(
(getLED(playerPosition+(ATTACK_WIDTH/2)) >= getLED(boss._pos - BOSS_WIDTH/2) && getLED(playerPosition+(ATTACK_WIDTH/2)) <= getLED(boss._pos + BOSS_WIDTH/2)) ||
(getLED(playerPosition-(ATTACK_WIDTH/2)) <= getLED(boss._pos + BOSS_WIDTH/2) && getLED(playerPosition-(ATTACK_WIDTH/2)) >= getLED(boss._pos - BOSS_WIDTH/2))
){
boss.Hit();
if(boss.Alive()){
moveBoss();
}else{
spawnPool[0].Kill();
spawnPool[1].Kill();
}
}
}
}
}
void drawPlayer(){
leds[getLED(playerPosition)] = CRGB(0, 255, 0);
}
void drawExit(){
if(!boss.Alive()){
leds[NUM_LEDS-1] = CRGB(0, 0, 255);
}
}
void tickSpawners(){
long mm = millis();
for(int s = 0; s<spawnCount; s++){
if(spawnPool[s].Alive() && spawnPool[s]._activate < mm){
if(spawnPool[s]._lastSpawned + spawnPool[s]._rate < mm || spawnPool[s]._lastSpawned == 0){
spawnEnemy(spawnPool[s]._pos, spawnPool[s]._dir, spawnPool[s]._sp, 0);
spawnPool[s]._lastSpawned = mm;
}
}
}
}
void tickLava(){
int A, B, p, i, brightness, flicker;
long mm = millis();
Lava LP;
for(i = 0; i<lavaCount; i++){
flicker = random8(5);
LP = lavaPool[i];
if(LP.Alive()){
A = getLED(LP._left);
B = getLED(LP._right);
if(LP._state == "OFF"){
if(LP._lastOn + LP._offtime < mm){
LP._state = "ON";
LP._lastOn = mm;
}
for(p = A; p<= B; p++){
leds[p] = CRGB(3+flicker, (3+flicker)/1.5, 0);
}
}else if(LP._state == "ON"){
if(LP._lastOn + LP._ontime < mm){
LP._state = "OFF";
LP._lastOn = mm;
}
for(p = A; p<= B; p++){
leds[p] = CRGB(150+flicker, 100+flicker, 0);
}
}
}
lavaPool[i] = LP;
}
}
bool tickParticles(){
bool stillActive = false;
for(int p = 0; p < particleCount; p++){
if(particlePool[p].Alive()){
particlePool[p].Tick(USE_GRAVITY);
leds[getLED(particlePool[p]._pos)] += CRGB(particlePool[p]._power, 0, 0);
stillActive = true;
}
}
return stillActive;
}
void tickConveyors(){
int b, dir, n, i, ss, ee, led;
long m = 10000+millis();
playerPositionModifier = 0;
for(i = 0; i<conveyorCount; i++){
if(conveyorPool[i]._alive){
dir = conveyorPool[i]._dir;
ss = getLED(conveyorPool[i]._startPoint);
ee = getLED(conveyorPool[i]._endPoint);
for(led = ss; led<ee; led++){
b = 5;
n = (-led + (m/100)) % 5;
if(dir == -1) n = (led + (m/100)) % 5;
b = (5-n)/2.0;
if(b > 0) leds[led] = CRGB(0, 0, b);
}
if(playerPosition > conveyorPool[i]._startPoint && playerPosition < conveyorPool[i]._endPoint){
if(dir == -1){
playerPositionModifier = -(MAX_PLAYER_SPEED-4);
}else{
playerPositionModifier = (MAX_PLAYER_SPEED-4);
}
}
}
}
}
void drawAttack(){
if(!attacking) return;
int n = map(millis() - attackMillis, 0, ATTACK_DURATION, 100, 5);
for(int i = getLED(playerPosition-(ATTACK_WIDTH/2))+1; i<=getLED(playerPosition+(ATTACK_WIDTH/2))-1; i++){
leds[i] = CRGB(0, 0, n);
}
if(n > 90) {
n = 255;
leds[getLED(playerPosition)] = CRGB(255, 255, 255);
}else{
n = 0;
leds[getLED(playerPosition)] = CRGB(0, 255, 0);
}
leds[getLED(playerPosition-(ATTACK_WIDTH/2))] = CRGB(n, n, 255);
leds[getLED(playerPosition+(ATTACK_WIDTH/2))] = CRGB(n, n, 255);
}
int getLED(int pos){
// The world is 1000 pixels wide, this converts world units into an LED number
return constrain((int)map(pos, 0, 1000, 0, NUM_LEDS-1), 0, NUM_LEDS-1);
}
bool inLava(int pos){
// Returns if the player is in active lava
int i;
Lava LP;
for(i = 0; i<lavaCount; i++){
LP = lavaPool[i];
if(LP.Alive() && LP._state == "ON"){
if(LP._left < pos && LP._right > pos) return true;
}
}
return false;
}
void updateLives(){
// Updates the life LEDs to show how many lives the player has left
for(int i = 0; i<3; i++){
digitalWrite(lifeLEDs[i], lives>i?HIGH:LOW);
}
}
// ---------------------------------
// --------- SCREENSAVER -----------
// ---------------------------------
void screenSaverTick(){
int n, b, c, i;
long mm = millis();
int mode = (mm/20000)%2;
for(i = 0; i<NUM_LEDS; i++){
leds[i].nscale8(250);
}
if(mode == 0){
// Marching green <> orange
n = (mm/250)%10;
b = 10+((sin(mm/500.00)+1)*20.00);
c = 20+((sin(mm/5000.00)+1)*33);
for(i = 0; i<NUM_LEDS; i++){
if(i%10 == n){
leds[i] = CHSV( c, 255, 150);
}
}
}else if(mode == 1){
// Random flashes
randomSeed(mm);
for(i = 0; i<NUM_LEDS; i++){
if(random8(200) == 0){
leds[i] = CHSV( 25, 255, 100);
}
}
}
}
// ---------------------------------
// ----------- JOYSTICK ------------
// ---------------------------------
void getInput(){
// This is responsible for the player movement speed and attacking.
// You can replace it with anything you want that passes a -90>+90 value to joystickTilt
// and any value to joystickWobble that is greater than ATTACK_THRESHOLD (defined at start)
// For example you could use 3 momentery buttons:
// if(digitalRead(leftButtonPinNumber) == HIGH) joystickTilt = -90;
// if(digitalRead(rightButtonPinNumber) == HIGH) joystickTilt = 90;
// if(digitalRead(attackButtonPinNumber) == HIGH) joystickWobble = ATTACK_THRESHOLD;
accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
int a = (JOYSTICK_ORIENTATION == 0?ax:(JOYSTICK_ORIENTATION == 1?ay:az))/166;
int g = (JOYSTICK_ORIENTATION == 0?gx:(JOYSTICK_ORIENTATION == 1?gy:gz));
if(abs(a) < JOYSTICK_DEADZONE) a = 0;
if(a > 0) a -= JOYSTICK_DEADZONE;
if(a < 0) a += JOYSTICK_DEADZONE;
MPUAngleSamples.add(a);
MPUWobbleSamples.add(g);
joystickTilt = MPUAngleSamples.getMedian();
if(JOYSTICK_DIRECTION == 1) {
joystickTilt = 0-joystickTilt;
}
joystickWobble = abs(MPUWobbleSamples.getHighest());
}
// ---------------------------------
// -------------- SFX --------------
// ---------------------------------
void SFXtilt(int amount){
int f = map(abs(amount), 0, 90, 80, 900)+random8(100);
if(playerPositionModifier < 0) f -= 500;
if(playerPositionModifier > 0) f += 200;
toneAC(f, min(min(abs(amount)/9, 5), MAX_VOLUME));
}
void SFXattacking(){
int freq = map(sin(millis()/2.0)*1000.0, -1000, 1000, 500, 600);
if(random8(5)== 0){
freq *= 3;
}
toneAC(freq, MAX_VOLUME);
}
void SFXdead(){
int freq = max(1000 - (millis()-killTime), 10);
freq += random8(200);
int vol = max(10 - (millis()-killTime)/200, 0);
toneAC(freq, MAX_VOLUME);
}
void SFXkill(){
toneAC(2000, MAX_VOLUME, 1000, true);
}
void SFXwin(){
int freq = (millis()-stageStartTime)/3.0;
freq += map(sin(millis()/20.0)*1000.0, -1000, 1000, 0, 20);
int vol = 10;//max(10 - (millis()-stageStartTime)/200, 0);
toneAC(freq, MAX_VOLUME);
}
void SFXcomplete(){
noToneAC();
}

View file

@ -1,729 +0,0 @@
// Required libs
#include "FastLED.h"
#include "I2Cdev.h"
#include "MPU6050.h"
#include "Wire.h"
//#include "toneAC.h"
#include "iSin.h"
#include "RunningMedian.h"
// Included libs
#include "Enemy.h"
#include "Particle.h"
#include "Spawner.h"
#include "Lava.h"
#include "Boss.h"
#include "Conveyor.h"
// MPU
MPU6050 accelgyro;
int16_t ax, ay, az;
int16_t gx, gy, gz;
// LED setup
#define NUM_LEDS 475
#define DATA_PIN 3
#define CLOCK_PIN 4
#define LED_COLOR_ORDER GRB//Try BGR or GBR
#define BRIGHTNESS 150
#define DIRECTION 1 // 0 = right to left, 1 = left to right
#define MIN_REDRAW_INTERVAL 16 // Min redraw interval (ms) 33 = 30fps / 16 = 63fps
#define USE_GRAVITY 1 // 0/1 use gravity (LED strip going up wall)
#define BEND_POINT 0 // 0/1000 point at which the LED strip goes up the wall
#define LED_TYPE APA102//type of LED strip to use(APA102 - DotStar, WS2811 - NeoPixel)
// GAME
long previousMillis = 0; // Time of the last redraw
int levelNumber = 0;
long lastInputTime = 0;
#define TIMEOUT 30000
#define LEVEL_COUNT 9
#define MAX_VOLUME 10
iSin isin = iSin();
// JOYSTICK
#define JOYSTICK_ORIENTATION 1 // 0, 1 or 2 to set the angle of the joystick
#define JOYSTICK_DIRECTION 1 // 0/1 to flip joystick direction
#define ATTACK_THRESHOLD 30000 // The threshold that triggers an attack
#define JOYSTICK_DEADZONE 5 // Angle to ignore
int joystickTilt = 0; // Stores the angle of the joystick
int joystickWobble = 0; // Stores the max amount of acceleration (wobble)
// WOBBLE ATTACK
#define ATTACK_WIDTH 70 // Width of the wobble attack, world is 1000 wide
#define ATTACK_DURATION 500 // Duration of a wobble attack (ms)
long attackMillis = 0; // Time the attack started
bool attacking = 0; // Is the attack in progress?
#define BOSS_WIDTH 40
// PLAYER
#define MAX_PLAYER_SPEED 10 // Max move speed of the player
char* stage; // what stage the game is at (PLAY/DEAD/WIN/GAMEOVER)
long stageStartTime; // Stores the time the stage changed for stages that are time based
int playerPosition; // Stores the player position
int playerPositionModifier; // +/- adjustment to player position
bool playerAlive;
long killTime;
int lives = 3;
// POOLS
int lifeLEDs[3] = {52, 50, 40};
Enemy enemyPool[10] = {
Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy(), Enemy()
};
int const enemyCount = 10;
Particle particlePool[40] = {
Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle(), Particle()
};
int const particleCount = 40;
Spawner spawnPool[2] = {
Spawner(), Spawner()
};
int const spawnCount = 2;
Lava lavaPool[4] = {
Lava(), Lava(), Lava(), Lava()
};
int const lavaCount = 4;
Conveyor conveyorPool[2] = {
Conveyor(), Conveyor()
};
int const conveyorCount = 2;
Boss boss = Boss();
CRGB leds[NUM_LEDS];
RunningMedian MPUAngleSamples = RunningMedian(5);
RunningMedian MPUWobbleSamples = RunningMedian(5);
void setup() {
Serial.begin(9600);
while (!Serial);
//Buttons
//pinMode(LEFT_PIN, INPUT);
//pinMode(RIGHT_PIN, INPUT);
//pinMode(ATTACK_PIN, INPUT);
// MPU
Wire.begin();
accelgyro.initialize();
// Fast LED
FastLED.addLeds<LED_TYPE, DATA_PIN, CLOCK_PIN, LED_COLOR_ORDER>(leds, NUM_LEDS);
FastLED.setBrightness(BRIGHTNESS);
FastLED.setDither(1);
// Life LEDs
for(int i = 0; i<3; i++){
pinMode(lifeLEDs[i], OUTPUT);
digitalWrite(lifeLEDs[i], HIGH);
}
loadLevel();
}
void loop() {
long mm = millis();
int brightness = 0;
/*
if(stage == "PLAY"){
if(attacking){
SFXattacking();
}else{
SFXtilt(joystickTilt);
}
}else if(stage == "DEAD"){
SFXdead();
}
*/
if (mm - previousMillis >= MIN_REDRAW_INTERVAL) {
getInput();
long frameTimer = mm;
previousMillis = mm;
if(abs(joystickTilt) > JOYSTICK_DEADZONE){
lastInputTime = mm;
if(stage == "SCREENSAVER"){
levelNumber = -1;
stageStartTime = mm;
stage = "WIN";
}
}else{
if(lastInputTime+TIMEOUT < mm){
stage = "SCREENSAVER";
}
}
if(stage == "SCREENSAVER"){
screenSaverTick();
}else if(stage == "PLAY"){
// PLAYING
if(attacking && attackMillis+ATTACK_DURATION < mm) attacking = 0;
// If not attacking, check if they should be
if(!attacking && joystickWobble > ATTACK_THRESHOLD){
attackMillis = mm;
attacking = 1;
}
// If still not attacking, move!
playerPosition += playerPositionModifier;
if(!attacking){
int moveAmount = (joystickTilt/6.0);
if(DIRECTION) moveAmount = -moveAmount;
moveAmount = constrain(moveAmount, -MAX_PLAYER_SPEED, MAX_PLAYER_SPEED);
playerPosition -= moveAmount;
if(playerPosition < 0) playerPosition = 0;
if(playerPosition >= 1000 && !boss.Alive()) {
// Reached exit!
levelComplete();
return;
}
}
if(inLava(playerPosition)){
die();
}
// Ticks and draw calls
FastLED.clear();
tickConveyors();
tickSpawners();
tickBoss();
tickLava();
tickEnemies();
drawPlayer();
drawAttack();
drawExit();
}else if(stage == "DEAD"){
// DEAD
FastLED.clear();
if(!tickParticles()){
loadLevel();
}
}else if(stage == "WIN"){
// LEVEL COMPLETE
FastLED.clear();
if(stageStartTime+500 > mm){
int n = max(map(((mm-stageStartTime)), 0, 500, NUM_LEDS, 0), 0);
for(int i = NUM_LEDS; i>= n; i--){
brightness = 255;
leds[i] = CRGB(0, brightness, 0);
}
//SFXwin();
}else if(stageStartTime+1000 > mm){
int n = max(map(((mm-stageStartTime)), 500, 1000, NUM_LEDS, 0), 0);
for(int i = 0; i< n; i++){
brightness = 255;
leds[i] = CRGB(0, brightness, 0);
}
//SFXwin();
}else if(stageStartTime+1200 > mm){
leds[0] = CRGB(0, 255, 0);
}else{
nextLevel();
}
}else if(stage == "COMPLETE"){
FastLED.clear();
//SFXcomplete();
if(stageStartTime+500 > mm){
int n = max(map(((mm-stageStartTime)), 0, 500, NUM_LEDS, 0), 0);
for(int i = NUM_LEDS; i>= n; i--){
brightness = (sin(((i*10)+mm)/500.0)+1)*255;
leds[i].setHSV(brightness, 255, 50);
}
}else if(stageStartTime+5000 > mm){
for(int i = NUM_LEDS; i>= 0; i--){
brightness = (sin(((i*10)+mm)/500.0)+1)*255;
leds[i].setHSV(brightness, 255, 50);
}
}else if(stageStartTime+5500 > mm){
int n = max(map(((mm-stageStartTime)), 5000, 5500, NUM_LEDS, 0), 0);
for(int i = 0; i< n; i++){
brightness = (sin(((i*10)+mm)/500.0)+1)*255;
leds[i].setHSV(brightness, 255, 50);
}
}else{
nextLevel();
}
}else if(stage == "GAMEOVER"){
// GAME OVER!
FastLED.clear();
stageStartTime = 0;
}
Serial.print(millis()-mm);
Serial.print(" - ");
FastLED.show();
Serial.println(millis()-mm);
}
}
// ---------------------------------
// ------------ LEVELS -------------
// ---------------------------------
void loadLevel(){
updateLives();
cleanupLevel();
playerPosition = 0;
playerAlive = 1;
switch(levelNumber){
case 0:
// Left or right?
playerPosition = 200;
spawnEnemy(1, 0, 0, 0);
break;
case 1:
// Slow moving enemy
spawnEnemy(900, 0, 1, 0);
break;
case 2:
// Spawning enemies at exit every 2 seconds
spawnPool[0].Spawn(1000, 3000, 2, 0, 0);
break;
case 3:
// Lava intro
spawnLava(400, 490, 2000, 2000, 0, "OFF");
spawnPool[0].Spawn(1000, 5500, 3, 0, 0);
break;
case 4:
// Sin enemy
spawnEnemy(700, 1, 7, 275);
spawnEnemy(500, 1, 5, 250);
break;
case 5:
// Conveyor
spawnConveyor(100, 600, -1);
spawnEnemy(800, 0, 0, 0);
break;
case 6:
// Conveyor of enemies
spawnConveyor(50, 1000, 1);
spawnEnemy(300, 0, 0, 0);
spawnEnemy(400, 0, 0, 0);
spawnEnemy(500, 0, 0, 0);
spawnEnemy(600, 0, 0, 0);
spawnEnemy(700, 0, 0, 0);
spawnEnemy(800, 0, 0, 0);
spawnEnemy(900, 0, 0, 0);
break;
case 7:
// Lava run
spawnLava(195, 300, 2000, 2000, 0, "OFF");
spawnLava(350, 455, 2000, 2000, 0, "OFF");
spawnLava(510, 610, 2000, 2000, 0, "OFF");
spawnLava(660, 760, 2000, 2000, 0, "OFF");
spawnPool[0].Spawn(1000, 3800, 4, 0, 0);
break;
case 8:
// Sin enemy #2
spawnEnemy(700, 1, 7, 275);
spawnEnemy(500, 1, 5, 250);
spawnPool[0].Spawn(1000, 5500, 4, 0, 3000);
spawnPool[1].Spawn(0, 5500, 5, 1, 10000);
spawnConveyor(100, 900, -1);
break;
case 9:
// Boss
spawnBoss();
break;
}
stageStartTime = millis();
stage = "PLAY";
}
void spawnBoss(){
boss.Spawn();
moveBoss();
}
void moveBoss(){
int spawnSpeed = 2500;
if(boss._lives == 2) spawnSpeed = 2000;
if(boss._lives == 1) spawnSpeed = 1500;
spawnPool[0].Spawn(boss._pos, spawnSpeed, 3, 0, 0);
spawnPool[1].Spawn(boss._pos, spawnSpeed, 3, 1, 0);
}
void spawnEnemy(int pos, int dir, int sp, int wobble){
for(int e = 0; e<enemyCount; e++){
if(!enemyPool[e].Alive()){
enemyPool[e].Spawn(pos, dir, sp, wobble);
enemyPool[e].playerSide = pos > playerPosition?1:-1;
return;
}
}
}
void spawnLava(int left, int right, int ontime, int offtime, int offset, char* state){
for(int i = 0; i<lavaCount; i++){
if(!lavaPool[i].Alive()){
lavaPool[i].Spawn(left, right, ontime, offtime, offset, state);
return;
}
}
}
void spawnConveyor(int startPoint, int endPoint, int dir){
for(int i = 0; i<conveyorCount; i++){
if(!conveyorPool[i]._alive){
conveyorPool[i].Spawn(startPoint, endPoint, dir);
return;
}
}
}
void cleanupLevel(){
for(int i = 0; i<enemyCount; i++){
enemyPool[i].Kill();
}
for(int i = 0; i<particleCount; i++){
particlePool[i].Kill();
}
for(int i = 0; i<spawnCount; i++){
spawnPool[i].Kill();
}
for(int i = 0; i<lavaCount; i++){
lavaPool[i].Kill();
}
for(int i = 0; i<conveyorCount; i++){
conveyorPool[i].Kill();
}
boss.Kill();
}
void levelComplete(){
stageStartTime = millis();
stage = "WIN";
if(levelNumber == LEVEL_COUNT) stage = "COMPLETE";
lives = 3;
updateLives();
}
void nextLevel(){
levelNumber ++;
if(levelNumber > LEVEL_COUNT) levelNumber = 0;
loadLevel();
}
void gameOver(){
levelNumber = 0;
loadLevel();
}
void die(){
playerAlive = 0;
if(levelNumber > 0) lives --;
updateLives();
if(lives == 0){
levelNumber = 0;
lives = 3;
}
for(int p = 0; p < particleCount; p++){
particlePool[p].Spawn(playerPosition);
}
stageStartTime = millis();
stage = "DEAD";
killTime = millis();
}
// ----------------------------------
// -------- TICKS & RENDERS ---------
// ----------------------------------
void tickEnemies(){
for(int i = 0; i<enemyCount; i++){
if(enemyPool[i].Alive()){
enemyPool[i].Tick();
// Hit attack?
if(attacking){
if(enemyPool[i]._pos > playerPosition-(ATTACK_WIDTH/2) && enemyPool[i]._pos < playerPosition+(ATTACK_WIDTH/2)){
enemyPool[i].Kill();
//SFXkill();
}
}
if(inLava(enemyPool[i]._pos)){
enemyPool[i].Kill();
//SFXkill();
}
// Draw (if still alive)
if(enemyPool[i].Alive()) {
leds[getLED(enemyPool[i]._pos)] = CRGB(255, 0, 0);
}
// Hit player?
if(
(enemyPool[i].playerSide == 1 && enemyPool[i]._pos <= playerPosition) ||
(enemyPool[i].playerSide == -1 && enemyPool[i]._pos >= playerPosition)
){
die();
return;
}
}
}
}
void tickBoss(){
// DRAW
if(boss.Alive()){
boss._ticks ++;
for(int i = getLED(boss._pos-BOSS_WIDTH/2); i<=getLED(boss._pos+BOSS_WIDTH/2); i++){
leds[i] = CRGB::DarkRed;
leds[i] %= 100;
}
// CHECK COLLISION
if(getLED(playerPosition) > getLED(boss._pos - BOSS_WIDTH/2) && getLED(playerPosition) < getLED(boss._pos + BOSS_WIDTH)){
die();
return;
}
// CHECK FOR ATTACK
if(attacking){
if(
(getLED(playerPosition+(ATTACK_WIDTH/2)) >= getLED(boss._pos - BOSS_WIDTH/2) && getLED(playerPosition+(ATTACK_WIDTH/2)) <= getLED(boss._pos + BOSS_WIDTH/2)) ||
(getLED(playerPosition-(ATTACK_WIDTH/2)) <= getLED(boss._pos + BOSS_WIDTH/2) && getLED(playerPosition-(ATTACK_WIDTH/2)) >= getLED(boss._pos - BOSS_WIDTH/2))
){
boss.Hit();
if(boss.Alive()){
moveBoss();
}else{
spawnPool[0].Kill();
spawnPool[1].Kill();
}
}
}
}
}
void drawPlayer(){
leds[getLED(playerPosition)] = CRGB(0, 255, 0);
}
void drawExit(){
if(!boss.Alive()){
leds[NUM_LEDS-1] = CRGB(0, 0, 255);
}
}
void tickSpawners(){
long mm = millis();
for(int s = 0; s<spawnCount; s++){
if(spawnPool[s].Alive() && spawnPool[s]._activate < mm){
if(spawnPool[s]._lastSpawned + spawnPool[s]._rate < mm || spawnPool[s]._lastSpawned == 0){
spawnEnemy(spawnPool[s]._pos, spawnPool[s]._dir, spawnPool[s]._sp, 0);
spawnPool[s]._lastSpawned = mm;
}
}
}
}
void tickLava(){
int A, B, p, i, brightness, flicker;
long mm = millis();
Lava LP;
for(i = 0; i<lavaCount; i++){
flicker = random8(5);
LP = lavaPool[i];
if(LP.Alive()){
A = getLED(LP._left);
B = getLED(LP._right);
if(LP._state == "OFF"){
if(LP._lastOn + LP._offtime < mm){
LP._state = "ON";
LP._lastOn = mm;
}
for(p = A; p<= B; p++){
leds[p] = CRGB(3+flicker, (3+flicker)/1.5, 0);
}
}else if(LP._state == "ON"){
if(LP._lastOn + LP._ontime < mm){
LP._state = "OFF";
LP._lastOn = mm;
}
for(p = A; p<= B; p++){
leds[p] = CRGB(150+flicker, 100+flicker, 0);
}
}
}
lavaPool[i] = LP;
}
}
bool tickParticles(){
bool stillActive = false;
for(int p = 0; p < particleCount; p++){
if(particlePool[p].Alive()){
particlePool[p].Tick(USE_GRAVITY);
leds[getLED(particlePool[p]._pos)] += CRGB(particlePool[p]._power, 0, 0);
stillActive = true;
}
}
return stillActive;
}
void tickConveyors(){
int b, dir, n, i, ss, ee, led;
long m = 10000+millis();
playerPositionModifier = 0;
for(i = 0; i<conveyorCount; i++){
if(conveyorPool[i]._alive){
dir = conveyorPool[i]._dir;
ss = getLED(conveyorPool[i]._startPoint);
ee = getLED(conveyorPool[i]._endPoint);
for(led = ss; led<ee; led++){
b = 5;
n = (-led + (m/100)) % 5;
if(dir == -1) n = (led + (m/100)) % 5;
b = (5-n)/2.0;
if(b > 0) leds[led] = CRGB(0, 0, b);
}
if(playerPosition > conveyorPool[i]._startPoint && playerPosition < conveyorPool[i]._endPoint){
if(dir == -1){
playerPositionModifier = -(MAX_PLAYER_SPEED-4);
}else{
playerPositionModifier = (MAX_PLAYER_SPEED-4);
}
}
}
}
}
void drawAttack(){
if(!attacking) return;
int n = map(millis() - attackMillis, 0, ATTACK_DURATION, 100, 5);
for(int i = getLED(playerPosition-(ATTACK_WIDTH/2))+1; i<=getLED(playerPosition+(ATTACK_WIDTH/2))-1; i++){
leds[i] = CRGB(0, 0, n);
}
if(n > 90) {
n = 255;
leds[getLED(playerPosition)] = CRGB(255, 255, 255);
}else{
n = 0;
leds[getLED(playerPosition)] = CRGB(0, 255, 0);
}
leds[getLED(playerPosition-(ATTACK_WIDTH/2))] = CRGB(n, n, 255);
leds[getLED(playerPosition+(ATTACK_WIDTH/2))] = CRGB(n, n, 255);
}
int getLED(int pos){
// The world is 1000 pixels wide, this converts world units into an LED number
return constrain((int)map(pos, 0, 1000, 0, NUM_LEDS-1), 0, NUM_LEDS-1);
}
bool inLava(int pos){
// Returns if the player is in active lava
int i;
Lava LP;
for(i = 0; i<lavaCount; i++){
LP = lavaPool[i];
if(LP.Alive() && LP._state == "ON"){
if(LP._left < pos && LP._right > pos) return true;
}
}
return false;
}
void updateLives(){
// Updates the life LEDs to show how many lives the player has left
for(int i = 0; i<3; i++){
digitalWrite(lifeLEDs[i], lives>i?HIGH:LOW);
}
}
// ---------------------------------
// --------- SCREENSAVER -----------
// ---------------------------------
void screenSaverTick(){
int n, b, c, i;
long mm = millis();
int mode = (mm/20000)%2;
for(i = 0; i<NUM_LEDS; i++){
leds[i].nscale8(250);
}
if(mode == 0){
// Marching green <> orange
n = (mm/250)%10;
b = 10+((sin(mm/500.00)+1)*20.00);
c = 20+((sin(mm/5000.00)+1)*33);
for(i = 0; i<NUM_LEDS; i++){
if(i%10 == n){
leds[i] = CHSV( c, 255, 150);
}
}
}else if(mode == 1){
// Random flashes
randomSeed(mm);
for(i = 0; i<NUM_LEDS; i++){
if(random8(200) == 0){
leds[i] = CHSV( 25, 255, 100);
}
}
}
}
// ---------------------------------
// ----------- JOYSTICK ------------
// ---------------------------------
void getInput(){
// This is responsible for the player movement speed and attacking.
// You can replace it with anything you want that passes a -90>+90 value to joystickTilt
// and any value to joystickWobble that is greater than ATTACK_THRESHOLD (defined at start)
// For example you could use 3 momentery buttons:
// if(digitalRead(leftButtonPinNumber) == HIGH) joystickTilt = -90;
// if(digitalRead(rightButtonPinNumber) == HIGH) joystickTilt = 90;
// if(digitalRead(attackButtonPinNumber) == HIGH) joystickWobble = ATTACK_THRESHOLD;
accelgyro.getMotion6(&ax, &ay, &az, &gx, &gy, &gz);
int a = (JOYSTICK_ORIENTATION == 0?ax:(JOYSTICK_ORIENTATION == 1?ay:az))/166;
int g = (JOYSTICK_ORIENTATION == 0?gx:(JOYSTICK_ORIENTATION == 1?gy:gz));
if(abs(a) < JOYSTICK_DEADZONE) a = 0;
if(a > 0) a -= JOYSTICK_DEADZONE;
if(a < 0) a += JOYSTICK_DEADZONE;
MPUAngleSamples.add(a);
MPUWobbleSamples.add(g);
joystickTilt = MPUAngleSamples.getMedian();
if(JOYSTICK_DIRECTION == 1) {
joystickTilt = 0-joystickTilt;
}
joystickWobble = abs(MPUWobbleSamples.getHighest());
}
// ---------------------------------
// -------------- SFX --------------
// ---------------------------------
/*void SFXtilt(int amount){
int f = map(abs(amount), 0, 90, 80, 900)+random8(100);
if(playerPositionModifier < 0) f -= 500;
if(playerPositionModifier > 0) f += 200;
toneAC(f, min(min(abs(amount)/9, 5), MAX_VOLUME));
}
void SFXattacking(){
int freq = map(sin(millis()/2.0)*1000.0, -1000, 1000, 500, 600);
if(random8(5)== 0){
freq *= 3;
}
toneAC(freq, MAX_VOLUME);
}
void SFXdead(){
int freq = max(1000 - (millis()-killTime), 10);
freq += random8(200);
int vol = max(10 - (millis()-killTime)/200, 0);
toneAC(freq, MAX_VOLUME);
}
void SFXkill(){
toneAC(2000, MAX_VOLUME, 1000, true);
}
void SFXwin(){
int freq = (millis()-stageStartTime)/3.0;
freq += map(sin(millis()/20.0)*1000.0, -1000, 1000, 0, 20);
int vol = 10;//max(10 - (millis()-stageStartTime)/200, 0);
toneAC(freq, MAX_VOLUME);
}
void SFXcomplete(){
noToneAC();
}
*/

View file

73
docs/CONTROLS.md Normal file
View file

@ -0,0 +1,73 @@
# Bottoni
Nota: qui "Bottoni" indica ingressi digitali (pulsanti fisici collegati ai pin). Usa `src/config.h` come fonte di verità per i numeri dei pin.
- Sinistra/Destra: muovi — pulsanti per spostare il giocatore a sinistra o a destra. Pin: `BUTTON_LEFT` = GPIO 15, `BUTTON_RIGHT` = GPIO 2
- Attacco: wobble — il pulsante di attacco innesca l'azione di gioco chiamata "wobble"; nel firmware questo attiva il flag `IN.attack` che viene consumato dal loop. Pin: `BUTTON_ATTACK` = GPIO 4
- Start: pausa/resume — mette in pausa o riprende il gioco; genera un evento a singolo fronte (vedi `IN.startPressedEdge`). Pin: `BUTTON_START` = GPIO 5
## Seriale (115200)
- `a`, `d`, `w`, `s` - movimenti/azioni (descritti esaustivamente sotto)
- `debug` - modalità debug (attiva pattern di prova)
- `restart` - riavvia
- `status` - stampa stato
Nota: all'avvio il firmware esegue un test LED (R, G, B) per verificare connessione e ordine colori.
---
## Dettagli comandi seriali
Formato riga
- Ogni comando è una singola riga terminata da newline (Invio).
- La prima parola è il comando, tutto ciò che segue (dopo il primo spazio) è l'argomento (`arg`).
Esempi
- `debug` (Invio) — entra in modalità debug del gioco; vedrai un pattern di prova sui LED.
- `restart` (Invio) — riavvia la scheda (equivale a premere reset).
- `status` (Invio) — stampa su seriale informazioni di stato (placeholder).
- `level 2` (Invio) — comando hook futuro: non fa nulla attualmente ma l'argomento viene passato al parser.
Cosa succede nel codice
In pratica:
- `a` imposta `IN.left = true` e `IN.right = false` (simula pressione del pulsante "sinistra").
- `d` imposta `IN.right = true` e `IN.left = false` (simula pressione del pulsante "destra").
- `w` imposta `IN.attack = true` (simula l'azione di attacco).
- `s` imposta `IN.start = true` e `IN.startPressedEdge = true` (simula il pulsante Start e segnala il fronte di salita).
Questi flag vengono letti e gestiti dal ciclo principale (`loop`) al passo successivo; ad esempio `startPressedEdge` è pensato come evento a singolo fronte e viene resettato dopo l'uso. Per questo motivo i comandi sono "input simulati": non aspettarti un riscontro sincrono immediato via seriale, ma guarda il comportamento del gioco (LED, suoni, spostamenti) per verificare l'effetto.
Se vuoi simulare un "hold" (tenere premuto) devi ripetere il comando periodicamente o modificare il firmware per mantenere il flag attivo fino a nuovo comando.
Cosa aspettarsi (feedback)
- Molti comandi non producono echo/ACK automatico: osserva i LED o il comportamento del gioco per conferma.
- `debug` attiva pattern visivo; `restart` riavvia la board e vedrai il log di boot sulla seriale; `status` stampa testo.
Come inviare comandi (PlatformIO)
- Apri il monitor seriale:
platformio device monitor --baud 115200
- Scrivi il comando e premi Invio. Assicurati che il monitor usi newline come terminatore.
Troubleshooting rapido
- Niente succede: verifica porta COM e baud (115200). Assicurati di premere Invio.
- Comandi non riconosciuti: usa minuscolo (il parser confronta esattamente le stringhe come nel codice).
- Porta occupata/instabile: chiudi altre applicazioni che possono accedere alla COM (IDE, tool di upload).
Debug e miglioramenti possibili
- Se vuoi echo/ACK quando un comando viene ricevuto, posso aggiungere una piccola modifica che stampa `OK <cmd>` su seriale.
- Per test automatici puoi anche creare uno script che apre la seriale e invia sequenze di comandi per verificare input e reazioni.
Se vuoi che applichi il cambiamento che aggiunge echo/ACK ai comandi, dimmi e preparo la patch.

33
docs/HARDWARE.md Normal file
View file

@ -0,0 +1,33 @@
# Hardware e collegamenti
- Alimenta la strip WS2812B **con 5V dedicati** (non dal solo 5V USB se > 60 LED). Unisci i GND.
- Inserisci **resistenza 330-470 Ω** in serie sul DATA verso la strip e **condensatore 1000 µF/6.3V** tra 5V e GND vicino alla strip.
- Il progetto usa `NeoPixelBus` per il controllo LED sull'ESP32. Vedi `src/hardware/LedController.h` per le impostazioni (pin, ordine colori).
- Bottoni verso GND, pin configurati con `INPUT_PULLUP`.
- Buzzer passivo sul GPIO 18 (PWM via `ledc`).
## Mappatura pin (fonte: `src/config.h`)
Usa `src/config.h` come fonte di verità per i pin. La mappatura corrente nel codice è:
- `NUM_LEDS` = 144 — numero di LED della striscia
- `DATA_PIN` = GPIO 23 — pin dati per la ledstrip WS2812B (NeoPixel)
Bottoni (collegati a GND, usati con INPUT_PULLUP):
- `BUTTON_LEFT` = GPIO 15 — pulsante Sinistra
- `BUTTON_RIGHT` = GPIO 2 — pulsante Destra (attenzione: GPIO2 può influire sul boot su alcune board)
- `BUTTON_ATTACK` = GPIO 4 — pulsante Attacco (wobble)
- `BUTTON_START` = GPIO 5 — pulsante Start (genera `startPressedEdge`)
Audio / buzzer:
- `BUZZER_PIN` = GPIO 18 — buzzer passivo (gestito via ledc PWM)
LED Vita (opzionali, definiti ma non obbligatori):
- `LIFE_LED_1` = GPIO 19
- `LIFE_LED_2` = GPIO 21
- `LIFE_LED_3` = GPIO 22
Se vuoi cambiare i pin, modifica `src/config.h` e aggiorna eventuale cablaggio hardware; la documentazione qui deve rimanere sincronizzata con quel file.

27
docs/LEVELS.md Normal file
View file

@ -0,0 +1,27 @@
# Livelli
Vedi `src/game/Level.h` e `src/game/Level.cpp` per la struttura attuale. Per aggiungere nuove feature (trasportatori, boss) crea nuove classi e integrale in `GameEngine`.
## Nota sul controllo LED
Il progetto ora usa la libreria NeoPixelBus per pilotare strisce WS2812/NeoPixel. I valori di default (vedi `src/config.h`) sono:
- `NUM_LEDS` = 144
- `DATA_PIN` = 23
- `BRIGHTNESS` = 64
All'avvio il firmware esegue un test automatico che accende progressivamente tutti i LED in tre fasi: rosso, verde, blu. Questo permette di verificare rapidamente la connessione e l'ordine dei colori prima di entrare nel gioco.
## Consigli di build
- Se i colori risultano errati, prova a cambiare la feature in `src/hardware/LedController.h` (es. `NeoGrbFeature``NeoRgbFeature`).
- Per strip molto lunghe, dosa `BRIGHTNESS` e considera alimentazione dedicata per la strip (iniettata su più punti).
- Se la porta seriale è instabile in upload, tieni premuto BOOT all'inizio del flash.
## Roadmap nel codice (TODO)
- Gestione multi-livello e caricamento da tabella
- Effetti audio più ricchi (melodie, win/lose)
- Interfaccia WiFi (WebSerial + controller browser)
- Salvataggio punteggi in NVS
- Editor livelli via web

38
iSin.h
View file

@ -1,38 +0,0 @@
#include "Arduino.h"
class iSin
{
public:
int convert(long x);
private:
uint8_t isinTable8[91] = {
0, 4, 9, 13, 18, 22, 27, 31, 35, 40, 44,
49, 53, 57, 62, 66, 70, 75, 79, 83, 87,
91, 96, 100, 104, 108, 112, 116, 120, 124, 128,
131, 135, 139, 143, 146, 150, 153, 157, 160, 164,
167, 171, 174, 177, 180, 183, 186, 190, 192, 195,
198, 201, 204, 206, 209, 211, 214, 216, 219, 221,
223, 225, 227, 229, 231, 233, 235, 236, 238, 240,
241, 243, 244, 245, 246, 247, 248, 249, 250, 251,
252, 253, 253, 254, 254, 254, 255, 255, 255, 255
};
};
int iSin::convert(long x)
{
boolean pos = true; // positive - keeps an eye on the sign.
if (x < 0)
{
x = -x;
pos = !pos;
}
if (x >= 360) x %= 360;
if (x > 180)
{
x -= 180;
pos = !pos;
}
if (x > 90) x = 180 - x;
if (pos) return isinTable8[x]/2 ;
return -isinTable8[x]/2 ;
}

3390
inspect-output.txt Normal file

File diff suppressed because one or more lines are too long

17
platformio.ini Normal file
View file

@ -0,0 +1,17 @@
; PlatformIO Project Configuration File
;
; Build options: build flags, source filter
; Upload options: custom upload port, speed and extra flags
; Library options: dependencies, extra library storages
; Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
build_flags =
lib_deps = makuna/NeoPixelBus@^2.8.4

32
src/config.h Normal file
View file

@ -0,0 +1,32 @@
#pragma once
// ====== LED Strip ======
#define NUM_LEDS 120 // Numero di LED nella striscia
#define DATA_PIN 23 // Pin GPIO per i dati LED
#define BRIGHTNESS 64 // Luminosità ridotta
// ====== Bottoni ======
#define BUTTON_LEFT 15
#define BUTTON_RIGHT 2
#define BUTTON_ATTACK 4
#define BUTTON_START 5
// ====== Audio ======
#define BUZZER_PIN 18
#define BUZZER_CH 0
#define BUZZER_FREQ 2000
#define BUZZER_RES 10
// ====== LED Vita opzionali ======
#define LIFE_LED_1 19
#define LIFE_LED_2 21
#define LIFE_LED_3 22
// ====== Gioco ======
#define WORLD_LENGTH 1200 // coordinate virtuali (unità astratte)
#define PLAYER_SPEED 12 // unità per frame
#define ENEMY_SPEED 6
#define ATTACK_DURATION 220 // ms
// ====== Serial ======
#define SERIAL_BAUD 115200

2
src/game/Enemy.cpp Normal file
View file

@ -0,0 +1,2 @@
#include "Enemy.h"
#include "../config.h"

22
src/game/Enemy.h Normal file
View file

@ -0,0 +1,22 @@
#pragma once
#include <Arduino.h>
#include "../config.h"
class Enemy {
public:
int x = 800;
bool alive = true;
void update() {
if (!alive) return;
// Movimento più semplice: va e viene tra due punti fissi
static bool goingLeft = true;
if (goingLeft) {
x -= ENEMY_SPEED;
if (x <= 200) goingLeft = false;
} else {
x += ENEMY_SPEED;
if (x >= 1000) goingLeft = true;
}
}
};

78
src/game/GameEngine.cpp Normal file
View file

@ -0,0 +1,78 @@
#include "GameEngine.h"
#include "../utils/Debug.h"
#include "../config.h"
void GameEngine::startDebug() {
debugPhase=0; debugStepUntil=millis()+200; mode=Mode::DEBUG_MODE; DBGLN("[DEBUG] start");
}
void GameEngine::update(const InputState &in) {
switch(mode) {
case Mode::DEBUG_MODE: {
if (millis()>debugStepUntil) {
debugPhase++;
if (debugPhase==1) audio->beepStart();
if (debugPhase>=40) { mode=Mode::MENU; }
debugStepUntil = millis()+80;
}
if (in.startPressedEdge) { mode=Mode::MENU; audio->beepOK(); }
break;
}
case Mode::MENU: {
if (in.startPressedEdge) { mode=Mode::PLAYING; level.reset(player); audio->beepStart(); }
break;
}
case Mode::PLAYING: {
player.update(in.left, in.right, in.attack);
level.enemy.update();
// collisione semplice con nemico
if (abs(player.x - level.enemy.x) < 20 && level.enemy.alive) {
if (player.attacking) { level.enemy.alive=false; audio->beepOK(); }
else if (millis()-lastHit>600) { lives--; lastHit=millis(); audio->beepHit(); if (lives<=0) mode=Mode::GAMEOVER; }
}
// lava
if (level.isLava(player.x) && millis()-lastHit>600) { lives--; lastHit=millis(); audio->beepHit(); if (lives<=0) mode=Mode::GAMEOVER; }
// uscita
if (player.x >= level.exitX) { mode=Mode::WIN; audio->beepOK(); }
if (in.startPressedEdge) mode=Mode::PAUSED;
break;
}
case Mode::PAUSED: if (in.startPressedEdge) mode=Mode::PLAYING; break;
case Mode::WIN: if (in.startPressedEdge) { mode=Mode::MENU; } break;
case Mode::GAMEOVER: if (in.startPressedEdge) { lives=3; level.reset(player); mode=Mode::PLAYING; } break;
}
}
void GameEngine::render() {
leds->fadeAll(80);
// DEBUG pattern
if (mode==Mode::DEBUG_MODE) {
int idx = (millis()/50)%NUM_LEDS; leds->setPixel(idx, RgbColor(255, 255, 255));
leds->show(); return;
}
// HUD vite
for (int i=0;i<lives && i<3;i++) leds->setPixel(i, RgbColor(255, 255, 255));
// Lava
for (int x=level.lava.start; x<=level.lava.end; x+= (WORLD_LENGTH/NUM_LEDS)) {
int i = leds->worldToIndex(x); leds->setPixel(i, RgbColor(255, 69, 0));
}
// Uscita
leds->setPixel(leds->worldToIndex(level.exitX), RgbColor(0, 255, 0));
// Nemico
if (level.enemy.alive) leds->setPixel(leds->worldToIndex(level.enemy.x), RgbColor(255, 0, 0));
// Player
RgbColor pc = player.attacking ? RgbColor(138, 43, 226) : RgbColor(0, 0, 255);
leds->setPixel(leds->worldToIndex(player.x), pc);
leds->show();
}

24
src/game/GameEngine.h Normal file
View file

@ -0,0 +1,24 @@
#pragma once
#include <Arduino.h>
#include "Player.h"
#include "Level.h"
#include "../hardware/LedController.h"
#include "../hardware/AudioManager.h"
#include "../hardware/InputManager.h"
enum class Mode { DEBUG_MODE, MENU, PLAYING, PAUSED, WIN, GAMEOVER };
class GameEngine {
public:
void begin(LedController* l, AudioManager* a) { leds=l; audio=a; mode=Mode::DEBUG_MODE; startDebug(); }
void startDebug();
void update(const InputState &in);
void render();
private:
LedController* leds=nullptr; AudioManager* audio=nullptr;
Player player; Level level;
Mode mode=Mode::MENU; unsigned long debugStepUntil=0; int debugPhase=0;
int lives=3; unsigned long lastHit=0;
};

1
src/game/Level.cpp Normal file
View file

@ -0,0 +1 @@
#include "Level.h"

16
src/game/Level.h Normal file
View file

@ -0,0 +1,16 @@
#pragma once
#include <Arduino.h>
#include "Player.h"
#include "Enemy.h"
struct Lava { int start=400, end=520; };
class Level {
public:
int exitX = 1150;
Lava lava;
Enemy enemy;
void reset(Player &p) { p.reset(); enemy=Enemy(); }
bool isLava(int x) const { return x>=lava.start && x<=lava.end; }
};

2
src/game/Player.cpp Normal file
View file

@ -0,0 +1,2 @@
#include "Player.h"
#include "../config.h"

29
src/game/Player.h Normal file
View file

@ -0,0 +1,29 @@
#pragma once
#include <Arduino.h>
#include "../config.h"
class Player {
public:
int x = 50; // Posizione iniziale fissa
bool attacking = false;
void reset() {
x = 50;
attacking = false;
}
// Overload per compatibilità: permette di resettare il player a una posizione specifica
void reset(int startX) {
x = startX;
attacking = false;
}
void update(bool left, bool right, bool attack) {
// Movimento semplificato
if (left) x = max(0, x - PLAYER_SPEED);
if (right) x = min(WORLD_LENGTH, x + PLAYER_SPEED);
// Attacco semplificato
attacking = attack;
}
};

View file

@ -0,0 +1 @@
#include "AudioManager.h"

View file

@ -0,0 +1,27 @@
#pragma once
#include <Arduino.h>
#include "../config.h"
class AudioManager {
public:
void begin() {
ledcSetup(BUZZER_CH, BUZZER_FREQ, BUZZER_RES);
ledcAttachPin(BUZZER_PIN, BUZZER_CH);
}
void tone(uint16_t freq, uint16_t ms=0) {
ledcWriteTone(BUZZER_CH, freq);
if (ms) { toneUntil = millis() + ms; active = true; }
}
void noTone() { ledcWriteTone(BUZZER_CH, 0); active=false; }
void beepOK() { tone(1200, 80); }
void beepHit() { tone(300, 120); }
void beepStart() { tone(900, 150); }
void update() {
if (active && millis() > toneUntil) { noTone(); }
}
private:
bool active=false; unsigned long toneUntil=0;
};

View file

@ -0,0 +1 @@
#include "InputManager.h"

View file

@ -0,0 +1,31 @@
#pragma once
#include <Arduino.h>
#include "../config.h"
struct InputState {
bool left=false, right=false, attack=false, start=false;
bool startPressedEdge=false; // fronte di salita per pausa/menu
};
class InputManager {
public:
void begin() {
pinMode(BUTTON_LEFT, INPUT_PULLUP);
pinMode(BUTTON_RIGHT, INPUT_PULLUP);
pinMode(BUTTON_ATTACK, INPUT_PULLUP);
pinMode(BUTTON_START, INPUT_PULLUP);
}
void update(InputState &s) {
bool l = !digitalRead(BUTTON_LEFT);
bool r = !digitalRead(BUTTON_RIGHT);
bool a = !digitalRead(BUTTON_ATTACK);
bool st= !digitalRead(BUTTON_START);
// semplice debounce via campionamento (polling veloce nel loop)
s.startPressedEdge = (!prevStart && st);
s.left=l; s.right=r; s.attack=a; s.start=st; prevStart = st;
}
private:
bool prevStart=false;
};

View file

@ -0,0 +1 @@
#include "LedController.h"

View file

@ -0,0 +1,55 @@
#pragma once
#include <Arduino.h>
#include <NeoPixelBus.h>
#include "../config.h"
class LedController {
NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod> *strip;
public:
LedController() : strip(nullptr) {}
void begin() {
if (strip == nullptr) {
strip = new NeoPixelBus<NeoGrbFeature, Neo800KbpsMethod>(NUM_LEDS, DATA_PIN);
}
strip->Begin();
strip->Show(); // Initialize all pixels to 'off'
clear();
}
void clear() {
strip->ClearTo(RgbColor(0));
show();
}
void show() {
strip->Show();
}
void setPixel(int i, const RgbColor &c) {
if (i >= 0 && i < NUM_LEDS) {
strip->SetPixelColor(i, c);
}
}
// Overload per compatibilità con il vecchio codice
void setPixel(int i, uint8_t r, uint8_t g, uint8_t b) {
setPixel(i, RgbColor(r, g, b));
}
int worldToIndex(int x) {
return map(x, 0, WORLD_LENGTH, 0, NUM_LEDS-1);
}
// Aggiunta funzione fadeAll per GameEngine
void fadeAll(uint8_t amount) {
for (int i = 0; i < NUM_LEDS; i++) {
RgbColor color = strip->GetPixelColor(i);
uint8_t r = (uint8_t)((int)color.R * (255 - amount) / 255);
uint8_t g = (uint8_t)((int)color.G * (255 - amount) / 255);
uint8_t b = (uint8_t)((int)color.B * (255 - amount) / 255);
strip->SetPixelColor(i, RgbColor(r, g, b));
}
}
};

60
src/main.cpp Normal file
View file

@ -0,0 +1,60 @@
#include <Arduino.h>
#include "config.h"
#include "utils/Debug.h"
#include "utils/SerialComm.h"
#include "hardware/LedController.h"
#include "hardware/InputManager.h"
#include "hardware/AudioManager.h"
#include "game/GameEngine.h"
LedController leds; InputManager input; AudioManager audio; GameEngine game; SerialComm serialComm;
InputState IN;
static void handleSerial(const String& cmd, const String& arg) {
if (cmd == "debug") { game.startDebug(); }
else if (cmd == "restart") { ESP.restart(); }
else if (cmd == "a") { IN.left = true; IN.right = false; }
else if (cmd == "d") { IN.right = true; IN.left = false; }
else if (cmd == "w") { IN.attack = true; }
else if (cmd == "s") { IN.start = true; IN.startPressedEdge = true; }
else if (cmd == "level") { /* hook futuro: carica livelli per indice */ }
else if (cmd == "status") { Serial.printf("mode=? lives=?\n"); }
}
void setup() {
serialComm.begin(SERIAL_BAUD);
delay(100);
Serial.println("\nTWANG ESP32 boot");
leds.begin();
// Startup LED test: sequentially fill all LEDs with Red, then Green, then Blue
const uint16_t startupDelay = 20; // ms between lighting each LED
for (int phase = 0; phase < 3; ++phase) {
uint8_t r = (phase == 0) ? 255 : 0;
uint8_t g = (phase == 1) ? 255 : 0;
uint8_t b = (phase == 2) ? 255 : 0;
leds.clear(); leds.show();
for (int i = 0; i < NUM_LEDS; ++i) {
leds.setPixel(i, r, g, b);
leds.show();
delay(startupDelay);
}
delay(300);
}
// clear after test
leds.clear(); leds.show();
input.begin();
audio.begin();
game.begin(&leds, &audio);
}
void loop() {
serialComm.update(handleSerial);
input.update(IN);
game.update(IN);
game.render();
audio.update();
// reset flags temporanei derivanti da seriale
IN.startPressedEdge = false; IN.attack = false;
}

2
src/utils/Debug.cpp Normal file
View file

@ -0,0 +1,2 @@
#include "Debug.h"
// Intenzionalmente vuoto: macro inline.

1
src/utils/SerialComm.cpp Normal file
View file

@ -0,0 +1 @@
#include "SerialComm.h"

28
src/utils/SerialComm.h Normal file
View file

@ -0,0 +1,28 @@
#pragma once
#include <Arduino.h>
#include <functional>
class SerialComm {
public:
void begin(unsigned long baud) { Serial.begin(baud); buffer.reserve(64); }
// Chiama cb(cmd, arg) quando riceve una riga intera.
void update(std::function<void(const String&, const String&)> cb) {
while (Serial.available()) {
char c = (char)Serial.read();
if (c == '\n' || c == '\r') { handleLine(cb); }
else buffer += c;
}
}
private:
String buffer;
void handleLine(std::function<void(const String&, const String&)> cb) {
if (buffer.length() == 0) return;
buffer.trim();
int sp = buffer.indexOf(' ');
String cmd = sp >= 0 ? buffer.substring(0, sp) : buffer;
String arg = sp >= 0 ? buffer.substring(sp + 1) : "";
cb(cmd, arg);
buffer = "";
}
};

12
src/utils/debug.h Normal file
View file

@ -0,0 +1,12 @@
#pragma once
#include <Arduino.h>
#ifndef DEBUG
#define DEBUG 1
#endif
#if DEBUG
#define DBG(...) Serial.printf(__VA_ARGS__)
#define DBGLN(...) Serial.printf(__VA_ARGS__), Serial.println()
#else
#define DBG(...)
#define DBGLN(...)
#endif