Gra Pokemon

Typ_projektu
Arduino
Zdjecie główne
Krótki opis projektu

Projekt gry polegał na stworzeniu uproszczonego systemu walki Pokémon w formacie 6 vs 6 na platformie Arduino z wykorzystaniem wyświetlacza OLED 128×64. Inspiracją była klasyczna mechanika walk znana z pierwszych generacji gier Pokémon, gdzie kluczowe znaczenie mają typy, dobór ataków oraz zarządzanie drużyną. Ze względu na bardzo ograniczoną rozdzielczość oraz pamięć mikrokontrolera, interfejs musiał zostać maksymalnie uproszczony, zachowując jednocześnie czytelność i funkcjonalność. W projekcie zaimplementowano system tur, w którym gracz wybiera jeden z czterech ataków, a następnie wykonywana jest akcja gracza oraz przeciwnika.

Niezbędne elementy

[przykładowe - prosimy o edycję]

1. Płytka Arduino UNO

2. ekran 128x64

Opis projektu

Kod gry został napisany w środowisku Arduino w języku C/C++, z wykorzystaniem struktur i enumeracji do reprezentacji typów Pokémonów, ruchów oraz stanów gry. Dużym wyzwaniem było zarządzanie ograniczoną pamięcią RAM, dlatego dane takie jak nazwy ataków przechowywane są w pamięci programu (PROGMEM). Dodatkowo konieczne było zaprojektowanie prostego, ale efektywnego systemu przełączania stanów gry, obejmującego overworld, walkę oraz ekrany informacyjne.

Jednym z głównych problemów podczas realizacji projektu były błędy kompilacji wynikające z łączenia dwóch osobnych programów – overworldu oraz systemu walki. Konflikty typów (np. enumów takich jak Direction czy PokeType) powodowały błędy kompilatora, które rozwiązano poprzez ujednolicenie definicji i uporządkowanie struktury kodu. Kolejnym problemem był czarny ekran po wgraniu programu, który wynikał z błędnej inicjalizacji lub konfliktów w logice głównej pętli – został on rozwiązany poprzez uproszczenie struktury programu i rozdzielenie logiki na czytelne moduły.

W trakcie pracy pojawiły się również trudności związane z wyświetlaniem informacji na małym ekranie – konieczne było ograniczenie ilości tekstu i zastosowanie skrótów, aby wszystkie dane mieściły się w dostępnej przestrzeni. Ostatecznie udało się stworzyć działający system walki 6 vs 6, który zachowuje podstawowe mechaniki znane z oryginalnych gier, jednocześnie będąc dostosowanym do ograniczeń sprzętowych Arduino. Projekt pozwolił na zdobycie praktycznej wiedzy z zakresu programowania systemów wbudowanych, zarządzania pamięcią oraz projektowania interfejsów użytkownika.

Zdjęcia
kod programu
#include <SPI.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SH110X.h>


 

#define I2C_ADDRESS 0x3C
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
#define OLED_RESET -1


 

Adafruit_SH1106G display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, OLED_RESET);


 

const uint8_t BTN_UP     = 7;
const uint8_t BTN_DOWN   = 9;
const uint8_t BTN_LEFT   = 10;
const uint8_t BTN_RIGHT  = 8;
const uint8_t BTN_START  = 4;
const uint8_t BTN_INFO   = 5;


 

enum GameState {    //lista możliwych trybów
  STATE_TITLE,      // Gra wykonuje jeden tryb naraz bo widzieć co może robić a czego nie
  STATE_PLAYER_TURN,
  STATE_ENEMY_TURN,
  STATE_MESSAGE,
  STATE_GAME_OVER,
  STATE_MOVE_INFO
};


 

GameState gameState = STATE_TITLE;    //zmienna przechowująca bieżący stan gry


 

enum PokeType {     //lista możliwych typów pokemonów
  TYPE_WATER,
  TYPE_GHOST,
  TYPE_FIRE,
  TYPE_PSYCHIC,
  TYPE_DRAGON,
  TYPE_ELECTRIC,
  TYPE_ICE,
  TYPE_NORMAL,
  TYPE_STEEL
};


 

enum Species {    //lista możliwych pokemonów
  PK_LAPRAS,
  PK_GENGAR,
  PK_RAIKOU,
  PK_MAWILE,
  PK_LUGIA,
  PK_LATIAS,
  PK_MEWTWO,
  PK_ZAPDOS,
  PK_MOLTRES,
  PK_ARTICUNO,
  PK_MEW,
  PK_ARCEUS
};


 

struct MoveData {   //pełen opis ataku pokeomna
  const __FlashStringHelper* name;
  PokeType type;
  uint8_t power;
  bool heals;
  uint8_t healAmount;
};


 

