Tutorial 3 – creating a new widget - Heigvd/Wegas GitHub Wiki

  1. Most of the files required by the game (CSS pages, variables, widget general) will be charged by, among other, the game model file. Widgets specific to the game will be dynamically sent to the client if they existances are specified in the file "Wegas-loader.js." This file is located in the folder "Wegas / Web Pages / Wegas-app / js". This file contains a variable named "CONFIG" which contains an object with a variable "groups". This one contains the variable "module" that contains all the widgets that can be called. To add a widget that is unique to your game, use the following example (two widgets added):

/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" }, ```

  • The first line (‘wegas-book, wegas-book-dice’) is a reference to the module (containing the widget) in the global obect YUI.
  • « Path » indicate the path (uri) of the module containing the widget to add.
  • « Requires » is an array of references of the modules used by this widget.
  • « ws_provides » is the name of the required widget.
  1. A widget is a GUI component. It is a YUI object that can cut an interface in reusable elements.

A Detailed documentation on YUI widgets is located at: http://yuilibrary.com/yui/docs/widget/

To better understand how the widgets works, we will create a dice (widget "dice") for a game called "book" Widgets created for Wegas inherit from the basic widget of YUI. To create a widget,create a new file "Wegas-book-dice.js" then add your module (containing your widget) in the global object "YUI" by writing this line:

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

The "add" function require two parameters: the name of the module you want to add a function and the function. It is also possible after the function, to add the minimum version of YUI to use in this widget and an array of items required by the widget (useless here beacause it's already specified in the loader).

In this function, it's usually written (in first) "" use strict ";". This line is used to specify that each error must be read by the browser. This allows you to more easily correct problems that may occur when you creating the widget.

Then some global variable of the module (or of the function) are specified. In general, only two global variables are specified: The variable "CONTENTBOX" which allows simplified access to the DOM element that surrounds the widgets (a second element existe around the widget: it's the bounding box) and the Dice variable (in our case) that will contain the widget of the same name. Before getting into the heart of the widget, it's important to specify the "namespace" of the widget and therefore where the item will be accessible.

After that, it is possible to create the widget itself and assigned it to our variable "Dice". For this, a method "Y.Base.create ()" exists and must be registered in our global element "Dice" This method take as parameter the name of our widget (same as the module's name because the module contains only one widget) the parent widget (Y.Widget), an array of extensions (widgets with attributes (ATTRS) and methods will be automatically added to it). The last two parameters are two objects: the methods and the attributes of your new widget. We detail this below.Your widget should now look like this:

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;
});

It is now possible to create the widget itself.

The object containing the "methods" widget lets you freely choose what you want to create. 5 methods representing the life cycle of the widget and inherited from "Y.Widget" can be redefined. These functions are "initializer", "renderUI", "bindUI", "syncUI" and "destructor".

The "init" function is automatically called once when the widget is created. It acts as constructor of the widget. Example of call: myDice = 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. Example of call: myDice.render(‘.myClass’) ;

The "bindUI" function is called automatically the first time the object is displayed (after the method "renderUI"). It contains all methods that act on the widget (monitoring of actions on buttons for example.)

The "syncUI" is not called automatically but can be called whenever a widget elements must be synchronized (modifcation of the contents or of the display). It is often called after an action defined in the function bindUI. Example of call: myDice.sync ()

Finally, the "destructor" function is called automatically at the destruction of the widget. It can destroy itself functions and sub-widgets associated with this widget. Example call: myDice.destroy ()

It is of course possible to create variables and methods specific to this widget.

The second object (of the method Y.Base.create ()) is a list of parameters (ATTRS) as shown below:

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

These attributes can be specified when creating the widget. For example by:

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

This is also what happens when these attributes are specified in the element type "Dice" in the file "Wegas-book-pages.json of this game Note that these attributes can have parameters (type, value, validator, getter and setter function, etc ...)

Below is a complete example of the widget "dice", ready for use. This widget consists of a button (Y.Wegas.Button) (with a customizable label and a tooltype) and DOM elements to display the resulting value of a dice roll. It is possible to activate an animation which display random values ofthe rolling dice. When the die is cast, the widget "throw" (fire) an event "diceRolling" and when the end result is displayed (animation ended if it is enabled), an event "diceRolled" is launched. Button allow to "launch" the die is unusable while the result is being calculated.

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));
      },
      
      destructor: 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);
      },
      
      destructor: 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": {
                          "name": "combatSkill"
                      },
                      "dataSource": "VariableDescriptor"
                  }, {
                      "type": "VariableDisplay",
                      "label": "Stamina",
                      "view": "text",
                      "variable": {
                          "name": "stamina"
                      },
                      "dataSource": "VariableDescriptor"
                  }
              ]}, {
              "label":"pageLoader",
              "type": "PageLoader",
              "pageLoaderId": "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 and add dice's images in your 'image' folder :
.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
⚠️ **GitHub.com Fallback** ⚠️