Slick2d, leçon 20 :: Les combats aux tour par tour (5/6)

17 May 2015 08:58 Slick2d - Leçons 20-29 Java, Jeux, Slick2d, Tutorial

Aujourd'hui on va voir comment ajouter un hud à notre combat par l'intermédiaire de bouton cliquable et une fenêtre de texte qui contiendra les logs du combat.

Préparation :: un bouton.

Comme pour la leçon 13 j'ai utilisé la ressource Golden UI trouvée OpenGameArt pour faire un bouton que voici :

bouton

Code :: BattleHud

Comme pour la leçon 13 créons une classe dédiée à l'affichage du hud BattleHud.

// Classe : BattleHud
public class BattleHud { 

  public void init(GameContainer container) throws SlickException { }
  
  public void render(GameContainer container, Graphics g) { }

}

Code :: Ajouter des boutons

Un bouton dans Slick c'est une zone clickable avec une image à l’intérieur, ces zones sont représentés avec la classe MouseOverArea. On va utiliser le constructeur avec les arguments :

  • GameContainer : container du jeux
  • Image : image du bouton
  • int x, int y : coin supérieur gauche
  • ComponentListener : écouteur pour les évènements

Construisons nos boutons,  dans la méthode init() et comme d'habitude affichons les dans la méthode render(). Il y a 3 boutons qui correspondent au 3 actions possible dans notre combat.

// Classe : BattleHud
// espace entre les éléments de l'interface
private static final int SPACE = 5;
// pour le centrage du text dans les boutons
private static final int Y_PADDING = 3; 
private static final int X_PADDING = 13;

private MouseOverArea attackButton;
private MouseOverArea fleeButton;
private MouseOverArea defendButton;

public void init(GameContainer container) throws SlickException {
  Image buttonImage = new Image("hud/button.png");
  attackButton = new MouseOverArea(container, buttonImage, SPACE, container.getHeight() - (buttonImage.getHeight() + SPACE) * 3, this);
  defendButton = new MouseOverArea(container, buttonImage, SPACE, container.getHeight() - (buttonImage.getHeight() + SPACE) * 2, this);
  fleeButton = new MouseOverArea(container, buttonImage, SPACE, container.getHeight() - (buttonImage.getHeight() + SPACE) * 1, this);
}

public void render(GameContainer container, Graphics g) { 
  g.setColor(Color.black);
  attackButton.render(container, g);
  g.drawString("Attaquer", attackButton.getX() + X_PADDING, attackButton.getY() + Y_PADDING);
  defendButton.render(container, g);
  g.drawString("Defendre", defendButton.getX() + X_PADDING, defendButton.getY() + Y_PADDING);
  fleeButton.render(container, g);
  g.drawString("Fuire", fleeButton.getX() + X_PADDING, fleeButton.getY() + Y_PADDING);
}

Les calculs sont fait pour que les trois boutons soit placés les uns en dessous des autres dans le coin inférieur gauche.

Code :: Cliquer sur les boutons

Pour que notre Hud puisse capter les événements de notre souris il doit implémenter l'interface ComponentListener.

// Classe : BattleHud
public class BattleHud implements ComponentListener {
  // [...]
  @Override
  public void componentActivated(AbstractComponent source) {
  }
}

La méthode reçois en argument la source, c'est à dire le bouton, qui est activé quand on clic dessus. Il faut encore compléter cette méthode en appelant notre BattleController pour activer les actions de combat qui correspondent à chaque bouton.

// Classe : BattleHud
// [...]
private BattleController controller;

// . il faut avoir une référence sur le contrôleur
public BattleHud(BattleController controller) {
  this.controller = controller;
}
// [...]

@Override
public void componentActivated(AbstractComponent source) {
  if (source == attackButton) {
    controller.controlPressed(BattleCommand.ATTACK);
  } else if (source == defendButton) {
    controller.controlPressed(BattleCommand.DEFEND);
  } else if (source == fleeButton) {
    controller.controlPressed(BattleCommand.FLEE);
  }
}
// [...]

Il faut intégrer tous cela dans notre la boucle de combat, cela commence à devenir routinier.

// Classe : BattleGameState
// [...]
private BattleHud hud;

