Big Tuto SFML 2 / Action-RPG : Legends of Meruvia

Chapitre 11 : Ajoutons des monstres !

 

Tutoriel présenté par : Jérémie F. Bellanger (Jay81)
Date d'écriture : 26 juin 2016
Date de révision : -

 

      Prologue

   Bien, nous avons désormais une grande map, offrant de nombreuses possibilités, mais... comment dire ? frown C'est un peu vide ! indecision

   Il est donc temps de peupler nos maps avec quelques monstres ! wink

   Dans ce chapitre, nous allons réemployer beaucoup de techniques déjà utilisées pour gérer notre joueur. A la différence près, que nos monstres devront se diriger tout seul (on leur programmera donc une Intelligence Artificielle - ou Bêtise Artificielle, c'est comme vous voulez cheeky !) et il n'y en aura pas qu'un (imaginez un jeu avec UN SEUL MONSTRE par map...) ! blush

   Il va donc falloir gérer un tableau de monstres, qui nous permettra d'afficher jusqu'à 50 monstres à l'écran (pourquoi 50 ? surprise Parce que c'est déjà pas mal wink, mais si vous en voulez des millions, lâchez-vous !! laugh) ! Bon, je ne vous cache pas que ce chapitre va être un peu long et ardu... frown Mais bon, il faut ce qu'il faut ! angel

   Qui plus est, contrairement au tuto Rabidja, nous allons ajouter ici deux monstres différents, avec des P.V. différents. Et cerise sur le gâteau, nous allons voir comment afficher une jauge de P.V. au-dessous d'eux. wink

 

 

      Classe et spritesheet

   Pour nos monstres, nous allons créer une nouvelle classe Monster, très proche de celle du Player. Ajoutez donc une nouvelle classe à votre projet avec les fichiers monster.cpp et monster.hwink

   Quoi ? surprise Le même type de classe que pour notre héros ? frown

   Mais oui, un monstre est très proche du héros et nécessite beaucoup de variables semblables. wink D'ailleurs, ceux qui souhaiteraient parfaire leur maîtrise du C++ pourraient s'amuser à créer des templates et des classes dérivées. cheeky

   Voilà, maintenant, avant de commencer, nous aurons besoin des feuilles de sprites de nos monstres. Je vous les redonne ci-dessous, mais normalement, elles devraient déjà figurer dans le dossier graphics de votre projet (notez qu'il existe aussi une variante verte de nos monstres, mais je ne m'en suis pas servi, car ils ne rendent pas très bien sur le gazon... wink) :

 

monster1.png

 

monster2.png

 

      Le code

 

   On va commencer par aller dans le fichier map.h, pour vérifier que les variables suivantes s'y trouvent bien. Si vous vous rappelez, je vous avais dit d'ajouter toute une floppée de variables d'un coup, comme cela, nous n'aurions pas besoin de venir les y copier une par une fastidieusement. wink Les voici :

- la variable nombreMonstres qui contiendra le nombre de monstres actuellement vivants, et qu'on devra donc gérer et afficher.

- la constante MONSTRES_MAX qui définira le nombre max de monstres qu'on acceptera d'initialiser par niveau (ici 50, mais vous pouvez en mettre plus, ce ne devrait pas être un réel problème en SFML2 wink). Une fois, ces 50 monstres initialisés, de nouveaux monstres ne seront pas créés, à moins que certains se fassent tuer. cheeky Notez toutefois qu'avec 50 monstres par map, y'a de quoi faire ! wink

   

Fichier : map.h : Vérifiez que ces variables y sont déjà :

//Nombre max de monstres à l'écran
int nombreMonstres;
 
//Et dans la partie CONSTANTES :
//Nombre max de monstres gérés
const int MONSTRES_MAX = 50;

 

 

      La classe Monster   

 

   Voilà, c'est dans cette classe que nous allons gérer nos monstres. Oui, mais de quoi va-t-on avoir besoin pour ça ? frown

   La réponse est plus ou moins dans l'en-tête de la classe ci-dessous (copiez-la dans le nouveau fichier monster.hwink.

   Elle ressemble beaucoup à celle du Player, comme je vous l'ai déjà dit, et je vous laisse donc la parcourir avant de revenir dessus. cheeky

 

Fichier : monster.h : Créer le fichier et y copier :

//Legends of Meruvia - C++ / SFML 2

//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger

 

#ifndef MONSTER_H

#define MONSTER_H

 

#include <SFML/Graphics.hpp>

#include <iostream>

 

class Map;

class Player;

 

 

class Monster