struct Fighter {    //pełen opis pokemona
  uint8_t species;
  PokeType type;
  int hp;
  int maxHp;
};


 

Fighter playerTeam[6];
Fighter enemyTeam[6];


 

uint8_t playerActive = 0;   //zmienna mowiąca który twój pokemon jest teraz aktywny
uint8_t enemyActive = 0;    //zmienna mowiąca który pokemon przeciwnika jest teraz aktywny
uint8_t selectedMove = 0;   //zmienna mowiąca który atak jest zaznaczony


 

bool needRedraw = true;   //zmiena stwierdzająca czy należy narysować gre od nowa
bool enemyNeedsTurn = false;  //zminna mowiąca czy przeciwnik ma się ruszyć po danej akcji


 

unsigned long lastInputTime = 0;
const unsigned long inputDelay = 280;   //zmienna która robi delay by jedno klikniecie przycisku nie było widzane
                                        // jako kliniecie wielokrotne tego samego przycisku
char messageLine1[24];
char messageLine2[24];
unsigned long messageStart = 0;
unsigned long messageDuration = 900;


 

const __FlashStringHelper* typeName(PokeType t) {   //funkcja która wypisuje typ pokomona jako text
  switch (t) {
    case TYPE_WATER:    return F("WATER");
    case TYPE_GHOST:    return F("GHOST");
    case TYPE_FIRE:     return F("FIRE");
    case TYPE_PSYCHIC:  return F("PSY");
    case TYPE_DRAGON:   return F("DRAGON");
    case TYPE_ELECTRIC: return F("ELEC");
    case TYPE_ICE:      return F("ICE");
    case TYPE_STEEL:    return F("STEEL");
    default:            return F("NORM");
  }
}


 

const __FlashStringHelper* strongVsText(PokeType t) {   //funckja która zwraca na co jest dany atak jest mocny
  switch (t) {
    case TYPE_WATER:    return F("STRONG: FIRE");
    case TYPE_GHOST:    return F("STRONG: PSY");
    case TYPE_FIRE:     return F("STRONG: ICE");
    case TYPE_PSYCHIC:  return F("STRONG: GHOST");
    case TYPE_DRAGON:   return F("STRONG: DRAGON");
    case TYPE_ELECTRIC: return F("STRONG: WATER");
    case TYPE_ICE:      return F("STRONG: DRAGON");
    case TYPE_STEEL:    return F("STRONG: ICE");
    case TYPE_NORMAL:   return F("STRONG: NONE");
    default:            return F("STRONG: NONE");
  }
}


 

const __FlashStringHelper* weakVsText(PokeType t) {
  switch (t) {
    case TYPE_WATER:    return F("WEAK: ELEC");
    case TYPE_GHOST:    return F("WEAK: NORM");
    case TYPE_FIRE:     return F("WEAK: WATER");
    case TYPE_PSYCHIC:  return F("WEAK: PSY");
    case TYPE_DRAGON:   return F("WEAK: ICE");
    case TYPE_ELECTRIC: return F("WEAK: DRAGON");
    case TYPE_ICE:      return F("WEAK: FIRE");
    case TYPE_STEEL:    return F("WEAK: FIRE");
    case TYPE_NORMAL:   return F("WEAK: NONE");
    default:            return F("WEAK: NONE");
  }
}


 

const __FlashStringHelper* speciesName(uint8_t s) {
  switch (s) {
    case PK_LAPRAS:   return F("LAPRAS");
    case PK_GENGAR:   return F("GENGAR");
    case PK_RAIKOU:   return F("RAIKOU");
    case PK_MAWILE:   return F("MAWILE");
    case PK_LUGIA:    return F("LUGIA");
    case PK_LATIAS:   return F("LATIAS");
    case PK_MEWTWO:   return F("MEWTWO");
    case PK_ZAPDOS:   return F("ZAPDOS");
    case PK_MOLTRES:  return F("MOLTRES");
    case PK_ARTICUNO: return F("ARTICUNO");
    case PK_MEW:      return F("MEW");
    case PK_ARCEUS:   return F("ARCEUS");
    default:          return F("UNKNOWN");
  }
}


 

PokeType speciesType(uint8_t s) {     //funckcja która przypisuje typ do danego pokeomna
  switch (s) {
    case PK_LAPRAS:   return TYPE_WATER;
    case PK_GENGAR:   return TYPE_GHOST;
    case PK_RAIKOU:   return TYPE_ELECTRIC;
    case PK_MAWILE:   return TYPE_STEEL;
    case PK_LUGIA:    return TYPE_PSYCHIC;
    case PK_LATIAS:   return TYPE_DRAGON;
    case PK_MEWTWO:   return TYPE_PSYCHIC;
    case PK_ZAPDOS:   return TYPE_ELECTRIC;
    case PK_MOLTRES:  return TYPE_FIRE;
    case PK_ARTICUNO: return TYPE_ICE;
    case PK_MEW:      return TYPE_PSYCHIC;
    case PK_ARCEUS:   return TYPE_NORMAL;
    default:          return TYPE_NORMAL;
  }
}


 