@Override
public void init(GameContainer container, StateBasedGame game) throws SlickException {
  // initialisation, vu dans les leçons précédentes
  BattleController controller = new BattleController(player, ennemy, game);
  provider.addListener(controller);
  this.hud = new BattleHud(controller);
  this.hud.init(container);
}

@Override
public void render(GameContainer container, StateBasedGame game, Graphics g) throws SlickException {
  // affichage, vu dans les leçons précédentes
  this.hud.render(container, g);
}
// [...]

Et voici le résultat :

Des bouttons dans le combat

Un log de combat

Je vous propose ensuite d'ajouter un log de combat, c'est à dire une fenêtre de texte ou est retranscrit par écrit tous les événements de combat.

Code :: TextArea

Slick ne propose pas solion clef en main pour écrire du texte en multi-ligne. Je vous passe les détails, je l'ai déjà fais pour vous. Il vous faut importer SlickUtils dans sa version 1.3. Ajoutons notre log à l'initialisation et au rendu :

// Classe : BattleHud
// [...]
private TextArea log;

public void init(GameContainer container) throws SlickException {
  log = new TextArea(SPACE + attackButton.getWidth() + SPACE, attackButton.getY(), container.getWidth() - attackButton.getWidth() - SPACE * 3, buttonImage.getHeight() * 3 + SPACE * 2);
  // affichage de bas en haut
  log.setBottomUp(true); 
  log.setDefaultFont(container.getDefaultFont());
}
// [...]

public void render(GameContainer container, Graphics g) { 
  // [...]
  g.drawString("Fuire", fleeButton.getX() + X_PADDING, fleeButton.getY() + Y_PADDING);
  log.render();
}
// [...]

public void addLog(String text) {
  log<.addFirstText(text, VerticalAlignement.LEFT);
}
// [...]

Maintenant il faut passer notre instance du hud au contrôleur pour que celui-ci puisse écrire dans le log.

Classe :: BattleHud
  // [...]
  public BattleHud(BattleController controller) {
    this.controller = controller;
    this.controller.setHud(this);
  }
  // [...]

Et dans le contrôleur :

// Classe :: BattleController
public class BattleController implements InputProviderListener {
  // [...]
  private BattleHud hud;

  // [...]

  public void setHud(BattleHud hud) {
    this.hud = hud;
  }
  // [...]

Pour finir il reste à appeler la méthode addLog de notre hud depuis la classe BattleController, pour afficher les messages qu'on souhaite lors des différentes étape du combat. C'est trivial aussi je passe cette partie sous silence. Mais voici le résultat :

Pour aller plus loin

Le combat avance bien, la prochaine fois on verra comment changer la musique et comment ajouter une pause a la fin du combat avant de retourner à la carte.

Ressources

par Shionn, dernière modification le 05 April 2017 20:57
39 réflexions au sujet de « Slick2d, leçon 20 :: Les combats aux tour par tour (5/6) »
  • moonlight 13 June 2015 14:31

    Super tes tuto comme d'habitude ;) , à quand la sauvegarde ? :P

    J'ai commencé la sauvegarde sous forme de BD hors ligne avec jdbc lite , penses tu que c'est une bonne idée ?

    Il y a t-il d'autres moyens ?

  • Shionn 15 June 2015 07:05

    La sauvegarde viendra, mais de la manière dont je souhait le faire elle demande un peu de réorganisation de code. Il existe plein de manière de faire une sauvegarde. La plus évidente est de faire une sauvegarde du joueur ou de l’état du jeu dans un fichier en utilisant la sérialisation java.

  • moonlight 17 June 2015 15:30