{

 

public:

 

//Constructeur

Monster();

 

//Accesseurs

int getType(void) const;

int getLife(void) const;

int getLifeMax(void) const;

int getX(void) const;

int getY(void) const;

int getW(void) const;

int getH(void) const;

float getDirX(void) const;

float getDirY(void) const;

int getFrameNumber(void) const;

int getFrameTimer(void) const;

int getFrameMax(void) const;

int getEtat(void) const;

int getDirection(void) const;

int getSaveX(void) const;

int getSaveY(void) const;

int getTimerMort(void) const;

int getIATimer(void) const;

int getInvincibleTimer(void) const;

int getisHurt(void) const;

int getHurtDirection(void) const;

 

 

//Mutateurs

void setLife(int valeur);

void setX(int valeur);

void setY(int valeur);

void setW(int valeur);

void setH(int valeur);

void setDirX(float valeur);

void setDirY(float valeur);

void setTimerMort(int valeur);

void setInvincibleTimer(int valeur);

void setisHurt(int valeur);

void setHurtDirection(int valeur);

 

//Fonctions

void initialize(int Atype, int x1, int y1);

void draw(Map &map, sf::RenderWindow &window);

int update(int monsterNumber, Map &map, Player &player, Monster monster[]);

void mapCollision(Map &map, Player &player);

bool collideWithMonsters(Monster &monster);

void copy(Monster &monster);

 

 

private:

 

//Variables de la classe en accès privé

 

// Points de vie/santé + chrono d'invicibilité

float life, lifeMax;

int invincibleTimer;

 

//Type de monstre

int type;

 

//Si le monstre est touché

int isHurt;

int hurtDirection;

 

// Coordonnées du sprite

int x, y;

 

// Largeur, hauteur du sprite

int h, w;

 

// Checkpoint pour le héros (actif ou non)

int checkpointActif;

// + coordonnées de respawn (réapparition)

int respawnX, respawnY;

 

// Variables utiles pour l'animation :

// Numéro de la frame (= image) en cours + timer

int frameNumber, frameTimer, frameMax;

// Nombre max de frames, état du sprite et direction

// dans laquelle il se déplace (gauche / droite)

int etat, direction;

//Timer pour l'IA

int IATimer;

 

// Variables utiles pour la gestion des collisions :

//Est-il sur le sol, chrono une fois mort

int timerMort;

 

//Vecteurs de déplacement temporaires avant détection

//des collisions avec la map

float dirX, dirY;

//Sauvegarde des coordonnées de départ

int saveX, saveY;

 

//Spritesheet

sf::Texture Texture[2];

sf::Sprite Sprite[2];

int numberOfMonstersSprites;

 

//LifeGauge

sf::Texture lifeGaugeTexture;

sf::Sprite lifeGauge;

sf::Texture energyTexture;

sf::Sprite energy;

 

 

/******************/

/* Constantes */

/******************/

 

/* Taille maxi de la map : 400 x 150 tiles */

const int MAX_MAP_X = 400;

const int MAX_MAP_Y = 150;

 

/* Taille d'une tile (32 x 32 pixels) */

const int TILE_SIZE = 32;

 

/* Constante pour l'animation */

const int TIME_BETWEEN_2_FRAMES_PLAYER = 4;

 

//Dimensions du monstre

const int MONSTER_WIDTH = 42;

const int MONSTER_HEIGTH = 42;

 

//Valeurs attribuées aux états/directions

const int IDLE = 0;

const int WALK = 1;

 

const int DEAD = 4;

 

const int DOWN = 0;

const int UP = 1;

const int RIGHT = 2;

const int LEFT = 3;

 

const int IATime = 50;

const int TIME_BETWEEN_2_SHOTS = 30;

const int MOONWALK_TIMER = 8;

 

// Taille de la fenêtre : 800x480 pixels

const int SCREEN_WIDTH = 800;

const int SCREEN_HEIGHT = 480;

 

 

};