int speciesMaxHP(uint8_t s) {   //funckja która przypisuje max hp do danego pokemona
  switch (s) {
    case PK_LAPRAS:   return 70;
    case PK_GENGAR:   return 50;
    case PK_RAIKOU:   return 80;
    case PK_MAWILE:   return 45;
    case PK_LUGIA:    return 80;
    case PK_LATIAS:   return 105;
    case PK_MEWTWO:   return 125;
    case PK_ZAPDOS:   return 110;
    case PK_MOLTRES:  return 110;
    case PK_ARTICUNO: return 110;
    case PK_MEW:      return 100;
    case PK_ARCEUS:   return 140;
    default:          return 100;
  }
}


 

MoveData getMove(uint8_t species, uint8_t idx) {    //funkcja która przypisuje każdemu pokemonowi 4 ataki
  MoveData m;
  m.name = F("MOVE");
  m.type = TYPE_NORMAL;
  m.power = 15;
  m.heals = false;
  m.healAmount = 0;


 

  switch (species) {
    case PK_LAPRAS:
      if (idx == 0) { m.name = F("SURF");      m.type = TYPE_WATER;    m.power = 22; }
      else if (idx == 1) { m.name = F("ICEBEAM");  m.type = TYPE_ICE;      m.power = 20; }
      else if (idx == 2) { m.name = F("PSYWAVE");   m.type = TYPE_PSYCHIC;  m.power = 17; }
      else if (idx == 3) { m.name = F("SLAM");      m.type = TYPE_NORMAL;   m.power = 16; }
      break;
    case PK_GENGAR:
      if (idx == 0) { m.name = F("SHADOW");    m.type = TYPE_GHOST;    m.power = 23; }
      else if (idx == 1) { m.name = F("THUNDER");   m.type = TYPE_ELECTRIC; m.power = 18; }
      else if (idx == 2) { m.name = F("PSYBEAM");   m.type = TYPE_PSYCHIC;  m.power = 16; }
      else if (idx == 3) { m.name = F("PUNCH");     m.type = TYPE_NORMAL;   m.power = 15; }
      break;
    case PK_RAIKOU:
      if (idx == 0) { m.name = F("THUNDER");   m.type = TYPE_ELECTRIC; m.power = 25; }
      else if (idx == 1) { m.name = F("CRUNCH");    m.type = TYPE_GHOST;    m.power = 18; }
      else if (idx == 2) { m.name = F("AURORA");    m.type = TYPE_ICE;      m.power = 17; }
      else if (idx == 3) { m.name = F("QATK");      m.type = TYPE_NORMAL;   m.power = 16; }
      break;
    case PK_MAWILE:
      if (idx == 0) { m.name = F("IRONHEAD"); m.type = TYPE_STEEL;    m.power = 22; }
      else if (idx == 1) { m.name = F("FIREFNG");  m.type = TYPE_FIRE;     m.power = 18; }
      else if (idx == 2) { m.name = F("ICEFNG");   m.type = TYPE_ICE;      m.power = 18; }
      else if (idx == 3) { m.name = F("BITE");      m.type = TYPE_GHOST;    m.power = 16; }
      break;
    case PK_LUGIA:
      if (idx == 0) { m.name = F("AEROBLAS");  m.type = TYPE_PSYCHIC;  m.power = 27; }
      else if (idx == 1) { m.name = F("HYDROPMP");  m.type = TYPE_WATER;    m.power = 25; }
      else if (idx == 2) { m.name = F("ICEWIND");   m.type = TYPE_ICE;      m.power = 18; }
      else if (idx == 3) { m.name = F("SWIFT");     m.type = TYPE_NORMAL;   m.power = 16; }
      break;
    case PK_LATIAS:
      if (idx == 0) { m.name = F("DRAGON");    m.type = TYPE_DRAGON;   m.power = 20; }
      else if (idx == 1) { m.name = F("PSYCHIC");   m.type = TYPE_PSYCHIC;  m.power = 21; }
      else if (idx == 2) { m.name = F("THUNDER");   m.type = TYPE_ELECTRIC; m.power = 18; }
      else if (idx == 3) { m.name = F("SWIFT");     m.type = TYPE_NORMAL;   m.power = 15; }
      break;


 

    case PK_MEWTWO:
      if (idx == 0) { m.name = F("PSYSTRKE");  m.type = TYPE_PSYCHIC;  m.power = 29; }
      else if (idx == 1) { m.name = F("SHADOW");    m.type = TYPE_GHOST;    m.power = 22; }
      else if (idx == 2) { m.name = F("ICEBEAM");   m.type = TYPE_ICE;      m.power = 20; }
      else if (idx == 3) { m.name = F("RECOVER");   m.type = TYPE_NORMAL;   m.power = 0; m.heals = true; m.healAmount = 20; }
      break;
    case PK_ZAPDOS:
      if (idx == 0) { m.name = F("THUNDER");   m.type = TYPE_ELECTRIC; m.power = 25; }
      else if (idx == 1) { m.name = F("HEATWVE");  m.type = TYPE_FIRE;     m.power = 18; }
      else if (idx == 2) { m.name = F("DRILLPCK"); m.type = TYPE_NORMAL;   m.power = 18; }
      else if (idx == 3) { m.name = F("PSYWAVE");  m.type = TYPE_PSYCHIC;  m.power = 16; }
      break;
    case PK_MOLTRES:
      if (idx == 0) { m.name = F("FLAMETHR");  m.type = TYPE_FIRE;     m.power = 24; }
      else if (idx == 1) { m.name = F("SUNBEAM");   m.type = TYPE_ELECTRIC; m.power = 17; }
      else if (idx == 2) { m.name = F("SKYATK");    m.type = TYPE_NORMAL;   m.power = 21; }
      else if (idx == 3) { m.name = F("PSYBEAM");   m.type = TYPE_PSYCHIC;  m.power = 15; }
      break;
    case PK_ARTICUNO:
      if (idx == 0) { m.name = F("ICEBEAM");  m.type = TYPE_ICE;      m.power = 22; }
      else if (idx == 1) { m.name = F("WATERPLS");  m.type = TYPE_WATER;    m.power = 18; }
      else if (idx == 2) { m.name = F("GUST");      m.type = TYPE_NORMAL;   m.power = 14; }
      else if (idx == 3) { m.name = F("MINDRD");    m.type = TYPE_PSYCHIC;  m.power = 12; }
      break;
    case PK_MEW:
      if (idx == 0) { m.name = F("PSYCHIC");   m.type = TYPE_PSYCHIC;  m.power = 21; }
      else if (idx == 1) { m.name = F("THUNDER");   m.type = TYPE_ELECTRIC; m.power = 18; }
      else if (idx == 2) { m.name = F("ICEBEAM");   m.type = TYPE_ICE;      m.power = 19; }
      else if (idx == 3) { m.name = F("MEGAPNC");   m.type = TYPE_NORMAL;   m.power = 18; }
      break;
    case PK_ARCEUS:
      if (idx == 0) { m.name = F("JUDGMNT");   m.type = TYPE_NORMAL;   m.power = 30; }
      else if (idx == 1) { m.name = F("PSYPWR");    m.type = TYPE_PSYCHIC;  m.power = 20; }
      else if (idx == 2) { m.name = F("ICEBLD");    m.type = TYPE_ICE;      m.power = 20; }
      else if (idx == 3) { m.name = F("RECOVER");   m.type = TYPE_NORMAL;   m.power = 0; m.heals = true; m.healAmount = 24; }
      break;
  }


 

  return m;
}


 

