Skip to content

Tutorial 3 – creating a new widget

ger-benjamin edited this page Oct 2, 2012 · 44 revisions
  1. La plupart des fichiers requis par le jeu (CSS, pages, variables, widget générales) seront chargés grâce, entre-autre, au fichier « Game Model ». Les widgets spécifiques au jeu ne seront envoyés dynamiquement au client uniquement qu’en spécifiant leur existence dans le fichier « wegas-loader.js ». Ce fichier se trouve dans les dossiers « Wegas/Web Pages/wegas-app/js ». Ce fichier contient une variable nommée « CONFIG » qui contient un objet avec une variable « groups ». Cette dernière contient la variable « module » qui contient tous les widgets qui peuvent être appelé. Pour ajouter un widget qui est propre à votre jeu, suivez l’exemple suivant (deux widgets ajoutés) :

/book CYOA/ 'wegas-book': { path: 'wegas-book/js/wegas-book-fight.js', requires:['wegas-book-fight', 'wegas-book-dice'], ix_provides: "Fight" },
'wegas-book-dice': { path: 'wegas-book/js/wegas-book-dice.js', ix_provides: "Dice" }, ```

  • La première ligne (‘wegas-book, wegas-book-dice’) est la référence du module contenant le widget à l’objet global « YUI ».
  • « Path » indique l’uri du module contenant le widget à ajouter.
  • « Requires » est un tableau de référence aux modules utilisé par ce widget.
  • « ix_provides » est le nom du widget requis.
  1. Un widget est un composant d’interface graphique. Il s’agit d’un objet YUI qui permet de découper sont interface en éléments réutilisable.

La documentation détaillé de YUI sur les widget se trouve à se trouve à cette adresse : http://yuilibrary.com/yui/docs/widget/

Pour mieux comprendre le fonctionnement des widgets, créons un dé (widget « dice ») pour un jeu nommé « book » Les widgets créés pour Wegas héritent du widget de base de YUI. Pour créer un widget, créez un nouveau fichier « wegas-book-dice.js » puis ajoutez votre module (qui contiendra votre widget) dans l’objet global « YUI » en écrivant cette ligne :

YUI.add( "wegas-book-dice", function ( Y ) {
};

La fonction « add » demande deux paramètres : le nom du module auquel on veut ajouter une fonction (votre widget) et cette fonction. Il est également possible, après la fonction, d’ajouter la version minimum de YUI pour utiliser ce widget ainsi qu’un tableau des éléments requis par le widget (inutile ici car déjà précisé dans le loader).

Dans la fonction, il généralement écrit en tout premier simplement « "use strict"; ». Cette ligne permet de préciser que chaque erreur doit être relevée par le navigateur. Ceci permet de corriger plus facilement les problèmes qui peuvent survenir lors de la création d’un widget.

Ensuite, certaine variable globale du module (ou de la fonction) sont précisé. En général, seul deux variables globales sont précisés : La variable « CONTENTBOX » qui permet de simplifié l’accès à l’élément du DOM qui entoure les widgets (la boundingbox deuxième élément DOM existant autour de chaque widget) peut aussi être précisé) et la variable Dice (dans notre cas) qui contiendra le widget du même nom. Avant de rentrer dans le cœur du widget, il est important de préciser le « namespace » du widget et donc l’endroit où cet élément sera accessible.

Après cela, il est possible de créer le widget en lui-même et assigné à notre variable « Dice ». Pour cela une méthode « Y.Base.create() » existe et doit être inscrite dans notre élément globale « Dice » Cette méthode prends en paramètre le nom de notre widget (identique au module puisque ce dernier ne contient qu’un widget), le widget parent (Y.Widget) ainsi qu’un tableau de widget dont les attributs (ATTRS) et méthodes seront automatiquement ajouté à celui-ci. Les deux derniers paramètres sont deux objets : les méthodes et les attributs propres à votre nouveau widget que nous détaillerons ci-après. Votre widget doit maintenant ressembler à ceci :

YUI.add( "wegas-book-dice", function ( Y ) {
  "use strict";

  var CONTENTBOX = "contentBox", Dice;

  Dice = Y.Base.create( "wegas-book-dice", Y.Widget, [ Y.WidgetChild, Y.Wegas.Widget, Y.Wegas.persistence.Editable ], {}, {} });

  Y.namespace( "Wegas" ).Dice = Dice;
});

Il est maintenant possible de créer le widget en lui-même.

L’objet contenant les « méthodes » du widget vous laisse le libre choix de ce que vous voulez créer. 5 méthode représentant le cycle de vie d’un widget et hérité de « Y.Widget » sont néanmoins souvent redéfinit. Il s’agit des fonctions « initializer », « renderUI », « bindUI », « syncUI » et « destroy ».

La fonction « init » est appelée automatiquement une seule fois au moment de la création du widget. Il fait office de constructeur du widget. Exemple d’appel : monDe = new Y.Wegas.Dice() ;

La fonction « renderUI » doit être appelée la première fois qu’il est demandé d’afficher le contenu du widget. Cette fonction demande en paramètre un sélecteur qui lui permet de savoir à quel endroit (nœud) du DOM s’afficher. Ce nœud du DOM sera la « Bounding Box » du widget. Exemple d’appel : monDe.render(‘.maclass’) ;

La fonction « bindUI » est appelées automatiquement la première fois que l’objet est affiché (fin de la méthode « renderUI »). Il contient toutes les méthodes permettant d’agir sur le widget (surveillance des actions sur les boutons de l’interface par exemple.)

La fonction « syncUI » n’est pas appelée automatiquement mais peut être appelée à chaque fois qu’un élément du widget doit être synchronisé (modification du contenu ou de l’affichage). Elle est souvent appelée après une action définie dans la fonction bindUI. Exemple d’appel : monDe.sync() ;

Finalement, la fonction « destroy » est appelée automatiquement au moment de la destruction du widget. Elle permet de détruire proprement les fonctions et sous-widgets liés à ce widget. Exemple d’appel : monDe.destroy() ;

A cela, il est bien sûre possible de créer les méthodes et variables propres à ce widget.

Le deuxième objet (de la méthode Y.Base.create()), les attributs est un liste de paramètres comme représenté ci-dessous :

ATTRS : {
  min: {
      type: "Integer",
      value: 1
  },
  max: {
      type: "Integer",
      value: 6                
  }
}

Ces attributs peuvent être précisés lors de la création du widget. Par exemple en faisant :

monDe = new Y.Wegas.Dice({
   min :1,
   max :10
});

C’est également ce qui se passe lorsque ces attributs sont précisés dans l’élément de tpe « Dice » dans le fichier « wegas-book-pages.json de ce jeu. Remarquez que ces attributs peuvent avoir des paramètres comme :

  • Un « type » pour n’accepter qu’un seul type de variable
  • Une « value » qui sera la valeur par défaut de l’attribut
  • Un « validator » qui est une fonction permettant de restreindre les valeurs possibles (par exemple « Y.Lang.isArray » ou une fonction de votre création).

Ci-dessous, un exemple complet du widget « dice », prêt à l’utilisation, est présenté. Ce widget est constitué d’un bouton (Y.Wegas.Button) (possédant un label et une info-bulle (tooltip) personnalisable) et d’éléments DOM permettant d’afficher la valeur résultante du jet de dé. Il est possible d’activer une animation affichants des valeurs d’aléatoire de dé. Lorsque le dé est jeté, le widget « lance » (fire) un évenement « diceRolling » et lorsque le résultat final affiché (animation finit si elle est activée) est finit, un événement « diceRolled » est lancé. Le bouton permettant de « lancer » le dé est inutilisable pendant que le résultat est en train d’être calculé.

YUI.add( "wegas-book-dice", function ( Y ) {
  "use strict";

  var CONTENTBOX = "contentBox", Dice;

  Dice = Y.Base.create( "wegas-book-dice", Y.Widget, [ Y.WidgetChild, Y.Wegas.Widget, Y.Wegas.persistence.Editable ], {
      
      result:0,
      handlers: new Array(),
      rollButton:null,
      isRolling:false,
      
      rollDice: function(when, iteration){
          var cb = this.get(CONTENTBOX), result,
          min = parseInt(this.get("min")), max = parseInt(this.get("max"));
          result = Math.ceil(Math.random()*(max-min+1))+min-1;
          cb.one(".result").setHTML();
          cb.one(".result").append("<p class='result-value-"+result+"'>"+result+"</p>");
          if(this.get("animated").indexOf("true")>-1 && iteration>0){
              Y.later(when, this, Y.bind(this.rollDice, this, when+Math.ceil(when/4), iteration-1));
          }
          else{
              this.result = result;
              this.fire("diceRolled");
              this.isRolling = false;
          }
      },
      
      initializer: function(){
          this.publish("diceRolling", {});
          this.publish("diceRolled", {});
          this.rollButton = new Y.Wegas.Button({
              label:this.get("label"),
              tooltip: this.get("tooltip")
          });
      },
      
      renderUI: function(){
          var cb = this.get(CONTENTBOX);
          cb.append("<div class='wegas-dice'></div>");
          cb.one(".wegas-dice").append("<div class='button'></div>");
          cb.one(".wegas-dice").append("<div class='result'></div>");
          this.rollButton.render(cb.one('.wegas-dice .button'));
      },
      
      bindUI: function(){
          var cb = this.get(CONTENTBOX);
          this.handlers.push(cb.one(".wegas-dice .button").delegate('click', function(){
              if(this.isRolling || this.rollButton.get("disabled")) return;
              this.isRolling = true;
              this.fire("diceRolling");
              this.rollDice(30, 12);
          }, "button", this));
      },
      
      destroy: function(){
          var i;
          for (i=0; i<this.handlers.length;i++) {
              this.handlers[i].detach();
          }
          this.rollButton.destroy();
      }  

  }, {
      ATTRS : {
          min: {
              type: "Integer",
              value: 1
          },
          max: {
              type: "Integer",
              value: 6                
          },
          animated: {
              type : "Boolean",
              value : "false"
          },
          label:{
              type : "String",
              value : "Lancer le dé"
          },
          tooltip:{
              type : "String",
              value : null
          }
      }
  });

  Y.namespace( "Wegas" ).Dice = Dice;
});
  1. Use the newly created widget in a other widget (follow the lifcycle of the variable 'dice') :
YUI.add( "wegas-book-fight", function ( Y ) {
  "use strict";
  
  var CONTENTBOX = "contentBox", Fight;
  
  Fight = Y.Base.create( "wegas-book-fight", Y.Widget, [ Y.WidgetChild, Y.Wegas.Widget, Y.Wegas.persistence.Editable ], {
      
      handlers: new Array(),
      dice:null,
      opponentStamina:0,
      opponentCombatSkill:0,
      success:null,
      failure:null,
      alternative:null,
      
      doFight: function(e){
          var combatSkill = Y.Wegas.VariableDescriptorFacade.rest.find("name", "combatSkill"),
          stamina = Y.Wegas.VariableDescriptorFacade.rest.find("name", "stamina").getInstance().get("value"),
          damageGiven, damageTaken, handicap, diceValue = e.target.result;
          handicap = combatSkill.getInstance().get("value")-this.opponentCombatSkill;
          if(handicap < -10) handicap = -10;
          if(handicap > 10) handicap = 10;
          switch(diceValue){
              case 1 :
                  damageGiven = 0;
                  damageTaken = ((handicap<0)?Math.abs(handicap):0)+2;
                  break;
              case 2 :
                  damageGiven = Math.floor(((handicap>0)?handicap:0)/5);
                  damageTaken = Math.ceil(((handicap<0)?Math.abs(handicap):0)/2)+1;
                  break;
              case 3 :
                  damageGiven = Math.floor(((handicap>0)?handicap:0)/4);
                  damageTaken = Math.ceil(((handicap<0)?Math.abs(handicap):0)/3)+1;
                  break;
              case 4 :
                  damageGiven = Math.ceil(((handicap>0)?handicap:0)/3)+1;
                  damageTaken = Math.floor(((handicap<0)?Math.abs(handicap):0)/4);
                  break;
              case 5 :
                  damageGiven = Math.ceil(((handicap>0)?handicap:0)/2)+1;
                  damageTaken = Math.floor(((handicap<0)?Math.abs(handicap):0)/5);
                  break;
              case 6 :
                  damageGiven = ((handicap>0)?handicap:0)+2;
                  damageTaken = 0;
                  break;
          }
          this.opponentStamina -= damageGiven;
          stamina -= damageTaken;
          if(this.opponentStamina<=0){
              this.opponentStamina = 0; 
              this.doBattleResult(true);
          } else if(stamina<=0){
              stamina=0;
              this.doBattleResult(false);
          }else{
              this.dice.rollButton.enable();
          }
          this.setStamina(stamina);
          this.syncUI();
      },
      
      setStamina:function(stamina){
          if(typeof stamina !== "number") return;
          Y.Wegas.VariableDescriptorFacade.rest.sendRequest({
              request: "/Script/Run/Player/" + Y.Wegas.app.get('currentPlayer'),
              headers:{
                  'Content-Type': 'application/json; charset=ISO-8859-1',
                  'Managed-Mode':'true'
              },
              cfg: {
                  method: "POST",
                  data: Y.JSON.stringify({
                      "@class": "Script",
                      "language": "JavaScript",
                      "content": "importPackage(com.wegas.core.script);\nstamina.value ="+stamina+";"
                  })
              }
          });
      },
      
      doBattleResult: function(success){
          var cb = this.get(CONTENTBOX);
          if(success){
              if(this.success)this.success.render(cb.one(".result"));
          } else{
              if(this.failure)this.failure.render(cb.one(".result"));
          }
      },
      
      displayOpponentState: function(cb){
          cb.one(".opponent .stamina .value").setHTML(this.opponentStamina);
          cb.one(".opponent .combatSkill .value").setHTML(this.opponentCombatSkill);
      },
      
      initializer: function(){
          this.dice = new Y.Wegas.Dice({
              label:"Combattre",
              animated:"true"
          });
          if(this.get("success")){
              this.success = new Y.Wegas.List({
                  "label":"success",
                  "cssClass":"success-list",
                  "children":this.get("success")
              });
          }
          if(this.get("failure")){
              this.failure = new Y.Wegas.List({
                  "label":"failure",
                  "cssClass":"failure-list",
                  "children":this.get("failure")
              });
          }
          if(this.get("alternative")){
              this.alternative = new Y.Wegas.List({
                  "label":"alternative",
                  "cssClass":"alternative-list",
                  "children":this.get("alternative")
              });
          }
          this.opponentStamina = this.get("stamina");
          this.opponentCombatSkill = this.get("combatSkill");
      },
      
      renderUI: function(){
          var cb = this.get(CONTENTBOX), opponement;
          opponement = Y.Node.create("<div class='opponent'></div>");
          opponement.append("<div class='name'></div>").append("<div class='stamina'></div>").append("<div class='combatSkill'></div>");
          opponement.one(".stamina").append("<div class='label'></div>").append("<div class='value'></div>");
          opponement.one(".combatSkill").append("<div class='label'></div>").append("<div class='value'></div>");
          opponement.one(".stamina .label").setHTML(this.get("staminaLabel"));
          opponement.one(".combatSkill .label").setHTML(this.get("combatSkillLabel"));
          opponement.one(".name").setHTML(this.get("name"));
          cb.append(opponement);
          cb.append("<div class='dice'></div>");
          cb.append("<div class='result'></div>");
          cb.append("<div class='alternative'></div>");
          this.dice.render(cb.one(".dice"));
          if(this.alternative)this.alternative.render(cb.one(".alternative"));
      },
      
      bindUI: function(){
          this.dice.after("diceRolling",function(){
              this.dice.rollButton.disable();
          }, this);
          this.dice.after("diceRolled", this.doFight, this);
      },
      
      syncUI: function(){
          var cb = this.get(CONTENTBOX);
          this.displayOpponentState(cb);
      },
      
      destroy: function(){
          var i;
          for (i=0; i<this.handlers.length;i++) {
              this.handlers[i].detach();
          }
          if(this.dice)this.dice.destroy();
          if(this.success)this.success.destroy();
          if(this.failure)this.failure.destroy();
          if(this.alternative)this.alternative.destroy();
      }  
  
  }, {
      ATTRS : {
          name:{
              type: "String",
              value: "unknown"
          },
          staminaLabel : {
              type: "String",
              value: "Stamina: "
          },
          stamina : {
              type: "Integer",
              value: 1
          },
          combatSkillLabel : {
              type: "String",
              value: "Combat skill: "
          },
          combatSkill : {
              type: "Integer",
              value: 1
          },
          success : {
              validator: Y.Lang.isArray
          },
          failure : {
              validator: Y.Lang.isArray
          },
          alternative : {
              validator: Y.Lang.isArray
          }
      }
  });
  
  Y.namespace( "Wegas" ).Fight = Fight;
});
  1. Add the newly created widget to the wegas-tuto/db/wegas-tuto-pages.json file (see 2nd pages (id 3), 2nd children). the "type" element fetch the widget, the others elements are the widget's ATTRS :
[{
      "id": 1,
      "label": "Main layout",
      "type": "List",
      "cssClass": "layout",
      "children": [{
              "type":"List",
              "direction": "horizontal",
              "cssClass":"menu",
              "children":[{
                      "type": "VariableDisplay",
                      "label": "Combat skills",
                      "view": "text",
                      "variable": "combatSkill",
                      "dataSource": "VariableDescriptor"
                  }, {
                      "type": "VariableDisplay",
                      "label": "Stamina",
                      "view": "text",
                      "variable": "stamina",
                      "dataSource": "VariableDescriptor"
                  }
              ]}, {
              "label":"pageLoader",
              "type": "PageLoader",
              "id": "maindisplayarea",
              "cssClass": "body",
              "pageId": 2
          }]
  },{
      "id":2,
      "label":"Page 1",
      "cssClass":"book-page",
      "type":"List",
      "children":[{
              "type": "Text",
              "content":"Bienvenue, ceci est la première page de ce tutoriel. La page (élément général existant sur chaque 'interface' du jeu) contient un titre ainsi que la valeur de vigueur (stamina) et de combat (combat skill) de notre héros. Cette page est constituée également d'une sous-page (interface changeante du jeu) contenant un widget 'list' qui contient un widget 'text' (ce texte) ainsi qu'un widget 'button' (le bouton ci-dessous). Cliquez sur ce dernier pour aller à la sous-page suivante."
          },{
              "type":"List",
              "label":"Choices",
              "cssClass":"book-choice",
              "children":[{
                      "type": "Button",
                      "cssClass": "book-bouton",
                      "tooltip":"Aller à la page 2",
                      "label":"Suivant",
                      "plugins": [{
                              "fn": "OpenPageAction",
                              "cfg": {
                                  "subpageId": 3,
                                  "targetPageLoaderId": "maindisplayarea"
                              }
                          }]
                  }]
          }
      ]
  },{
      "id":3,
      "label":"Page 2",
      "cssClass":"book-page",
      "type":"List",
      "children":[{
              "type": "Text",
              "content":"Cette deuxième sous-page contient un widget 'fight' créer exclusivement pour les scénarios de ce jeu. Cliquez sur combattre pour réduire la vigueur (stamina) de l'adversaire à 0"
          },{
              "type": "Fight",
              "name" : "Loup",
              "staminaLabel": "Vigueur : ",
              "stamina": 8,
              "combatSkillLabel" : "Talent de combat : ",
              "combatSkill" : 7,
              "success":[{
                      "type":"Text",
                      "content":"Victoire ! Vous pouvez passer à la page suivante."
                  },{
                      "type": "Button",
                      "cssClass": "book-bouton",
                      "tooltip":"Aller à la page 3",
                      "label":"Continuer",
                      "plugins": [{
                              "fn": "OpenPageAction",
                              "cfg": {
                                  "subpageId": 4,
                                  "targetPageLoaderId": "maindisplayarea"
                              }
                          }]
                  }],
              "failure":[{
                      "type":"Text",
                      "content":"Perdu ! Vous feriez mieux de fuire le temps de regagner de la vie."
                  },{
                      "type": "Button",
                      "cssClass": "book-bouton",
                      "tooltip":"Aller à la page 3",
                      "label":"Fuire !",
                      "plugins": [{
                              "fn": "ExecuteScriptAction",
                              "cfg": {
                                  "onClick": "importPackage(com.wegas.core.script);\nstamina.value += 10;"
                              }
                          },{
                              "fn": "OpenPageAction",
                              "cfg": {
                                  "subpageId": 4,
                                  "targetPageLoaderId": "maindisplayarea"
                              }
                          }]
                  }],
              "alternative":[{
                      "type":"Text",
                      "content":"Si ce combat vous semble trop dangereux, prenez donc la fuite."
                  },{
                      "type": "Button",
                      "cssClass": "book-bouton",
                      "tooltip":"Aller à la page 1",
                      "label":"Fuire !",
                      "plugins": [{
                              "fn": "OpenPageAction",
                              "cfg": {
                                  "subpageId": 2,
                                  "targetPageLoaderId": "maindisplayarea"
                              }
                          }]
                  }]
          }
      ]
  },{
      "id":4,
      "label":"Page 3",
      "cssClass":"book-page",
      "type":"List",
      "children":[{
              "type": "Text",
              "content":"Créez maintenant vos propres sous-pages et widgets afin de créer un nouveau jeu !"
          }
      ]
  }
]
  1. adjust your CSS file :
.body {
  margin:10px;
}
.menu {
  color: white;
  background: green;
}
.wegas-variabledisplay {
  margin:10px;
}

.body *{
  margin-top:10px;
  max-width:700px;
}

.book-titre{
  font-family: "helvetica";
  font-size:1.2em;
  margin:10px;
  text-decoration: underline;
}

.indicator-stamina{
  float:left;
  margin-left:10px;
}

.indicator-combatSkill{
  float:left;
  margin-left:10px;
}

.book-page{
  clear:both;
  padding-top: 10px;
}

.wegas-book-dice .result p{
  height:33px;
  width:33px;
  color:rgba(0,0,0,0);
  background-repeat: no-repeat;
  background-position: center;
  background-color:transparent;
}

.wegas-book-dice .result-value-1{
  background-image:url(/Wegas/wegas-book/images/de1.png);
  background-color: black;
}

.wegas-book-dice .result-value-2{
  background-image:url(/Wegas/wegas-book/images/de2.png);
}

.wegas-book-dice .result-value-3{
  background-image:url(/Wegas/wegas-book/images/de3.png);
}

.wegas-book-dice .result-value-4{
  background-image:url(/Wegas/wegas-book/images/de4.png);
}

.wegas-book-dice .result-value-5{
  background-image:url(/Wegas/wegas-book/images/de5.png);
}

.wegas-book-dice .result-value-6{
  background-image:url(/Wegas/wegas-book/images/de6.png);
}

.wegas-book-fight-content .opponent div{
  display:inline-block;
  margin-right:5px;
}

.wegas-book-fight-content .opponent .name{
  font-weight: bold;
  margin-right:10px;
}
  1. Reload page