#endif

 

   Hum, voyons un peu comment nous allons gérer nos monstres, maintenant. cheeky

   On veut qu'ils apparaissent quand on va rencontrer une tile monstre sur la map (que vous avez sans doute déjà placées un peu partout avec le level editor, sinon rassurez-vous, il y en a déjà avec les fichiers téléchargeables wink). Il va donc falloir tester dans la fonction map.draw() si on s'apprête à dessiner une tile monstre et si c'est le cas, ne pas le faire, la supprimer et initialiser le monstre à la place grâce à la fonction initialize(). De plus, suivant le numéro de la tile monstre, il faudra aussi initialiser le bon monstre ! cheeky

   Une fois notre monstre initialisé, il va falloir le gérer dans une fonction update() puis le dessiner dans une fonction draw() (que c'est original ! cheeky).

   Ensuite, comme pour le joueur, il va falloir tester les collisions avec la map, pour qu'il puisse se déplacer dessus et il nous faudra aussi tester les collisions avec le joueur pour pouvoir le blesser ou tuer le monstre ! wink Cool ! angel

   Je vous donne maintenant ci-dessous le code complet du fichier monster.cpp, que je vous laisse lire et on y revient ensuite wink :

 

Fichier : monster.cpp : Créer le fichier et y copier :

//Legends of Meruvia - C++ / SFML 2

//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger

 

#include "monster.h"

#include "player.h"

#include "map.h"

 

 

using namespace std;

using namespace sf;

 

 

//Constructeur

 

Monster::Monster()

{

numberOfMonstersSprites = 2;

 

//Chargement des spritesheets des monstres

for (int i = 0; i < numberOfMonstersSprites; i++)

{

if (!Texture[i].loadFromFile("graphics/monster" + to_string(i + 1) + ".png"))

{

// Erreur

cout << "Erreur durant le chargement des spritesheets des monstres." << endl;

}

else

Sprite[i].setTexture(Texture[i]);

}

 

if (!lifeGaugeTexture.loadFromFile("graphics/lifegauge.png"))

{

// Erreur

cout << "Erreur durant le chargement de la jauge de vie des monstres." << endl;

}

else

lifeGauge.setTexture(lifeGaugeTexture);

 

if (!energyTexture.loadFromFile("graphics/energy2.png"))

{

// Erreur

cout << "Erreur durant le chargement de l'énergie pour la jauge de vie des monstres." << endl;

}

else

energy.setTexture(energyTexture);

 

 

//Initialisation des variables :

type = 1;

dirX = 0;

dirY = 0;

life = lifeMax = 1;

invincibleTimer = 0;

x = y = 0;

h = w = 42;

checkpointActif = respawnX = respawnY = 0;

frameNumber = frameTimer = frameMax = 0;

etat = direction = 0;

timerMort = 0;

dirX = dirY = 0;

saveX = saveY = 0;

IATimer = IATime;

 

}

 

 

//Accesseurs

int Monster::getType(void) const { return type; }

int Monster::getLife(void) const { return life; }

int Monster::getLifeMax(void) const { return lifeMax; }

int Monster::getX(void) const { return x; }

int Monster::getY(void) const { return y; }

int Monster::getW(void) const { return w; }

int Monster::getH(void) const { return h; }

float Monster::getDirX(void) const { return dirX; }

float Monster::getDirY(void) const { return dirY; }

int Monster::getFrameNumber(void) const { return frameNumber; }

int Monster::getFrameTimer(void) const { return frameTimer; }

int Monster::getFrameMax(void) const { return frameMax; }

int Monster::getEtat(void) const { return etat; }

int Monster::getDirection(void) const { return direction; }

int Monster::getSaveX(void) const { return saveX; }

int Monster::getSaveY(void) const { return saveY; }

int Monster::getTimerMort(void) const { return timerMort; }

int Monster::getIATimer(void) const { return IATimer; }

int Monster::getInvincibleTimer(void) const { return invincibleTimer; }

int Monster::getisHurt(void) const { return isHurt; }

int Monster::getHurtDirection(void) const { return hurtDirection; }

 

 

//Mutateurs

void Monster::setLife(int valeur) { life = valeur; }

void Monster::setX(int valeur) { x = valeur; }

void Monster::setY(int valeur) { y = valeur; }

void Monster::setW(int valeur) { w = valeur; }

void Monster::setH(int valeur) { h = valeur; }

void Monster::setDirX(float valeur) { dirX = valeur; }

void Monster::setDirY(float valeur) { dirY = valeur; }

void Monster::setTimerMort(int valeur) { timerMort = valeur; }

void Monster::setInvincibleTimer(int valeur) { invincibleTimer = valeur; }

void Monster::setisHurt(int valeur) { isHurt = valeur; }

void Monster::setHurtDirection(int valeur) { hurtDirection = valeur; }

 

 

//Fonctions

 

void Monster::initialize(int Atype, int x1, int y1)

{

//On réinitialise la frame et le timer

frameNumber = 0;

frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;

frameMax = 8;

 

//On initialise le type

type = Atype;

 

if (type == 1)

life = lifeMax = 2;

else if (type == 2)

life = lifeMax = 3;

 

invincibleTimer = 0;

 

//On indique sa direction (il viendra à l'inverse du joueur, logique)

direction = LEFT;

 

/* Ses coordonnées de démarrage seront envoyées par la fonction drawMap() en arguments */

x = x1;

y = y1;

 

/* Hauteur et largeur de notre monstre (rajoutés dans les defs ;) ) */

w = MONSTER_WIDTH;

h = MONSTER_HEIGTH;

 

//Variables nécessaires au fonctionnement de la gestion des collisions comme pour le héros

timerMort = 0;

 

//On le met à l'état 0, étant donné qu'il n'en a qu'un seul (la marche)

etat = 0;

 

isHurt = 0;

hurtDirection = 0;

}

 

 

void Monster::draw(Map &map, RenderWindow &window)

{

/* Gestion du timer */

// Si notre timer (un compte à rebours en fait) arrive à zéro

if (frameTimer <= 0)

{

//On le réinitialise

frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;

 

//Et on incrémente notre variable qui compte les frames de 1 pour passer à la suivante

frameNumber++;

 

//Mais si on dépasse la frame max, il faut revenir à la première :

if (frameNumber >= frameMax)

frameNumber = 0;

 

}

//Sinon, on décrémente notre timer

else

frameTimer--;

 

 

//Ensuite, on peut passer la main à notre fonction

Sprite[type - 1].setPosition(Vector2f(x - map.getStartX(), y - map.getStartY()));

 

//Pour connaître le X de la bonne frame à dessiner, il suffit de multiplier

//la largeur du sprite par le numéro de la frame à afficher -> 0 = 0; 1 = 40; 2 = 80...

//On calcule le Y de la bonne frame à dessiner, selon la valeur de l'état du héros :

//Aucun Mouvement (Idle) = 0, marche (walk) = 1, etc...

//Tout cela en accord avec notre spritesheet, of course ;)

 

//Gestion du flip (retournement de l'image selon que le sprite regarde à droite ou à gauche)

if (direction == LEFT)

{

Sprite[type - 1].setTextureRect(sf::IntRect(

frameNumber * w,

(etat * 3 * h + (direction - 1) * h),

w, h));

window.draw(Sprite[type - 1]);

}

else

{

Sprite[type - 1].setTextureRect(sf::IntRect(

(frameNumber + 1) * w,

(etat * 3 * h + direction * h),

-w, h));

window.draw(Sprite[type - 1]);

}

 

//Dessin de la jauge de vie au-dessus du monstre

int gaugeX = x + (w - 38) / 2 - map.getStartX();

int gaugeY = y + h + 4 - map.getStartY();

lifeGauge.setPosition(Vector2f(gaugeX, gaugeY));

 

//Calcul et affichage de l'énergie

float gaugeEnergy = (life / lifeMax) * 32;

for (int i = 0; i < gaugeEnergy; i++)

{

energy.setPosition(gaugeX + 3 + i, gaugeY + 4);

window.draw(energy);

}

window.draw(lifeGauge);

 

}

 

 

 

int Monster::update(int monsterNumber, Map &map, Player &player, Monster monster[])

{

//Même fonctionnement que pour le joueur

if (timerMort == 0)

{

//On diminue le timer d'invincibilité et de recul

if (invincibleTimer > 0)

invincibleTimer--;

 

if (isHurt > 0)

isHurt--;

 

dirX = 0;

dirY = 0;

 

//Test de collision dans un mur : si la variable x ou y reste la même, deux tours de boucle

//durant, le monstre est bloqué et on lui fait faire demi-tour.

if (x == saveX && isHurt == 0)

{

if (direction == LEFT)

{

direction = RIGHT;

 

//Sécurité : système d'éjection du monstre pour

//l'évacuer s'il reste coincé dans un obstacle

if (y == saveY && direction == UP)

y += 1;

else if (y == saveY && direction == DOWN)

y -= 1;

}

 

else if (direction == RIGHT)

{

direction = LEFT;

 

//Sécurité : système d'éjection du monstre pour

//l'évacuer s'il reste coincé dans un obstacle

if (y == saveY && direction == UP)

y += 1;

else if (y == saveY && direction == DOWN)

y -= 1;

}

 

}

 

else if (y == saveY)

{

if (direction == UP)

{

direction = DOWN;

 

//Sécurité : système d'éjection du monstre pour

//l'évacuer s'il reste coincé dans un obstacle

if (x == saveX && direction == LEFT)

x += 1;

else if (x == saveX && direction == RIGHT)

x -= 1;

}

 

else if (direction == DOWN)

{

direction = UP;

 

//Sécurité : système d'éjection du monstre pour

//l'évacuer s'il reste coincé dans un obstacle

if (x == saveX && direction == LEFT)

x += 1;

else if (x == saveX && direction == RIGHT)

x -= 1;

}

 

}

 

//Si le timer de l'IA descend en-dessous de 0, il est temps

//de changer de direction au hasard.

if (IATimer <= 0 && isHurt == 0)

{

direction = rand() % 4;

IATimer = IATime;

}

else

IATimer--;

 

//Déplacement du monstre selon la direction

//s'il est touché

if (isHurt > 0)

{

if (hurtDirection == LEFT)

dirX -= 10;

else if (hurtDirection == RIGHT)

dirX += 10;

else if (hurtDirection == UP)

dirY -= 10;

else if (hurtDirection == DOWN)

dirY += 10;

}

else

{

if (direction == LEFT)

dirX -= 1;

else if (direction == RIGHT)

dirX += 1;

else if (direction == UP)

dirY -= 1;

else if (direction == DOWN)

dirY += 1;

}

 

 

 

//On sauvegarde les coordonnées du monstre pour gérer le demi-tour

//avant que mapCollision() ne les modifie.

saveX = x;

saveY = y;

 

//On détecte les collisions avec la map comme pour le joueur

mapCollision(map, player);

 

}

 

 

//On détecte les collisions avec les autres monstres

for (int i = 0; i < map.getNombreMonstres(); i++)

{

//On ne reteste pas le monstre avec lui-même, sinon

//gare à la cata !!

if (i != monsterNumber)

if (collideWithMonsters(monster[i]))

{

if (direction == UP)

{

direction = DOWN;

y = monster[i].getY() + monster[i].getH() + 1;

}

else if (direction == DOWN)

{

direction = UP;

y = monster[i].getY() - h - 1;

}

else if (direction == RIGHT)

{

direction = LEFT;

x = monster[i].getX() - w - 1;

}

else if (direction == LEFT)

{

direction = RIGHT;

x = monster[i].getX() + monster[i].getW() + 1;

}

 

}

 

 

}

 

//Si le monstre meurt, on active une tempo

if (timerMort > 0)

{

timerMort--;

 

/* Si le monstre meurt, on renvoie 2 pour le remplace simplement par le dernier

monstre du tableau dans le main puis on rétrécit le tableau d'une case

(on ne peut pas laisser de case vide), sinon on renvoie 0 */

if (timerMort == 0)

return 2;

else

return 0;

}

else

return 0;

}

 

 

 

void Monster::mapCollision(Map &map, Player &player)

{

 

int i, x1, x2, y1, y2;

 

/* Ensuite, on va tester les mouvements horizontaux en premier

(axe des X). On va se servir de i comme compteur pour notre boucle.

En fait, on va découper notre sprite en blocs de tiles pour voir

quelles tiles il est susceptible de recouvrir.

On va donc commencer en donnant la valeur de Tile_Size à i pour qu'il

teste la tile où se trouve le x du joueur mais aussi la suivante SAUF

dans le cas où notre sprite serait inférieur à la taille d'une tile.

Dans ce cas, on lui donnera la vraie valeur de la taille du sprite

Et on testera ensuite 2 fois la même tile. Mais comme ça notre code

sera opérationnel quelle que soit la taille de nos sprites ! */

 

if (h > TILE_SIZE)

i = TILE_SIZE;

else

i = h;

 

 

//On lance alors une boucle for infinie car on l'interrompra selon

//les résultats de nos calculs

for (;;)

{

//On va calculer ici les coins de notre sprite à gauche et à

//droite pour voir quelle tile ils touchent.

x1 = (x + dirX) / TILE_SIZE;

x2 = (x + dirX + w - 1) / TILE_SIZE;

 

//Même chose avec y, sauf qu'on va descendre au fur et à mesure

//pour tester toute la hauteur de notre sprite, grâce à notre

//fameuse variable i.

y1 = (y) / TILE_SIZE;

y2 = (y + i - 1) / TILE_SIZE;

 

//De là, on va tester les mouvements

//grâce aux vecteurs dirX et dirY, tout en testant avant qu'on

//se situe bien dans les limites de l'écran.

if (x1 >= 0 && x2 < MAX_MAP_X && y1 >= 0 && y2 < MAX_MAP_Y)

{

//Si on a un mouvement à droite

if (dirX > 0)

{

//On vérifie si les tiles recouvertes sont solides

if (map.getTile(y1, x2) == 1 || map.getTile(y2, x2) == 1)

{

// Si c'est le cas, on place le joueur aussi près que possible

// de ces tiles, en mettant à jour ses coordonnées. Enfin, on

//réinitialise son vecteur déplacement (dirX).

 

x = x2 * TILE_SIZE;

x -= (w + 1);

dirX = 0;

 

}

}

 

//Même chose à gauche

else if (dirX < 0)

{

if (map.getTile(y1, x1) == 1 || map.getTile(y2, x1) == 1)

{

x = (x1 + 1) * TILE_SIZE;

dirX = 0;

}

 

}

 

}

 

//On sort de la boucle si on a testé toutes les tiles le long de la hauteur du sprite.

if (i == h)

{

break;

}

 

//Sinon, on teste les tiles supérieures en se limitant à la heuteur du sprite.

i += TILE_SIZE;

 

if (i > h)

{

i = h;

}

}

 

 

//On recommence la même chose avec le mouvement vertical (axe des Y)

if (w > TILE_SIZE)

i = TILE_SIZE;

else

i = w;

 

 

for (;;)

{

x1 = (x) / TILE_SIZE;

x2 = (x + i) / TILE_SIZE;

 

y1 = (y + dirY) / TILE_SIZE;

y2 = (y + dirY + h) / TILE_SIZE;

 

if (x1 >= 0 && x2 < MAX_MAP_X && y1 >= 0 && y2 < MAX_MAP_Y)

{

if (dirY > 0)

{

// Déplacement en bas

if (map.getTile(y2, x1) == 1 || map.getTile(y2, x2) == 1)

{

//Si la tile est une tile solide, on y colle le monstre

y = y2 * TILE_SIZE;

y -= (h + 1);

dirY = 0;

}

 

}

 

else if (dirY < 0)

{

 

// Déplacement vers le haut

if (map.getTile(y1, x1) == 1 || map.getTile(y1, x2) == 1)

{

y = (y1 + 1) * TILE_SIZE;

dirY = 0;

}

 

}

}

 

//On teste la largeur du sprite (même technique que pour la hauteur précédemment)

if (i == w)

{

break;

}

 

i += TILE_SIZE;

 

if (i > w)

{

i = w;

}

}

 

/* Maintenant, on applique les vecteurs de mouvement si le sprite n'est pas bloqué */

x += dirX;

y += dirY;

 

//Et on contraint son déplacement aux limites de l'écran.

if (x < 0)

x = 0;

else if (x + w >= map.getMaxX())

x = map.getMaxX() - w;

else if (y < 0)

y = 0;

else if (y + h > map.getMaxY())

y = map.getMaxY() - h;

 

}

 

 

bool Monster::collideWithMonsters(Monster &monster)

{

//Fonction de gestion des collisions

//On teste pour voir s'il n'y a pas collision, si c'est le cas, on renvoie false

if ((monster.getX() >= x + w)

|| (monster.getX() + monster.getW() <= x)

|| (monster.getY() >= y + h)

|| (monster.getY() + monster.getH() <= y)

)

return false;

//Sinon, on renvoie true

else

return true;

}

 

 

 

void Monster::copy(Monster &monster)

{

//Copie des variables nécessaires :

type = monster.getType();

life = monster.getLife();

lifeMax = monster.getLifeMax();

dirX = monster.getDirX();

dirY = monster.getDirY();

isHurt = monster.getisHurt();

hurtDirection = monster.getHurtDirection();

x = monster.getX();

y = monster.getY();

h = monster.getH();

w = monster.getW();

frameNumber = monster.getFrameNumber();

frameTimer = monster.getFrameTimer();

frameMax = monster.getFrameMax();

etat = monster.getEtat();

direction = monster.getDirection();

saveX = monster.getSaveX();

saveY = monster.getSaveY();

timerMort = monster.getTimerMort();

IATimer = monster.getIATimer();

invincibleTimer = monster.getInvincibleTimer();

} 

 

   On va ensuite développer un peu plus longuement les fonctions suivantes : wink

 

     La fonction update() :

   Elle ressemble beaucoup à celle de la classe Player, mais avec des ajouts concernant l'IA en plus. Comme notre monstre peut se déplacer dans 4 directions, les possibilités sont plus complexes que pour un jeu de plateformes (Cf. Tuto Rabidja).

   De base, nos monstres ne font que d'avancer, cependant, en plus de tester les collisions avec la map comme pour le joueur, on teste si celui-ci reste coincé (2 tours de suite à la même position, car bloqué contre un mur) et dans ce cas-là, on le fait changer de direction. On teste aussi s'il rencontre un autre monstre, pour éviter qu'ils ne se chevauchent. En effet, si ce n'est pas très gênant dans un jeu de plateformes (on peut penser que les monstres marchent côte à côte), c'est plus difficilement concevable dans un jeu vu du dessus ! cheeky

   Concernant les déplacements, on ne peut pas laisser le monstre se déplacer indéfiniment dans la même direction, ça n'aurait pas de sens ! surprise On met donc en place un chrono : IATimer au bout duquel, on changera la direction du monstre au hasard grâce à la formule : direction = rand() % 4; (notez que la même direction pourra être retirée au hasard wink).

   On gère également le recul du monstre, quand ce dernier sera touché par un coup d'épée, ou par de la magie (à venir dans les chapitres suivants wink). En effet, s'il est touché (variable isHurt à true), on ne veut pas que notre monstre continue à foncer droit dans le joueur, mais plutôt qu'il recule ! C'est ce qu'on fait ici, en utilisant la variable hurtDirection qui enregistrera plus tard la direction d'où venait le coup pour pouvoir reculer du bon côté ! wink

    Enfin, la fonction renvoie normalement 0 (return 0;) sauf si le monstre meurt. Dans ce cas, on renvoie 2, pour indiquer au main() qu'il doit lui régler son compte ! laugh On verra cela en fin de chapitre.

 

   Mais, au fait, on ne gère qu'un seul monstre dans notre classe ! surprise Comment va-t-on faire pour en avoir plusieurs ! frown

   C'est exact, mais vous oubliez qu'on est en POO, et qu'on peut donc créer autant d'objets Monster que l'on souhaite ! angel

   Dans notre main(), on va donc créer 50 objets de type Monster, qu'on va ensuite passer en boucle (on effectuera donc l'update de chaque objet à la suite wink).

   Chaque objet contiendra donc les infos d'un monstre, mais là où cela va se compliquer, c'est justement quand l'un d'eux va mourir. Je vous ai déjà indiqué que la fonction update() allait renvoyer 2 dans ce cas, mais que va en faire le main() ? En effet, on ne peut pas laisser de case vide dans notre tableau d'objets Monster (on aurait pu utiliser une variable : alive ou active éventuellement, mais on aurait perdu de la place dans notre tableau et ça aurait compliqué les choses au final wink).

    Non, le plus simple, c'est tout simplement de copier le monstre de la dernière case du tableau dans la case du monstre mort et de réduire la taille de notre tableau d'une case (la dernière déjà copiée) en réduisant d'1 la variable nombreMonstres de la classe Map (cf ci-dessus). C'est pour cela que notre classe contient également une fonction copy() qui permet de copier les variables d'un objet Monster à un autre. smiley Cette fonction s'apparente un peu à un constructeur par copie, à la différence qu'ici notre objet existe déjà. wink

 