bool isSuperEffective(PokeType attackType, PokeType defenderType) {   //funkcja która sprawdz czy dany atak jest super efektywny
  if (attackType == TYPE_WATER    && defenderType == TYPE_FIRE)     return true;
  if (attackType == TYPE_FIRE     && defenderType == TYPE_ICE)      return true;
  if (attackType == TYPE_ELECTRIC && defenderType == TYPE_WATER)    return true;
  if (attackType == TYPE_ICE      && defenderType == TYPE_DRAGON)   return true;
  if (attackType == TYPE_GHOST    && defenderType == TYPE_PSYCHIC)  return true;
  if (attackType == TYPE_PSYCHIC  && defenderType == TYPE_GHOST)    return true;
  if (attackType == TYPE_DRAGON   && defenderType == TYPE_DRAGON)   return true;
  if (attackType == TYPE_STEEL    && defenderType == TYPE_ICE)      return true;
  return false;
}


 

bool isNotVeryEffective(PokeType attackType, PokeType defenderType) {   //funckja która sprawdza czy dany atak był słaby na dany typ pokemona
  if (attackType == TYPE_FIRE     && defenderType == TYPE_WATER)    return true;
  if (attackType == TYPE_WATER    && defenderType == TYPE_ELECTRIC) return true;
  if (attackType == TYPE_ELECTRIC && defenderType == TYPE_DRAGON)   return true;
  if (attackType == TYPE_ICE      && defenderType == TYPE_FIRE)     return true;
  if (attackType == TYPE_GHOST    && defenderType == TYPE_NORMAL)   return true;
  if (attackType == TYPE_PSYCHIC  && defenderType == TYPE_PSYCHIC)  return true;
  if (attackType == TYPE_STEEL    && defenderType == TYPE_FIRE)     return true;
  return false;
}


 