    Aurais tu un site à me conseiller pour apprendre et voir comment fonctionne la sérialisation ? :)

  • moonlight 07 October 2015 11:24

    Tu fais de très bon tuto !

    Penses tu continuer ?

    Tu n'as pas mis le lien de la leçon 20 sur l'accueil on doit y accéder en modifiant l'url, ce qui ne facilite pas son accès :)

  • Shionn 14 October 2015 14:11

    Oui je pense continuer mais j'ai beaucoup d'autre projet en ce moment qui ne me permette pas d'avancer sur les tutos. J’espérai pouvoir reprendre avec la rentrée mais je me rend compte que ce n'est pas possible. Actuellement je travail sur la bdd de magic qui reçoit une grosse mise à jour technique.

    Merci je vais le rajouter prochainement.

  • Le Hegaret 17 October 2015 00:21

    Coucou Shionn ! la suite ! la suite ! moi j'ai un souci d'algo pour ramasser un objet ou alors voir l'ennemi sur la map ! faut le dire les choses carrément j'ai un mal de chien à utiliser la classe TiledMap du genre les méthodes map.getLayerIndex(), map.getLayerProperty() et surtout comment les manipuler entre elles !

    Merci d'avance :D

  • Shionn 18 October 2015 08:14

    Salut.

    En fait ce sont des méthode pour la lecture pas vraiment pour la modification. Mais normalement avec l'article sur la collision tu devrais tous comprendre sur ces méthodes. Si tu veux faire tomber des objets (genre une pomme) au sol, il faut utilisé ta propre liste avec les objets (java) représentant ces objets (genre la pomme) et tu les affichent / gerent comme le personnage.

  • Le Hegaret 18 October 2015 22:02

    je vais aller voir ca :) parce que là j'essaye désespérément de mettre un objet sur tiled map editor genre une boite sauf que je sais pas est-ce que j'essaye de le mettre dans un calque object ou calque tile puis de le lire par les propriété tout en essayant de mettre un log en console pour implementer un truc qui ressemble à de la reflexion (Class.forName(name) ) ! bref ce vaseux et laborieux

  • Le Hegaret 19 October 2015 03:48

    voilà mon inspiration : https://github.com/Actimia/TDF-Platforms/blob/master/TDF%20Platforms/src/org/dyndns/tdfpro/platforms/Map.java

    voilà mon code :

    import org.newdawn.slick.AppGameContainer;
    import org.newdawn.slick.BasicGame;
    import org.newdawn.slick.GameContainer;
    import org.newdawn.slick.Graphics;
    import org.newdawn.slick.Input;
    import org.newdawn.slick.SlickException;
    import org.newdawn.slick.tiled.TiledMap;
    
    
    public class ReflexionMapEditor extends BasicGame{
    	TiledMap aMap;
    	private GameContainer container;
    	public ReflexionMapEditor(String title) {
    		super(title);
    		// TODO Auto-generated constructor stub
    	}
    	public static void main(String[] args) throws SlickException {
    		new AppGameContainer(new ReflexionMapEditor("Reflexion Tiled Map Editor"), 640, 480, false).start();
    	}
    	@Override
    	public void render(GameContainer arg0, Graphics arg1) throws SlickException {
    		this.aMap.render(0, 0);
    		
    	}
    	@Override
    	public void init(GameContainer container) throws SlickException {
    		this.container = container;
    			aMap = new TiledMap("resources/map/exemple.tmx");
    			int width = aMap.getWidth();
    	        int height = aMap.getHeight();
    			 int layer = aMap.getLayerIndex("cime");
    
    			String propr=aMap.getLayerProperty(layer, "cime", "");
    		       for (int x = 0; x < width; x++) {
    		            for (int y = 0; y < height; y++) {
    		                int tileID = aMap.getTileId(x, y, layer);
    		                System.out.println(tileID+" "+x+" "+y+" "+layer+" "+width+" "+height+" "+propr);
    		            }}
    			/*for (int gi = 0; gi < aMap.getObjectGroupCount(); gi++) {
    
    				 for (int oi = 0; oi < aMap.getObjectCount(gi); oi++) {
    
    		                
    			        System.out.println("Object Type : "+ aMap.getObjectType(gi, oi) );
    			        System.out.println("Object Name : "+ aMap.getObjectName(gi, oi) );
    			        System.out.println("Object Property: "+ aMap.getObjectProperty(gi, oi, "coffre", "" ) );}}
    			    */
    	}
    	@Override
    	public void update(GameContainer arg0, int arg1) throws SlickException {
    		// TODO Auto-generated method stub
    		
    	}
    	@Override
    	public void keyReleased(int key, char c) {
    		if (Input.KEY_ESCAPE == key) {
    			this.container.exit();
    		}
    	}
    }
    

    et voilà ma console et je sais pas pourquoi ! le code qui plante se trouve dans le commentaire !

    Mon Oct 19 04:46:49 CEST 2015 INFO:Found 0 controllers
    Mon Oct 19 04:46:49 CEST 2015 ERROR:For input string: ""
    java.lang.NumberFormatException: For input string: ""
    	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    	at java.lang.Integer.parseInt(Integer.java:504)
    	at java.lang.Integer.parseInt(Integer.java:527)
    	at org.newdawn.slick.tiled.TiledMap$ObjectGroup.(TiledMap.java:1008)
    	at org.newdawn.slick.tiled.TiledMap.load(TiledMap.java:688)
    	at org.newdawn.slick.tiled.TiledMap.(TiledMap.java:106)
    	at org.newdawn.slick.tiled.TiledMap.(TiledMap.java:90)
    	at ReflexionMapEditor.init(ReflexionMapEditor.java:28)
    	at org.newdawn.slick.AppGameContainer.setup(AppGameContainer.java:393)
    	at org.newdawn.slick.AppGameContainer.start(AppGameContainer.java:317)
    	at ReflexionMapEditor.main(ReflexionMapEditor.java:18)
    Mon Oct 19 04:46:49 CEST 2015 ERROR:Failed to parse tilemap
    org.newdawn.slick.SlickException: Failed to parse tilemap
    	at org.newdawn.slick.tiled.TiledMap.load(TiledMap.java:695)
    	at org.newdawn.slick.tiled.TiledMap.(TiledMap.java:106)
    	at org.newdawn.slick.tiled.TiledMap.(TiledMap.java:90)
    	at ReflexionMapEditor.init(ReflexionMapEditor.java:28)
    	at org.newdawn.slick.AppGameContainer.setup(AppGameContainer.java:393)
    	at org.newdawn.slick.AppGameContainer.start(AppGameContainer.java:317)
    	at ReflexionMapEditor.main(ReflexionMapEditor.java:18)
    Caused by: java.lang.NumberFormatException: For input string: ""
    	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
    	at java.lang.Integer.parseInt(Integer.java:504)
    	at java.lang.Integer.parseInt(Integer.java:527)
    	at org.newdawn.slick.tiled.TiledMap$ObjectGroup.(TiledMap.java:1008)
    	at org.newdawn.slick.tiled.TiledMap.load(TiledMap.java:688)
    	... 6 more
    et voici le bout de code de tmx :
    
  • Shionn 25 October 2015 07:53

    Haaaa tu essayes d'utilisé les objets de Tiled ! Ouai en effet j'ai jamais essayé et je pense que cela marche pas toppissime.

    Y a beaucoup de soucis avec les cartes de Slick2d je pense la refaire (un jour si je peu).

    On ne peu pas mettre de xml dans mes commentaires wordpress enlève les caractères < et >

  • Clément 27 January 2016 16:00

    Bonjour, je suis totalement debutant avec slick et j'ai un peu de mal a faire certaines choses

    Ma question est un peu HS avec cette leçon mais plus en rapport avec le tuto sur les deplacements des personnage :

    Je souhaite deplacer mon personnage non pas avec les touches flechées mais automatiquement (avec une fonction moveTo(int x, int y) par exemple) cependant je n'arrive pas vraiment a implementer ce genre de fonction dans mon code...

  • Clément 28 January 2016 13:48

    C'est bon j'ai finalement réussi a le faire (sa m'apprendra a ne plus lire les tutos à moitié)...

  • Ultraime 13 March 2016 23:21

    Salut, sait tu comment laisser le jeu tourné quand on clique hors de la fenêtre ? Exemple : je lance un chrono, je quitte la fenêtre le chrono doit continuer de s'actualiser

    merci d'avance

  • Shionn 14 March 2016 18:51

    Techniquement le jeu ne s’arrête pas, l'affichage s’arrête mais la logique elle continue. Je ne sais pas comment tu as codé ton chrono mais si celui-ci dépend de l'affichage ce n'est pas bon. Car si la machine est trop puissante le chrono ira trop vite et inversement.

    Sinon pour répondre à ta question, sur la classe AppGameContainer il y a une méthode setAlwaysRender().

  • Ultraime 16 March 2016 19:16

    Merci beaucoup ! :),

    je n'ai pas fait de chrono c'était pour un exemple ^^ , le vrai problème est que j'ai commencer à ajouter du réseau au jeu, mais vue que je teste pour le moment sur une seul machine je ne pouvais pas voir l'actualisation

  • Shionn 17 March 2016 17:58

    J'avais fait du jeu en réseau pendant un temps avec RedDwarf. C'est bien cool. Je crois que le projet est mort malheureusement.

    Quel techno utilise tu pour le réseau ?

  • Ultraime 17 March 2016 18:49

    ah dommage :/. Pour le moment j'utilise de simple socket

  • Ultraime 19 March 2016 18:48

    J'ai encore besoin de ton aide ^^, peux tu m'expliquer comment fonctionne le textefield stp car je n'arrive absolument pas à écrire avec :/

  • Shionn 19 March 2016 20:03
  • UnPassant 20 March 2016 21:04

    Salut à toi ! J'aurais aimé savoir si tu penses faire la suite de tes tutos ?

    Cordialement, Un petit bonhomme passant par là.

  • Shionn 21 March 2016 13:00

    J'ai déjà répondu au moins 100 fois, oui, quand ? Je ne sais pas je n'ai pas de temps libre pour ça.

  • Ultraime 03 April 2016 10:14

    merci shionn pour le textfield

  • Saliwan 03 June 2016 22:47

    Salut,

    Je tiens à te remercier pour ce super tuto qui m'aide beaucoup :)

    J'ai tout de même une question ^^

    Dans le tutoriel la méthode componentActivated est appelée lors du clic sur l'un des boutons, cependant cette méthode ne permet que de détecter "l'activation" d'un bouton (qui ne correspond pour les MouseOverArea qu'au clic gauche si j'ai bien compris) . Cependant j'aimerais pouvoir détecter un clic droit sur un bouton ou même séparer la pression du relachement (pour mettre en place un système de drag and drop).

    La seule méthode que j'ai trouvée est de réimplémenter les méthodes telles que mouseClicked et d'y ajouter une condition afin de détecter si la souris est sur le bouton ou non. Je n'utilise donc plus la méthode componentActivated et donc m'éloigne de la fonction du MouseOverArea.

    J'aimerais savoir si cette façon de faire est correcte ou si il existe une autre méthode plus proche du tutoriel.

    Merci d'avance :)

  • Shionn 06 June 2016 08:01

    Bonjour Saliwan.

    Quand tu dis réimplémenter tu parles de surcharge par héritage de la classe MouseOverArea ? Je ne sais pas si c'est la meilleure manière de faire, mais c'est ce que je ferrai :

    Classe :: ExempleMouseOverArea
    public class ExempleMouseOverArea extends MouseOverArea {
      @Override
      void mouseDragged(int oldx, int oldy, int newx, int newy) {
      }
      @Override
      void mouseMoved(int oldx, int oldy, int newx, int newy) {
      }
      @Override
      void mousePressed(int button, int mx, int my) {
      }
      @Override
      void mouseReleased(int button, int mx, int my) {
      }
    }
    
  • Saliwan 06 June 2016 13:14

    Oui c'est ce que j'ai fait :)

    Le problème que j'avais était par rapport à la méthode componentActivated qui ne sert plus, mais qui doit être déclarée dans la classe implémentant le componentListener (nécessaire au constructeur de MouseOverArea).

    Ce n'est pas grave je vais laisser une méthode vide ^^

    Merci de ta réponse :)

  • Ultraime 03 July 2016 11:01

    Salut j'ai essayer de téléporter le personnage en cliquant sur la souris, mais je récupéré la position de l'écran et pas de la carte :/. Je ne sais pas comment convertir la position écran vers la position du monde. Pourrais tu me donner une piste s'il te plait ?

  • Shionn 05 July 2016 10:09

    Salut désoler d'avoir mis du temps pour te répondre. Dans la leçon 4 se trouve une partie de la réponse. On utilise une transformation pour décaler l'affichage par rapport à la caméra. Il faut faire la fonction inverse pour transformer les coordonnées de la souris en coordonnée de carte. Quelque chose comme : X = Xsouris - container.getWidth()/2 + Xcamera. Enfin c'est dans cet esprits la.

  • Ultraime 09 July 2016 10:40

    Merci, ça fonctionne super bien, mais je ne comprend pas tellement pourquoi :(

  • Shionn 10 July 2016 06:28

    C'est assez simple.

    La caméra ne modifie pas le monde c'est à dire ton joueur est dans des coordonnées bien en dehors de la taille de ton écran. L'astuce que j'ai utilisé c'est que la caméra "déplace" le monde pour que le joueur soit sous l'écran.

    La souris elle n'est pas impactée par cela.

  • max 24 July 2016 08:12

    Bonjour Shionn! D'abord, Merci beaucoup pour votre tutoriel très complet! Je suis en ce moment entrain de tester la bibliothèque Slick et je voulais essayé de refaire dans un but pédagogique une classe bouton en utilisant le pattern observer. Je crée donc un projet test avec une classe BasicGame (la même que dans la leçon 1). Je crée une classe bouton à laquelle j'implémente MouseListener. Pour test je mets des system.out.print() dans les méthodes implémentés. J'instancie mon bouton dans la classe BasicGame et je rajoute dans init() container.getInput().addMouseListener(monBouton); mais lorsque je lance le projet et que je clique dans la fenêtre rien ne s'affiche dans mon terminal. Ai je oublié quelque chose?

  • max 24 July 2016 08:41

    finalement j'ai reessayé ce matin et bingo! Il fallait juste changer la méthode implémenté isAcceptingInput() et la faire retourné true :)

  • Shionn 26 July 2016 09:25

    Bonjour max, ça fait plaisir que mes lecteurs trouvent parfois la solution avant que j'ai le temps de chercher :]

  • max 26 July 2016 22:08

    Bonjour! Encore trois petites questions! Y a-t-il un moyen d'éviter l'écran noir de la fenêtre pendant l’initialisation et d'y mettre à la place un écran de chargement? Et quel est la différence entre utiliser g.drawImage() et image.draw()? Et enfin, j'ai essayé d'utiliser la méthode public void texture(Shape shape,Image image) pour mapper une petite texture. La texture est bien splitter dans la zone mais un quadrillage noir se forme entre chaque "tile".Est-ce un bug de la librairie ou ai-je oublié quelque chose? Merci d'avance pour votre réponse!

  • Shionn 28 July 2016 11:20

    Y a-t-il un moyen d’éviter l’écran noir de la fenêtre pendant l’initialisation ?

    Oui il est possible de faire un systreme de chargement intelligent, mais cela demande beaucoup de programmation multithreadeée...

    Quel est la différence entre utiliser g.drawImage() et image.draw()?

    Hum faudrait faire quelque essai. Mais je suppose g.drawimage se fait dans le context g et donc est impacté les transformations. Alors image.draw() doit permettre de dessiner dans un autre buffer.

    Et enfin, j’ai essayé d’utiliser la méthode public void texture(Shape shape,Image image) pour mapper une petite texture. La texture est bien splitter dans la zone mais un quadrillage noir se forme entre chaque « tile ».Est-ce un bug de la librairie ou ai-je oublié quelque chose?

    Hum je n'ai fais joujou avec ces fonctions qu'une seul fois et je n'ai pas été confronté a ce problème.

  • max 28 July 2016 12:18

    ok, merci beaucoup pour toutes vos réponses!

  • Ultraime 17 August 2016 11:04

    Bonjour Shionn tu vas bien ? Je viens encore t'embêter :D.

    Je rencontre actuellement un soucis, quand je clique sur l 'icone "réduire" et qu'après je retourne sur la fenêtre du jeu plus rien ne s'affiche, aurais tu une idée de comment régler le problème ?

    Merci d'avance.

    Ps: j'ai des classes pour générer des PNJ et gérer leur déplacement, pas encore optimiser et pas encore terminer.. mais si certain souhaite une base..

  • Shionn 18 August 2016 13:05

    Salut Ultraine,

    Malheureusement je n'ai jamais été confronté à ce problème, mais je n'ai jamais utilisé que Linux.

  • Ultraime 09 February 2017 19:49

    Bonjour,

    J'ai créer un objet qui posséde des boutons.

    Dans le main principal du jeu j'affiche les boutons de l'objet, mais impossible de l'activer au clique.... pourtant j'ai bien donnée à l'objet le container. Aurais tu une idée ?

  • Shionn 12 February 2017 08:36

    Salut,

    Comme ca la je n'ai aucune piste. Un oublie de listener ? Tu peu m'envoyer ton code par mail ?

Laissez un commentaire

Vous pouvez utilisez du markdown pour la mise en forme

Votre adresse de messagerie ne sera pas publiée.

Temporairement, pour lutter contre les bots, il n'est pas permis de mettre http:// dans le commentaire.