Note : Nous avons deux types de monstres différents, cependant leurs différences se situent simplement au niveau du sprite et du nombre de P.V., cela n'a donc pas d'impact sur notre fonction update(). Toutefois, vous pouvez envisager de les faire se déplacer à des vitesses différentes (en utilisant if (type == 1) par exemple). wink

 

     La fonction collideWithMonsters() :

   C'est elle qui va gérer les collisions entre les monstres.

   Son fonctionnement est simple : on teste si les deux sprites ne se touchent pas. Si c'est la cas, on renvoie false, et c'est fini ! wink

   Sinon, c'est qu'ils se touchent et on renvoie true ! cheeky

 

     La fonction draw() :

   Cette fonction est similaire à celle qui affiche le héros. Mais cette fois, on va afficher de méchants monstres ! laugh On n'a pas non plus la partie qui gère le clignotement, mais vous pouvez la rajouter, si vous préférez. wink

   Sinon, vous remarquerez qu'on affiche le bon sprite, selon notre tableau de sprite en écrivant : window.draw(Sprite[type - 1]);

   Pourquoi le -1 ? frown Tout simplement parce que le sprite du monstre 1 se trouve dans la case 0 de notre tableau (erreur classique en prog !) laugh

   Vous noterez enfin qu'on affiche une jauge de PV (lifegauge) sous le sprite de notre monstre. On la remplit à l'aide d'un sprite d'1 pixel de large (energy). Pour ce faire, on applique un simple produit en croix : (life / lifeMax) * 32. Mais pourquoi 32 ? frown Eh bien simplement car notre jauge fait 32 pixels de long ! wink

 

     La fonction mapCollision() :

   Elle est identique à celle de la classe Player, de base. Notre monstre ne pourra pas entrer dans des bâtiments ou changer de map ! cheeky (Manquerait plus que ça ! laugh)

 

     La fonction copy() :

   Comme nous l'avons évoqué ci-dessus, cette fonction permet de copier les variables d'un objet Monster dans un autre, pour supprimer les monstres morts. wink

   Et voilà pour notre classe Monster qui compose le plus gros de ce chapitre avec ce qui va suivre : la détection des tiles monstres ! On est parti ! wink

 

 

      Détectons les tiles Monster  

 

   On retourne dans notre fichier map.cpp pour rajouter d'abord un include vers notre fichier monster.h: 

 