// funkcja która przelicza podstawową siłe ataku pokemona *150% jeśli jest super effekt lub *50% jeśli jest słaby
int applyTypeEffectiveness(int basePower, PokeType attackType, PokeType defenderType) {
  if (isSuperEffective(attackType, defenderType)) return basePower + (basePower / 2);
  if (isNotVeryEffective(attackType, defenderType)) {
    int reduced = basePower / 2;
    if (reduced < 1) reduced = 1;
    return reduced;
  }
  return basePower;
}


 

//funkcja która sprawdza czy przycisk jest całyczas wciśniety
bool buttonPressed(uint8_t pin) {
  return digitalRead(pin) == LOW;
}


 

//funckja która opóźnia wcisniecie jednego przycisku wielo krotnie
//czyli by gra nie regowała na wielie razy wcisnietego przycisku pomimo że wcisneliśmy go jeden raz
bool buttonJustPressed(uint8_t pin) {
  if (millis() - lastInputTime < inputDelay) return false;
  if (buttonPressed(pin)) {
    lastInputTime = millis();
    return true;
  }
  return false;
}


 

//funkcja która pokazuje napis po atak o obrażeniach zadanych przeciwnikowi
void setMessage(const char* l1, const char* l2, unsigned long durationMs = 900) {
  strncpy(messageLine1, l1, sizeof(messageLine1) - 1);
  messageLine1[sizeof(messageLine1) - 1] = '\0';
  strncpy(messageLine2, l2, sizeof(messageLine2) - 1);
  messageLine2[sizeof(messageLine2) - 1] = '\0';
  messageStart = millis();
  messageDuration = durationMs;
  gameState = STATE_MESSAGE;
  needRedraw = true;
}


 

//funkcja która tworzy obie drużyny pokemonów na start walki oraz przypisuje im hp
void initTeams() {
  uint8_t p[6] = {PK_LAPRAS, PK_GENGAR, PK_RAIKOU, PK_MAWILE, PK_LUGIA, PK_LATIAS};
  uint8_t e[6] = {PK_MOLTRES, PK_MEWTWO, PK_ZAPDOS, PK_ARTICUNO, PK_MEW, PK_ARCEUS};


 

  for (uint8_t i = 0; i < 6; i++) {
    playerTeam[i].species = p[i];
    playerTeam[i].type = speciesType(p[i]);
    playerTeam[i].maxHp = speciesMaxHP(p[i]);
    playerTeam[i].hp = playerTeam[i].maxHp;


 

    enemyTeam[i].species = e[i];
    enemyTeam[i].type = speciesType(e[i]);
    enemyTeam[i].maxHp = speciesMaxHP(e[i]);
    enemyTeam[i].hp = enemyTeam[i].maxHp;
  }


 

  playerActive = 0;
  enemyActive = 0;
  selectedMove = 0;
}


 

//funkcja która resetuje całą walke i rysuje ekran od nowa
void resetBattle() {
  initTeams();
  enemyNeedsTurn = false;
  gameState = STATE_PLAYER_TURN;
  needRedraw = true;
}


 

// funkcja która liczy ile pokemonów  jeszcze zyje w twojej druzynie
int aliveCount(Fighter team[]) {
  int c = 0;
  for (uint8_t i = 0; i < 6; i++) if (team[i].hp > 0) c++;
  return c;
}
// funkcja ta słuzy do zamiany pokemona jesli którys zginie
bool advanceToNextAlive(Fighter team[], uint8_t &idx) {
  for (uint8_t i = idx + 1; i < 6; i++) {
    if (team[i].hp > 0) { idx = i; return true; }
  }
  return false;
}


 

// funkcja odpowiada za rysowanie paska zycia
void drawHPBarWithText(int x, int y, int w, int h, int hp, int maxHp) {
  display.drawRect(x, y, w, h, SH110X_WHITE);
  int fillW = 0;
  if (hp > 0) {
    fillW = ((long)(w - 2) * hp) / maxHp;
    if (fillW < 1) fillW = 1;
  }
  if (fillW > 0) display.fillRect(x + 1, y + 1, fillW, h - 2, SH110X_WHITE);


 

  char hpText[12];
  snprintf(hpText, sizeof(hpText), "%d/%d", hp, maxHp);


 

  int textX = x + 8;
  int textY = y + 2;


 

  if ((textX + 28) < (x + 1 + fillW)) display.setTextColor(SH110X_BLACK);
  else display.setTextColor(SH110X_WHITE);


 

  display.setCursor(textX, textY);
  display.print(hpText);
  display.setTextColor(SH110X_WHITE);
}


 