Fichier : map.cpp : Rajouter en haut du fichier :

#include "monster.h"

 

   Puis on rajoute une fonction detectTilesSpeciales() à notre fichier map.cpp. C'est elle qui va se charger de repérer si la tile à dessiner par la fonction draw() est une tile monstre ou pas. Si tel est le cas, on initialisera un monstre si on n'a pas déjà atteint la limite MONSTRES_MAX et on effacera la tile du tableau en la remplaçant par la tile 0, afin de ne pas créer des monstres en boucle à chaque frame ! surprise Notez que cela n'a aucune incidence sur notre fichier map. wink

   Pour ceux qui ont suivi les tutos antérieurs, vous noterez aussi que cette fonction faisait généralement partie de la fonction draw() dans ces autres tutos. J'ai choisi de l'en dissocier ici par souci de lisibilité, c'est tout. wink

 

Fichier : map.cpp : Rajouter la fonction detectTilesSpeciales() :

void Map::detectTilesSpeciales(int mapY, int mapX, Monster monster[])

{

//Si la tile à dessiner n'est pas une tile vide

if (tile4[mapY][mapX] != 0)

{

/*On teste si c'est une tile monstre */

if (tile4[mapY][mapX] >= TILE_MONSTRE_DEBUT

&& tile4[mapY][mapX] <= TILE_MONSTRE_FIN)

{

//Si on le peut, on initialise un monstre en envoyant les coordonnées de la tile

if (nombreMonstres < MONSTRES_MAX)

{

monster[nombreMonstres].initialize(tile4[mapY][mapX] - TILE_MONSTRE_DEBUT + 1,

mapX * TILE_SIZE, mapY * TILE_SIZE);

nombreMonstres++;

}

 

//Et on efface cette tile de notre tableau pour éviter un spawn de monstres

//infini !

tile4[mapY][mapX] = 0;

}

 

}

 

}

  

    Modifions désormais notre fonction map.draw(). Cela ne va pas être bien compliqué puisqu'il suffira juste d'ajouter l'appel à notre fonction detectTilesSpeciales() dans la boucle de la layer 1 :

 

Fichier : map.cpp : Remplacer la fonction précédente par :

void Map::draw(int layer, RenderWindow &window, Monster monster[])

{

int x, y, mapX, x1, x2, mapY, y1, y2, xsource, ysource, a;

 

/* On initialise mapX à la 1ère colonne qu'on doit blitter.

Celle-ci correspond au x de la map (en pixels) divisés par la taille d'une tile (32)

pour obtenir la bonne colonne de notre map

Exemple : si x du début de la map = 1026, on fait 1026 / 32

et on sait qu'on doit commencer par afficher la 32eme colonne de tiles de notre map */

mapX = startX / TILE_SIZE;

 

/* Coordonnées de départ pour l'affichage de la map : permet

de déterminer à quels coordonnées blitter la 1ère colonne de tiles au pixel près

(par exemple, si la 1ère colonne n'est visible qu'en partie, on devra commencer à blitter

hors écran, donc avoir des coordonnées négatives - d'où le -1). */

x1 = (startX % TILE_SIZE) * -1;

 

/* Calcul des coordonnées de la fin de la map : jusqu'où doit-on blitter ?

Logiquement, on doit aller à x1 (départ) + SCREEN_WIDTH (la largeur de l'écran).

Mais si on a commencé à blitter en dehors de l'écran la première colonne, il

va falloir rajouter une autre colonne de tiles sinon on va avoir des pixels

blancs. C'est ce que fait : x1 == 0 ? 0 : TILE_SIZE qu'on pourrait traduire par:

if(x1 != 0)

x2 = x1 + SCREEN_WIDTH + TILE_SIZE , mais forcément, c'est plus long ;)*/

x2 = x1 + SCREEN_WIDTH + (x1 == 0 ? 0 : TILE_SIZE);

 

/* On fait exactement pareil pour calculer y */

mapY = startY / TILE_SIZE;

y1 = (startY % TILE_SIZE) * -1;

y2 = y1 + SCREEN_HEIGHT + (y1 == 0 ? 0 : TILE_SIZE);

 

 

//On met en place un timer pour animer la map

if (mapTimer <= 0)

{

if (tileSetNumber == 0)

{

tileSetNumber = 1;

mapTimer = TIME_BETWEEN_2_FRAMES * 3;

}

else

{

tileSetNumber = 0;

mapTimer = TIME_BETWEEN_2_FRAMES * 3;

}

 

}

else

mapTimer--;

 

 

/* Dessine la carte en commençant par startX et startY */

 

/* On dessine ligne par ligne en commençant par y1 (0) jusqu'à y2 (480)

A chaque fois, on rajoute TILE_SIZE (donc 32), car on descend d'une ligne

de tile (qui fait 32 pixels de hauteur) */

if (layer == 1)

{

for (y = y1; y < y2; y += TILE_SIZE)

{

/* A chaque début de ligne, on réinitialise mapX qui contient la colonne

(0 au début puisqu'on ne scrolle pas) */

mapX = startX / TILE_SIZE;

 

/* A chaque colonne de tile, on dessine la bonne tile en allant

de x = 0 à x = 640 */

for (x = x1; x < x2; x += TILE_SIZE)

{

//On détecte les tiles spéciales

detectTilesSpeciales(mapY, mapX, monster);

 

/* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro

de la tile */

a = tile[mapY][mapX];

 

/* Calcul pour obtenir son y (pour un tileset de 10 tiles

par ligne, d'où le 10 */

ysource = a / 10 * TILE_SIZE;

/* Et son x */

xsource = a % 10 * TILE_SIZE;

 

/* Fonction qui blitte la bonne tile au bon endroit suivant le timer */

if (tileSetNumber == 0)

{

tileSet1.setPosition(Vector2f(x, y));

tileSet1.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));

window.draw(tileSet1);

}

else

{

tileSet1B.setPosition(Vector2f(x, y));

tileSet1B.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));

window.draw(tileSet1B);

}

 