// funkcja odpowiada za naryzwanie portretu pokemona przeciwnika
void drawEnemySprite(int x, int y) {
  display.drawCircle(x + 12, y + 10, 9, SH110X_WHITE);
  display.drawLine(x + 3, y + 10, x + 21, y + 10, SH110X_WHITE);
  display.fillCircle(x + 9, y + 7, 1, SH110X_WHITE);
  display.fillCircle(x + 15, y + 7, 1, SH110X_WHITE);
  display.drawLine(x + 8, y + 15, x + 16, y + 15, SH110X_WHITE);
  display.drawLine(x + 6, y + 2, x + 4, y, SH110X_WHITE);
  display.drawLine(x + 18, y + 2, x + 20, y, SH110X_WHITE);
}


 

//funckcja rysująca panel przeciwnika (pasek życia, nazwa przeciwnego pokoemona oraz jego typ)
void drawTopPanel() {
  Fighter &e = enemyTeam[enemyActive];
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(2, 0); display.print(speciesName(e.species));
  display.setCursor(2, 8); display.print(typeName(e.type));
  display.setCursor(104, 0); display.print(F("E")); display.print(aliveCount(enemyTeam));
  drawHPBarWithText(44, 2, 52, 12, e.hp, e.maxHp);
}


 

//funkcja rysuje panel naszego pokemona (pasek życia, nazwa przeciwnego pokoemona oraz jego typ)
void drawBottomPanel() {
  Fighter &p = playerTeam[playerActive];
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  display.setCursor(2, 18); display.print(speciesName(p.species));
  display.setCursor(2, 26); display.print(typeName(p.type));
  display.setCursor(104, 18); display.print(F("Y")); display.print(aliveCount(playerTeam));
  drawHPBarWithText(44, 20, 52, 12, p.hp, p.maxHp);
}


 

// funkcja która torzy kontrast kolorów dla ataku zaznaczonego
void drawMoveCell(int x, int y, int w, int h, bool selected, const MoveData &m) {
  if (selected) {
    display.fillRect(x, y, w, h, SH110X_WHITE);
    display.drawRect(x, y, w, h, SH110X_WHITE);
    display.setTextColor(SH110X_BLACK);
  } else {
    display.drawRect(x, y, w, h, SH110X_WHITE);
    display.setTextColor(SH110X_WHITE);
  }
  display.setTextSize(1);
  display.setCursor(x + 4, y + 4);
  if (m.name) display.print(m.name);
  else display.print(F("MOVE"));
}


 

//funkcja która pobiera 4 ataki pokemona i rysuje komórke w której je umieszcza
void drawMoveMenu() {
  Fighter &p = playerTeam[playerActive];
  MoveData m0 = getMove(p.species, 0);
  MoveData m1 = getMove(p.species, 1);
  MoveData m2 = getMove(p.species, 2);
  MoveData m3 = getMove(p.species, 3);
  drawMoveCell(0,  36, 63, 14, selectedMove == 0, m0);
  drawMoveCell(65, 36, 63, 14, selectedMove == 1, m1);
  drawMoveCell(0,  50, 63, 14, selectedMove == 2, m2);
  drawMoveCell(65, 50, 63, 14, selectedMove == 3, m3);
}


 

// funckja która rysuje ekran startowy
void drawTitleScreen() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(2);
  display.setCursor(18, 2);
  display.println(F("POKEMON"));
  display.setTextSize(1);
  display.setCursor(12, 22); display.println(F("TEAM BATTLE 6 VS 6"));
  display.setCursor(6, 38); display.println(F("START = BEGIN"));
  display.setCursor(4, 50); display.println(F("INFO BTN = PIN 5"));
  display.display();
}


 

// funkcja która rysuje cały ekran walki
void drawBattleScreen() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  drawTopPanel();
  drawBottomPanel();
  drawEnemySprite(101, 8);
  drawMoveMenu();
  display.display();
}


 

// funkcja która rysuje ekran komunikatu obrażen lub leczenia po ataku
void drawMessageScreen() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  drawTopPanel();
  drawBottomPanel();
  drawEnemySprite(101, 8);
  display.drawRect(2, 36, 124, 28, SH110X_WHITE);
  display.setCursor(8, 44); display.print(messageLine1);
  display.setCursor(8, 54); display.print(messageLine2);
  display.display();
}


 

//funkcja która rysuje ekran o aktualnie zaznaczonym ruchu
void drawMoveInfoScreen() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(1);
  Fighter &p = playerTeam[playerActive];
  MoveData m = getMove(p.species, selectedMove);
  display.drawRect(0, 0, 128, 64, SH110X_WHITE);
  display.setCursor(6, 6);  display.print(F("MOVE INFO"));
  display.setCursor(6, 18); if (m.name) display.print(m.name); else display.print(F("MOVE"));
  display.setCursor(6, 30); display.print(F("TYPE: ")); display.print(typeName(m.type));
  display.setCursor(6, 42); display.print(strongVsText(m.type));
  display.setCursor(6, 52); display.print(weakVsText(m.type));
  display.display();
}


 

//funkcja która rysuje ekran końcowy
void drawGameOverScreen() {
  display.clearDisplay();
  display.setTextColor(SH110X_WHITE);
  display.setTextSize(2);
  if (aliveCount(playerTeam) <= 0) {
    display.setCursor(20, 8); display.println(F("YOU LOSE"));
  } else {
    display.setCursor(24, 8); display.println(F("YOU WIN"));
  }
  display.setTextSize(1);
  display.setCursor(12, 34); display.print(F("YOU LEFT: ")); display.println(aliveCount(playerTeam));
  display.setCursor(12, 44); display.print(F("ENEMY LEFT: ")); display.println(aliveCount(enemyTeam));
  display.setCursor(10, 56); display.println(F("START = AGAIN"));
  display.display();
}


 

// funkcja centralna gdy zmina needRedraw == True sprawdza game state i wywołuje odpowiednia funkcje rysowania
void drawCurrentScreen() {
  if (!needRedraw) return;
  needRedraw = false;
  switch (gameState) {
    case STATE_TITLE:      drawTitleScreen(); break;
    case STATE_PLAYER_TURN:
    case STATE_ENEMY_TURN: drawBattleScreen(); break;
    case STATE_MESSAGE:    drawMessageScreen(); break;
    case STATE_GAME_OVER:  drawGameOverScreen(); break;
    case STATE_MOVE_INFO:  drawMoveInfoScreen(); break;
  }
}
// funkcja która obsługuje atak gracza
void playerAttack() {
  Fighter &p = playerTeam[playerActive];
  Fighter &e = enemyTeam[enemyActive];
  MoveData m = getMove(p.species, selectedMove);


 

  char l1[24], l2[24];


 

  if (m.heals) {
    p.hp += m.healAmount;
    if (p.hp > p.maxHp) p.hp = p.maxHp;
    snprintf(l1, sizeof(l1), "YOU USED HEAL");
    snprintf(l2, sizeof(l2), "+%d HP", m.healAmount);
    setMessage(l1, l2, 850);
    return;
  }


 

  int finalDamage = applyTypeEffectiveness(m.power, m.type, e.type);
  e.hp -= finalDamage;
  if (e.hp < 0) e.hp = 0;


 

  snprintf(l1, sizeof(l1), "DMG %d", finalDamage);
  if (isSuperEffective(m.type, e.type)) snprintf(l2, sizeof(l2), "SUPER!");
  else if (isNotVeryEffective(m.type, e.type)) snprintf(l2, sizeof(l2), "WEAK...");
  else snprintf(l2, sizeof(l2), "NORMAL");


 

  setMessage(l1, l2, 950);
}


 

// funkcja która wybiera atak przeciwnika
void enemyAttack() {
  Fighter &e = enemyTeam[enemyActive];
  Fighter &p = playerTeam[playerActive];
  MoveData m = getMove(e.species, random(0, 4));


 

  char l1[24], l2[24];


 

  if (m.heals) {
    e.hp += m.healAmount;
    if (e.hp > e.maxHp) e.hp = e.maxHp;
    snprintf(l1, sizeof(l1), "ENEMY HEALS");
    snprintf(l2, sizeof(l2), "+%d HP", m.healAmount);
    setMessage(l1, l2, 850);
    return;
  }


 

  int finalDamage = applyTypeEffectiveness(m.power, m.type, p.type);
  p.hp -= finalDamage;
  if (p.hp < 0) p.hp = 0;


 

  snprintf(l1, sizeof(l1), "ENEMY DMG %d", finalDamage);
  if (isSuperEffective(m.type, p.type)) snprintf(l2, sizeof(l2), "SUPER!");
  else if (isNotVeryEffective(m.type, p.type)) snprintf(l2, sizeof(l2), "WEAK...");
  else snprintf(l2, sizeof(l2), "NORMAL");


 

  setMessage(l1, l2, 950);
}


 

// funckja która sprawdza czy reset został wcisniety jeśli tak to wywołuje rest battle który resetuje walkę
void handleTitle() {
  if (buttonJustPressed(BTN_START)) resetBattle();
}


 