mapX++;

}

 

mapY++;

}

}

 

else if (layer == 2)

{

//Deuxième couche de tiles ;)

for (y = y1; y < y2; y += TILE_SIZE)

{

mapX = startX / TILE_SIZE;

 

for (x = x1; x < x2; x += TILE_SIZE)

{

/* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro

de la tile */

a = tile2[mapY][mapX];

 

/* Calcul pour obtenir son y (pour un tileset de 10 tiles

par ligne, d'où le 10 */

ysource = a / 10 * TILE_SIZE;

/* Et son x */

xsource = a % 10 * TILE_SIZE;

 

/* Fonction qui blitte la bonne tile au bon endroit suivant le timer */

if (tileSetNumber == 0)

{

tileSet1.setPosition(Vector2f(x, y));

tileSet1.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));

window.draw(tileSet1);

}

else

{

tileSet1B.setPosition(Vector2f(x, y));

tileSet1B.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));

window.draw(tileSet1B);

}

 

mapX++;

}

 

mapY++;

}

}

 

else if (layer == 3)

{

//Troisième couche de tiles ;)

for (y = y1; y < y2; y += TILE_SIZE)

{

mapX = startX / TILE_SIZE;

 

for (x = x1; x < x2; x += TILE_SIZE)

{

/* Suivant le numéro de notre tile, on découpe le tileset (a = le numéro

de la tile */

a = tile3[mapY][mapX];

 

/* Calcul pour obtenir son y (pour un tileset de 10 tiles

par ligne, d'où le 10 */

ysource = a / 10 * TILE_SIZE;

/* Et son x */

xsource = a % 10 * TILE_SIZE;

 

/* Fonction qui blitte la bonne tile au bon endroit suivant le timer */

if (tileSetNumber == 0)

{

tileSet1.setPosition(Vector2f(x, y));

tileSet1.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));

window.draw(tileSet1);

}

else

{

tileSet1B.setPosition(Vector2f(x, y));

tileSet1B.setTextureRect(sf::IntRect(xsource, ysource, TILE_SIZE, TILE_SIZE));

window.draw(tileSet1B);

}

 

mapX++;

}

 

mapY++;

}

}

 

 

}

 

   Sans oublier d'adapter le header en rajoutant la class Monster en haut du fichier, en ajoutant les prototypes de detectTilesSpeciales() et en mettant à jour le prototype de draw()wink

 

Fichier : map.h : Modifier le code suivant :

//Fonctions

void loadMap(std::string filename);

void draw(int layer, sf::RenderWindow &window, Monster monster[]);

void changeLevel(void);

void testDefilement(void);

void detectTilesSpeciales(int mapY, int mapX, Monster monster[]);

POINT detectWarpSpe(int number);

 

 

     Mise à jour du fichier player.cpp

 

   Mettons désormais à jour notre fichier player.cpp en rajoutant map.setNombreMonstres(0); aux fonctions initialize() et reinitialize() :

 

Fichier : player.cpp : Mettre à jour les fonctions :

void Player::initialize(Map &map)

{

//PV à 3

life = 3;

 

//Timer d'invincibilité à 0

invincibleTimer = 0;

 

//Indique l'état et la direction de notre héros

direction = RIGHT;

etat = IDLE;

 

//Indique le numéro de la frame où commencer

frameNumber = 0;

 

//...la valeur de son chrono ou timer

frameTimer = TIME_BETWEEN_2_FRAMES_PLAYER;

 

//... et son nombre de frames max (8 pour l'anim' IDLE

// = ne fait rien)

frameMax = 8;

 

x = map.getBeginX();

y = map.getBeginY();

 

//On recentre la caméra

map.setStartX(map.getBeginX() - (SCREEN_WIDTH / 2));

map.setStartY(map.getBeginY() - (SCREEN_HEIGHT / 2));

 

/* Hauteur et largeur de notre héros */

w = PLAYER_WIDTH;

h = PLAYER_HEIGTH;

 

//Variables nécessaires au fonctionnement de la gestion des collisions

timerMort = 0;

isAttacking = 0;

 

//On réinitialise le nombre de monstres

map.setNombreMonstres(0);

 

map.setWarpDirection(-1);

 

}

 

 

void Player::reinitialize(Map &map)

{

 

// Coordonnées de démarrage de notre héros selon la direction de warp

//Si on n'a pas warpé, alors on commence aux coordonnées de départ de la map

if (map.getWarpDirection() == -1)

{

x = map.getBeginX();

y = map.getBeginY();

 

//On recentre la caméra

map.setStartX(map.getBeginX() - (SCREEN_WIDTH / 2));

map.setStartY(map.getBeginY() - (SCREEN_HEIGHT / 2));

}

//Si on a warpé en haut

else if (map.getWarpDirection() == UP)

{

//On change la valeur en y du héros pour qu'il se

//trouve en bas de la map

y = map.getMaxY() - h - 1;

 

//On recentre la caméra

map.setStartY(map.getMaxY() - SCREEN_HEIGHT);

}

//Si on a warpé en bas

else if (map.getWarpDirection() == DOWN)

{

//On change la valeur en y du héros pour qu'il se

//trouve en haut de la map

y = 1;

 

//On recentre la caméra

map.setStartY(0);

}

//Si on a warpé à gauche

else if (map.getWarpDirection() == LEFT)

{

//On change la valeur en x du héros pour qu'il se

//trouve à droite de la map

x = map.getMaxX() - w - 1;

 

//On recentre la caméra

map.setStartX(map.getMaxX() - SCREEN_WIDTH);

}

//Si on a warpé à droite

else if (map.getWarpDirection() == RIGHT)

{

//On change la valeur en x du héros pour qu'il se

//trouve à gauche de la map

x = 1;

 

//On recentre la caméra

map.setStartX(0);

}

 

//Si on a utilisé une warp spéciale

else if (map.getWarpDirection() == 4)

{

POINT point;

 

//On détecte la warp spéciale utilisée (1, 2, etc)

//et on enregistre ses coordonnées dans point

point.x = map.detectWarpSpe(numberSPE + SPE1).x;

point.y = map.detectWarpSpe(numberSPE + SPE1).y;

 

//On place le héros près de cette warp (sans la toucher)

//suivant la direction qu'il avait en arrivant :

//Ainsi s'il allait vers le haut, on va le faire réapparaître

//au-dessus de la warp, et ainsi de suite...

if (direction == UP)

{

x = point.x + 6;

y = point.y - h - 6;

}

else if (direction == DOWN)

{

x = point.x + 6;

y = point.y + TILE_SIZE + 6;

}

else if (direction == RIGHT)

{

x = point.x + TILE_SIZE + 6;

y = point.y + 6;

}

else if (direction == LEFT)

{

x = point.x - w - 6;

y = point.y + 6;

}

else

{

x = map.getBeginX();

y = map.getBeginY();

}

 

//On recentre la caméra

map.setStartX(x - (SCREEN_WIDTH / 2));

map.setStartY(y - (SCREEN_HEIGHT / 2));

}

 

//On réinitialise le nombre de monstres

map.setNombreMonstres(0);

 

} 

  

 

     Branchements dans la boucle principale du jeu   

 

   Commençons d'abord par l'en-tête main.h : On y rajoute la constante MONSTRES_MAX ainsi que l'include de monster.h.
   Enfin, on met à jour les prototypes de draw() et update() :
 