//te funkcje przesuwają znacznik po siatce ataków
void moveSelectionUp()    { if (selectedMove >= 2) selectedMove -= 2; }
void moveSelectionDown()  { if (selectedMove <= 1) selectedMove += 2; }
void moveSelectionLeft()  { if (selectedMove == 1) selectedMove = 0; else if (selectedMove == 3) selectedMove = 2; }
void moveSelectionRight() { if (selectedMove == 0) selectedMove = 1; else if (selectedMove == 2) selectedMove = 3; }


 

//funkcja która przetwarza wcisnięcie klawisza na wywołanie odpowiedniej funkcji np przesuniecia zaznaczenia ataku
void handlePlayerTurn() {
  if (buttonJustPressed(BTN_UP))    { moveSelectionUp(); needRedraw = true; }
  if (buttonJustPressed(BTN_DOWN))  { moveSelectionDown(); needRedraw = true; }
  if (buttonJustPressed(BTN_LEFT))  { moveSelectionLeft(); needRedraw = true; }
  if (buttonJustPressed(BTN_RIGHT)) { moveSelectionRight(); needRedraw = true; }


 

  if (buttonJustPressed(BTN_INFO)) {
    gameState = STATE_MOVE_INFO;
    needRedraw = true;
    return;
  }


 

  if (buttonJustPressed(BTN_START)) {
    enemyNeedsTurn = true;
    playerAttack();
  }
}


 

//funkcja która wywołuje atak przeciwnika
void handleEnemyTurn() {
  enemyNeedsTurn = false;
  enemyAttack();
}


 

//funkcja która obsługuje przebieg walki
//1 czeka aż mini komunikat ataku
//2 sprawdza czy któaś z drużyn nie przegrała
//3 jeśli przeciwnik padł szuka kolejnego pokomona
//4 jeśli nasz pokomon umarł szuka zastępstwa dla nas
//5 jeśli zaden nie padł przechodzi do tury przeciwnika
void handleMessage() {
  if (millis() - messageStart < messageDuration) return;


 

  if (aliveCount(playerTeam) <= 0 || aliveCount(enemyTeam) <= 0) {
    gameState = STATE_GAME_OVER;
    needRedraw = true;
    return;
  }


 

  if (enemyTeam[enemyActive].hp <= 0) {
    if (!advanceToNextAlive(enemyTeam, enemyActive)) {
      gameState = STATE_GAME_OVER;
      needRedraw = true;
      return;
    }
    gameState = STATE_PLAYER_TURN;
    selectedMove = 0;
    needRedraw = true;
    return;
  }


 

  if (playerTeam[playerActive].hp <= 0) {
    if (!advanceToNextAlive(playerTeam, playerActive)) {
      gameState = STATE_GAME_OVER;
      needRedraw = true;
      return;
    }
    gameState = STATE_PLAYER_TURN;
    selectedMove = 0;
    needRedraw = true;
    return;
  }


 

  gameState = enemyNeedsTurn ? STATE_ENEMY_TURN : STATE_PLAYER_TURN;
  needRedraw = true;
}
// obsługuje wejście na ekran końca gry
void handleGameOver() {
  if (buttonJustPressed(BTN_START)) {
    gameState = STATE_TITLE;
    needRedraw = true;
  }
}
// obsługuje wejscie na ekran informacji
void handleMoveInfo() {
  if (buttonJustPressed(BTN_INFO) || buttonJustPressed(BTN_START)) {
    gameState = STATE_PLAYER_TURN;
    needRedraw = true;
  }
}


 

// ta funkcja wykonuje się tylko raz i przydziela przyciskom konkretne piny oraz obraca ekran
void setup() {
  pinMode(BTN_UP, INPUT_PULLUP);
  pinMode(BTN_DOWN, INPUT_PULLUP);
  pinMode(BTN_LEFT, INPUT_PULLUP);
  pinMode(BTN_RIGHT, INPUT_PULLUP);
  pinMode(BTN_START, INPUT_PULLUP);
  pinMode(BTN_INFO, INPUT_PULLUP);


 

  randomSeed(analogRead(A0));


 

  delay(250);
  display.begin(I2C_ADDRESS, true);
  display.setRotation(2);
  display.clearDisplay();
  display.display();


 

  initTeams();
  needRedraw = true;
}


 

//pentla działajaca bez końca
void loop() {
  drawCurrentScreen();


 

  switch (gameState) {
    case STATE_TITLE:       handleTitle(); break;
    case STATE_PLAYER_TURN: handlePlayerTurn(); break;
    case STATE_ENEMY_TURN:  handleEnemyTurn(); break;
    case STATE_MESSAGE:     handleMessage(); break;
    case STATE_GAME_OVER:   handleGameOver(); break;
    case STATE_MOVE_INFO:   handleMoveInfo(); break;
  }
}


 

Youtube
Tagi
Nauka, Arduino