Fichier : main.h : Mettre à jour :

//Legends of Meruvia - C++ / SFML 2

//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger

 

#include <cstdlib>

#include <iostream>

#include <SFML/Graphics.hpp>

 

#include "input.h"

#include "map.h"

#include "player.h"

#include "monster.h"

 

using namespace std;

using namespace sf;

 

 

//Fonctions

void update(Input &input, Map &map, Player &player, Monster monster[]);

void draw(sf::RenderWindow &window, Map &map, Player &player, Monster monster[]);

 

 

// Taille de la fenêtre : 800x480 pixels

const int SCREEN_WIDTH = 800;

const int SCREEN_HEIGHT = 480;

 

//Nombre max de monstres à l'écran

const int MONSTRES_MAX = 50;

 

   Et enfin, voilà le code mis à jour de notre fichier main.cpp :

 

Fichier : main.cpp : Mettre à jour :

//Legends of Meruvia - C++ / SFML 2.3.2

//Copyright / Droits d'auteur : www.meruvia.fr - Jérémie F. Bellanger

 

#include "main.h"

 

 

int main(int argc, char *argv[])

{

// Création d'une fenêtre en SFML

RenderWindow window(VideoMode(SCREEN_WIDTH, SCREEN_HEIGHT, 32),

"Meruvia - Big Tuto A-RPG/SFML2 - Chapitre 11 - www.meruvia.fr");

 

 

//On active la synchro verticale

window.setVerticalSyncEnabled(true);

 

//Instanciation des classes

Input input;

Map map;

Player player;

//On instancie autant de classes que de monstres gérables

Monster monster[MONSTRES_MAX];

 

//On commence au premier niveau

map.setLevel(1);

map.changeLevel();

 

//On initialise le player

player.initialize(map);

player.setGold(100);

 

// Boucle infinie, principale, du jeu

while (window.isOpen())

{

 

// Gestion des inputs

input.gestionInputs(window);

 

//Updates

update(input, map, player, monster);

 

// Dessin - draw

draw(window, map, player, monster);

 

window.display();

}

 

// On quitte

return 0;

 

}

 

 

 

//Fonction de mise à jour du jeu : gère la logique du jeu

void update(Input &input, Map &map, Player &player, Monster monster[])

{

//On met à jour le player

player.update(input, map);

 

//On met à jour les monstres un par un

for (int i = 0; i < map.getNombreMonstres(); i++)

{

if (monster[i].update(i, map, player, monster) == 2)

{

//Si l'update du monstre renvoie 2, c'est qu'il doit mourir :

//on copie à sa place le dernier monstre avant de retirer un monstre

monster[i].copy(monster[map.getNombreMonstres() - 1]);

map.setNombreMonstres(map.getNombreMonstres() - 1);

}

 

}

}

 

 

 

//Fonction de dessin du jeu : dessine tous les éléments

void draw(RenderWindow &window, Map &map, Player &player, Monster monster[])

{

//On efface tout

window.clear();

 

// Affiche la map de tiles : layer 2 (couche du fond)

map.draw(2, window, monster);

 

// Affiche la map de tiles : layer 1 (couche active : sol, etc.)

map.draw(1, window, monster);

 

// Affiche le joueur

player.draw(map, window);

 

//On affiche les monstres un par un

for (int i = 0; i < map.getNombreMonstres(); i++)

{

monster[i].draw(map, window);

}

 

// Affiche la map de tiles : layer 3 (couche en foreground / devant)

map.draw(3, window, monster);

 

}

 

   On y instancie d'abord, toutes nos classes Monster Monster monster[MONSTRES_MAX];

   Il s'agit là de notre tableau de sprites, représentés sous la forme d'un tableau d'objets de type Monster. Il ne faut pas que la notion de tableau de monstres vous effraie. En fait, on crée juste 50 objets Monster qu'on met dans un tableau pour pouvoir les appeler ainsi : monster[1], monster[2], etc... Pratique, non ? wink

   Dans la boucle principale, maintenant, on met à jour les appels vers draw() et update().

 

   Enfin, on adapte nos fonctions draw() et update() pour qu'elles dessinent et gèrent nos monstres.

   Notez que dans la fonction update() si l'update() des monstres renvoie 2, c'est que le monstre en question doit mourir. On appelle alors la fonction de copie (copy) pour copier le dernier monstre du tableau à sa place, et on raccourcit notre tableau en décrémentant la variable nombreMonstres (c'est le plus simple wink).

 

  Voilà, plus qu'à compiler et à lancer le programme ! wink   

   De beaux globulos jaunes et bleus se baladent maintenant gentiment sur la map ! indecision

   Ne sont-ils pas mignons !?! angel

 

 

   @ bientôt pour la suite ! angel

                                                      Jay.

 

 

 
 

This site uses cookies to enable you to log in. We do not store or sell any personal data. By continuing to use this website, you agree to their use. Thanks!