This is a documentation for Board Game Arena: play board games online !
Game interface logic: yourgamename.js: Difference between revisions
m (→this.ajaxcall(url, parameters, obj_callback, callback, callback_error, ajax_method): mention of breaking replays/tutorials) |
|||
Line 580: | Line 580: | ||
==== this.ajaxcall(url, parameters, obj_callback, callback, callback_error, ajax_method) ==== | ==== this.ajaxcall(url, parameters, obj_callback, callback, callback_error, ajax_method) ==== | ||
This method must be used to send a player input to the game server. '''It should not be triggered programmatically''', especially not in loops, in callbacks, in notifications, or in onEnteringState/onUpdateActionButtons/onLeavingState, in order not to create race conditions or break replay game and tutorial features. | This method must be used to send a player input to the game server. '''It should not be triggered programmatically''', especially not in loops, in callbacks, in notifications, or in onEnteringState/onUpdateActionButtons/onLeavingState, in order not to create race conditions or break replay game and tutorial features. It should be used only in reaction to a user action in the interface. | ||
* url: the url of the action to perform. For a game, it must be: "/<mygame>/<mygame>/myAction.html" | * url: the url of the action to perform. For a game, it must be: "/<mygame>/<mygame>/myAction.html" |
Revision as of 13:55, 9 June 2022
This is the main file for your game interface. Here you will define:
- Which actions on the page will generate calls to the server.
- What happens when you get a notification for a change from the server and how it will show in the browser.
File structure
The details of how the file is structured are described below with comments on the code skeleton provided to you.
Here is the basic structure:
- constructor: here you can define global variables for your whole interface.
- setup: this method is called when the page is refreshed, and sets up the game interface.
- onEnteringState: this method is called when entering a new game state. You can use it to customize the view for each game state.
- onLeavingState: this method is called when leaving a game state.
- onUpdateActionButtons: called when entering a new state, in order to add action buttons to the status bar.
- (utility methods): this is where you can define your utility methods.
- (player's actions): this is where you can write your handlers for player actions on the interface (example: click on an item).
- setupNotifications: this method associates notifications with notification handlers. For each game notification, you can trigger a javascript method to handle it and update the game interface.
- (notification handlers): this is where you define the notifications handlers associated with notifications in setupNotifications, above.
More details:
- setup(gamedatas)
This method must set up the game user interface according to current game situation specified in parameters. The method is called each time the game interface is displayed to a player, ie:
- when the game starts
- when a player refreshes the game page (F5)
"gamedatas" argument contains all datas retrieved by your "getAllDatas" PHP method.
- onEnteringState(stateName, args)
This method is called each time we are entering into a new game state. You can use this method to perform some user interface changes at this moment. To access state arguments passed via calling arg* method use args.args. Typically you would do something only for active player, using this.isCurrentPlayerActive() check.
Warning: for multipleactiveplayer states: the active players are NOT active yet so you must use onUpdateActionButtons to perform the client side operation which depends on a player active/unactive status. If you are doing initialization of some structure which do not depend on active player, you can just replace (this.isCurrentPlayerActive()) with (!this.isSpectator) for the main switch in that method.
See more details in Your_game_state_machine:_states.inc.php#Diffrence_between_Single_active_and_Multi_active_states
- onLeavingState(stateName)
This method is called each time we are leaving a game state. You can use this method to perform some user interface changes at this moment (i.e. cleanup).
- onUpdateActionButtons(stateName, args)
In this method you can manage "action buttons" that are displayed in the action status bar and highlight active UI elements. To access state arguments passed via calling arg* method use args parameter. Note: args can be null! For game states and when you don't supply state args function - it is null. This method is called when active or multiactive player changes. In classic "activePlayer" state this method is called before the onEnteringState state. In multipleactiveplayer state it is a mess. The sequencing of calls would depends on either you get into that state from transitions OR from reloading the whole game (i.e. F5).
See more details in Your_game_state_machine:_states.inc.php#Difference_between_Single_active_and_Multi_active_states
General tips
- this.player_id
- ID of the player on whose browser the code is running.
- this.isSpectator
- Flag set to true if the user at the table is a spectator (not a player).
- Note: This is a variable, not a function.
- Note: If you want to hide an element from spectators, you should use CSS 'spectatorMode' class.
- this.gamedatas
- Contains the initial set of data to init the game, created at game start or by game refresh (F5).
- You can update it as needed to keep an up-to-date reference of the game on the client side if you need it. (Most of the time this is unnecessary).
- Note: In hotseat mode, the framework does not keep this.gamedatas of hotseat players and shares the same set as the table administrator to store data.
- Updating and using it after initialization might break some features relying on it.
- this.isCurrentPlayerActive()
- Returns true if the player on whose browser the code is running is currently active (it's his turn to play). Note: see remarks above about usage of this function inside onEnteringState method.
- this.getActivePlayerId()
- Return the ID of the active player, or null if we are not in an "activeplayer" type state.
- this.getActivePlayers()
- Return an array with the IDs of players who are currently active (or an empty array if there are none).
- this.bRealtime
- Return true if the game is in realtime. Note that having a distinct behavior in realtime and turn-based should be exceptional.
- g_replayFrom
- Global contains reply number in live game, it is set to undefined (i.e. not set) when it is not a reply mode, so consequentially the good check is g_replayFrom !== undefined which returns true if the game is in replay mode during the game (the game is ongoing but the user clicked "reply from this move" in the log)
- g_archive_mode
- Returns true if the game is in archive mode after the game (the game has ended)
- this.instantaneousMode
- Returns true during replay/archive mode if animations should be skipped. Only needed if you are doing custom animations. (The BGA-provided animation functions like this.slideToObject() automatically handle instantaneous mode.)
- Technically, when you click "replay from move #20", the system replays the game from the very beginning with moves 0 - 19 happening in instantaneous mode and moves 20+ happening in normal mode.
- g_tutorialwritten
- Returns an object like the below if the game is in tutorial mode, or undefined otherwise. Tutorial mode is a special case of archive mode where comments have been added to a previous game to teach new players the rules.
{ author: "91577332", id: "576", mode: "view" status: "alpha" version_override: null viewer_id: "84554161" }
You may consider making a function like this, to detect if the game is in a read-only state:
// Returns true for spectators, instant replay (during game), archive mode (after game end) isReadOnly: function () { return this.isSpectator || typeof g_replayFrom != 'undefined' || g_archive_mode; }
Dojo framework
BGA uses the Dojo Javascript framework.
The Dojo framework allows us to do complex things more easily. The BGA framework uses Dojo extensively.
To implement a game, you only need to use a few parts of the Dojo framework. All the Dojo methods you need are described on this page.
Javascript minimization (after July 2020)
For performance reasons, when deploying a game the javascript code is minimized using terser (https://github.com/terser/terser). This minifier works with modern javascript syntax. From your project "Manage game" page, you can now test a minified version of your javascript on the studio (and revert to the original).
NB: it has been reported that there is an issue with this minifier and percentage values for opacity.
Accessing and manipulating the DOM
$('some_html_element_id')
The $() function is used to get an HTML element using its "id" attribute.
Example 1: modify the content of a "span" element:
In your HTML code: <span id="a_value_in_the_game_interface">1234</span> In your Javascript code: $('a_value_in_the_game_interface').innerHTML = "9999";
Note: $() is the standard method to access some HTML element with the BGA Framework. You can use getElementById but a longer to type and less handy as it does not do some checks. Note²: It is safe to use if you don't know if variable is string (id of element) or element itself, i.e.
foo: function(card) { card = $(card); // now its node, no need to write if (typeof card === 'string') ... // but its good idea to check for null here ... }
dojo.style
With dojo.style you can modify the CSS property of any HTML element in your interface.
Examples:
// Make an element disappear dojo.style( 'my_element', 'display', 'none' ); // Give an element a 2px border dojo.style( 'my_element', 'borderWidth', '2px' ); // Change the background position of an element // (very practical when you are using CSS sprites to transform an element to another) dojo.style( 'my_element', 'backgroundPosition', '-20px -50px' );
Note: you must always use dojo.style to modify the CSS properties of HTML elements.
Note²: if you have to modify several CSS properties of an element, or if you have a complex CSS transformation to do, you should consider using dojo.addClass/dojo.removeClass (see below).
Vanila JS style
$('my_element').style.display='none'; // set var display = $('my_element').style.display; // get $('my_element').style.removeProperty('display'); // remove
dojo.addClass, dojo.removeClass, dojo.hasClass, dojo.toggleClass
In many situations, many small CSS property updates can be replaced by a CSS class change (i.e., you add a CSS class to your element instead of applying all modifications manually).
Advantages are:
- All your CSS stuff remains in your CSS file.
- You can add/remove a list of CSS modifications with a simple function and without error.
- You can test whether you applied the CSS to an element with the dojo.hasClass method.
Example from Reversi:
// We add "possibleMove" to an element dojo.addClass( 'square_'+x+'_'+y, 'possibleMove' ); // In our CSS file, the class is defined as: .possibleMove { background-color: white; opacity: 0.2; filter:alpha(opacity=20); /* For IE8 and earlier */ cursor: pointer; } // So we've applied 4 CSS property changes in one line of code. // ... and when we need to check if a square is a possible move on the client side: if( dojo.hasClass( 'square_'+x+'_'+y, 'possibleMove' ) ) { ... } // ... and if we want to remove all possible moves in one line of code (see "dojo.query" method): dojo.query( '.possibleMove' ).removeClass( 'possibleMove' );
Conclusion: We encourage you to use dojo.addClass, dojo.removeClass and dojo.hasClass to make your life easier :)
dojo.attr
With dojo.attr you can access or change the value of an attribute or property of any HTML element in your interface.
Exemple:
// Get the title of a node var title = dojo.attr( id, 'title' ); // Change the height of a node dojo.attr( 'img_growing_tree', 'height', 100 );
Vanila JS attr
$(token).id=new_id; // set attr for "id" var id = $(token).id; // get
dojo.query
With dojo.query, you can query a bunch of HTML elements with a single function, with a "CSS selector" style.
Example:
// All elements with class "possibleMove": var elements = dojo.query( '.possibleMove' ); // Count number of tokens (i.e., elements of class "token") on the board (i.e., the element with id "board"): dojo.query( '#board .token' ).length;
But what is really cool with dojo.query is that you can combine it with almost all methods above.
Examples:
// Trigger a method when the mouse enter in any element with class "meeple": dojo.query( '.meeple' ).connect( 'onmouseenter', this, 'myMethodToTrigger' ); // Hide all meeples who are on the board dojo.query( '#board .meeple' ).style( 'display', 'none' );
Vanila JS query
var cards=document.querySelectorAll(".hand .card");// all cards in all hands var cards=$('hand').querySelectorAll(".card");// all cards in specific hand var card=document.querySelector(".hand .card");// first card or null if none (super handy)
dojo.place
dojo.place is the best function to insert HTML code somewhere in your game interface without breaking something. It is much better to use than the innerHTML= method if you must insert HTML tags and not only values.
// Insert your HTML code as a child of a container element dojo.place( "<your html code>", "your_container_element_id" ); // Replace the container element with your new html dojo.place( "<your html code>", "your_container_element_id", "replace" );
The third parameter of dojo.place can take various interesting values:
"replace" : (see description above).
"first" : Places the node as a child of the reference node. The node is placed as the first child.
"last" (default) : Places the node as a child of the reference node. The node is placed as the last child.
"before" : places the node right before the reference node.
"after" : places the node right after the reference node.
"only" : replaces all children of the reference node with the node.
positive integer: This parameter can be a positive integer. In this case, the node will be placed as a child of the reference node with this number (counting from 0). If the number is more than number of children, the node will be appended to the reference node making it the last child.
See also full doc on dojo.place: [1]
Usually, when you want to insert some piece of HTML in your game interface, you should use "Javascript templates".
But you can also relocate elements like that. Note: it won't animate if you do that.
dojo.empty
Remove all children of the node element
dojo.empty('my_hand');
dojo.destroy
Remove the element
dojo.query(".green", mynode).forEach(dojo.destroy); // this remove all subnode of class green from mynode
dojo.create
Create element
dojo.create("div", { class: "yellow_arrow" }, parent); // this creates div with class yellow_array and places it in "parent"
addStyleToClass: function( cssClassName, cssProperty, propertyValue )
Same as dojo.style(), but for all the nodes set with the specified cssClassName
this.format_block This bga function that takes global var from template file and substitute variables, typical use would be
var player = gamedatas.players[player_id]; var div = this.format_block('jstpl_player_board', player ); // var jstpl_player_board = ... is defined in .tpl file
this.format_string This bga function just substitute variables in a string, i.e.
var div = this.format_string('
', {player_color: '#ff0000'} );
this.format_string_recursive This bga function is similar to this.format_string but is capable of processing recursive argument structures and translations. It is used to format server notifications.
Animations
Dojo Animations
BGA animations is based on Dojo Animation (see tutorial here).
However, most of the time, you can just use methods below, which are built on top of Dojo Animation.
Note: one interesting method from Dojo that could be useful from time to time is "Dojo.Animation". It allows you to make any CSS property "slide" from one value to another.
Note 2: the slideTo methods are not compatible with CSS transform (scale, zoom, rotate...). If possible, avoid using CSS transform on nodes that are being slided. Eventually, the only possible solution to make these 2 compatible is to disable all CSS transform properties, use slideToObjectPos/placeOnObjectPos, and then apply them again.
this.slideToObject( mobile_obj, target_obj, duration, delay )
You can use slideToObject to "slide" an element to a target position.
Sliding element on the game area is the recommended and the most used way to animate your game interface. Using slides allow players to figure out what is happening on the game, as if they were playing with the real boardgame.
The parameters are:
- mobile_obj: the ID of the object to move. This object must be "relative" or "absolute" positioned.
- target_obj: the ID of the target object. This object must be "relative" or "absolute" positioned. Note that it is not mandatory that mobile_obj and target_obj have the same size. If their size are different, the system slides the center of mobile_obj to the center of target_obj.
- duration: (optional) defines the duration in millisecond of the slide. The default is 500 milliseconds.
- delay: (optional). If you defines a delay, the slide will start only after this delay. This is particularly useful when you want to slide several object from the same position to the same position: you can give a 0ms delay to the first object, a 100ms delay to the second one, a 200ms delay to the third one, ... this way they won't be superposed during the slide.
BE CAREFUL: The method returns an dojo.fx animation, so you can combine it with other animation if you want to. It means that you have to call the "play()" method, otherwise the animation WON'T START.
Example:
this.slideToObject( "some_token", "some_place_on_board" ).play();
this.slideToObjectPos( mobile_obj, target_obj, target_x, target_y, duration, delay )
This method does exactly the same as "slideToObject", except than you can specify some (x,y) coordinates. This way, "mobile_obj" will slide to the specified x,y position relatively to "target_obj".
Example: slide a token to some place on the board, 10 pixels to the bottom:
this.slideToObjectPos( "some_token", "some_place_on_board", 0, 10 ).play();
this.slideTemporaryObject( mobile_obj_html, mobile_obj_parent, from, to, duration, delay )
This method is useful when you want to slide a temporary HTML object from one place to another. As this object does not exists before the animation and won't remain after, it could be complex to create this object (with dojo.place), to place it at its origin (with placeOnObject) to slide it (with slideToObject) and to make it disappear at the end.
slideTemporaryObject does all of this for you:
- mobile_obj_html is a piece of HTML code that represent the object to slide.
- mobile_obj_parent is the ID of an HTML element of your interface that will be the parent of this temporary HTML object.
- from is the ID of the origin of the slide.
- to is the ID of the target of the slide.
- duration/delay works exactly like in "slideToObject"
Example:
this.slideTemporaryObject( '<div class="token_icon"></div>', 'tokens', 'my_origin_div', 'my_target_div' ).play();
this.slideToObjectAndDestroy: function( node, to, time, delay )
This method is a handy shortcut to slide an existing HTML object to some place then destroy it upon arrival. It can be used for example to move a victory token or a card from the board to the player panel to show that the player earns it, then destroy it when we don't need to keep it visible on the player panel.
It works the same as this.slideToObject and takes the same arguments, but it starts the animation.
Example:
this.slideToObjectAndDestroy( "some_token", "some_place_on_board", 1000, 0 );
this.fadeOutAndDestroy( node, duration, delay )
This function fade out the target HTML node, then destroy it. Its starts the animation.
Example:
this.fadeOutAndDestroy( "a_card_that_must_disappear" );
CAREFUL: the HTML node still exists until during few milliseconds, until the fadeOut has been completed.
Rotating elements
You can check here an example of use of Dojo to make an element rotate.
This example combines "Dojo.Animation" method and a CSS3 property that allow you to rotate the element.
IMPORTANT: to asses browser compatibility, you must select the CSS property to use just like in the example (see sourcecode below):
var transform; dojo.forEach( ['transform', 'WebkitTransform', 'msTransform', 'MozTransform', 'OTransform'], function (name) { if (typeof dojo.body().style[name] != 'undefined') { transform = name; } }); // ... and then use "transform" as the name of your CSS property for rotation
Note: This wiki was probably written 10 years ago - now all modern broser support 'transform' so just ignore this and use transform.
Animation Callbacks
If you wish to run some code only after an animation has completed you can do this by linking a callback method.
var animation_id = this.slideToObject( mobile_obj, target_obj, duration, delay ); dojo.connect(animation_id, 'onEnd', dojo.hitch(this, 'callback_function', parameters)); animation_id.play(); … callback_function: function(params) { // this will be called after the animation ends },
If you wish to call a second animation after the first (rather than general code) then you can use a dojo animation chain (see tutorial referenced above).
Moving elements
this.placeOnObject( mobile_obj, target_obj )
placeOnObject works exactly like "slideToObject", except that the effect is immediate and that it is centered.
This is not really an animation, but placeOnObject is frequently used before starting an animation.
Example:
// (We just created an object "my_new_token") // Place the new token on current player board this.placeOnObject( "my_new_token", "overall_player_board_"+this.player_id ); // Then slide it to its position on the board this.slideToObject( "my_new_token", "a_place_on_board" ).play();
this.placeOnObjectPos( mobile_obj, target_obj, target_x, target_y )
This method works exactly like placeOnObject, except than you can specify some (x,y) coordinates. This way, the center of "mobile_obj" will be placed to the specified x,y position relatively to the center of "target_obj".
Note: the placement works differently from this.slideToObjectPos, since coordinates are calculated based on the center of objects.
this.attachToNewParent( mobile_obj, target_obj )
With this method, you change the HTML parent of "mobile_obj" element. "target_obj" is the new parent of this element. The beauty of attachToNewParent is that the mobile_obj element DOES NOT MOVE during this process.
Note: what happens is that the method calculate a relative position of mobile_obj to make sure it does not move after the HTML parent changes.
Why using this method?
Changing the HTML parent of an element can be useful for the following reasons:
- When the HTML parent moves, all its child are moving with them. If some game elements is no more linked with a parent HTML object, you may want to attach it to another place.
- The z_order (vertical order of display) depends on the position in the DOM, so you may need to change the parent of some game elements when they are moving in your game area.
CAREFUL: this function destroys original object and places a clone onto a new parent, this will break all references to this HTML element (ex: dojo.connect).
Players input
dojo.connect(element, event, context, method)
Used to associate a player event with one of your notification methods.
Example: associate a click on an element ("my_element") with one of our methods ("onClickOnMyElement"):
dojo.connect( $('my_element'), 'onclick', this, 'onClickOnMyElement' );
Same idea but base on query (i.e. all element of 'pet' class)
dojo.query(".pet").connect('onclick', this, 'onPet');
Note: the methods described here are the only correct ways to associate a player input event to your code, and you should not use anything else.
this.connect(element, event, handler)
Used to associate a player event with one of your notification methods.
this.connect( $('my_element'), 'onclick', 'onClickOnMyElement' );
Or you can use an in-place handler
this.connect( $('my_element'), 'onclick', (e) => { console.log('boo'); } );
Note that this function stores the connection handler. That is the only real difference between this.connect and dojo.connect. If you plan to destroy the element you connected, you must call this.disconnect() to prevent memory leaks.
this.connectClass(cssClassName, event, handler)
Same as connect(), but for all the nodes set with the specified cssClassName.
this.connectClass('pet', 'onclick', 'onPet');
this.disconnect(element, event)
Disconnect event handler (previously registered with this.connect or this.connectClass).
this.disconnect( $('my_element'), 'onclick');
Note: dynamic connect/disconnect is for advanced cases ONLY, you should always connect elements statically if possible, i.e. in setup() method.
this.disconnectAll()
Disconnect all previously registed event handlers (registered via this.connect or this.connectClass)
this.checkAction(action, nomessage)
Check if player can do the specified action by taking into account:
- current game state
- interface locking (a player can't do any action if an action is already in progress)
return true if action is authorized (i.e: the action is listed as a "possibleaction" in current game state).
return false and display an error message if not authorized (display no message if nomessage parameter is true). The displayed error message could be either "This move is not allowed at this moment" or "An action is already in progress".
Example:
function onClickOnGameElement( evt ) { if( this.checkAction( "my_action" ) ) { // Do the action } }
this.checkPossibleActions(action, nomessage)
- this is independent of the player being active, so can be used instead of this.checkAction(). This is particularly useful for multiplayer states when the player is not active in a 'player may like to change their mind' scenario. Unlike this.checkAction, this function does NOT take interface locking into account (bug or feature?)
Check if player can do the specified action by taking into account:
- current game state
this.ajaxcall(url, parameters, obj_callback, callback, callback_error, ajax_method)
This method must be used to send a player input to the game server. It should not be triggered programmatically, especially not in loops, in callbacks, in notifications, or in onEnteringState/onUpdateActionButtons/onLeavingState, in order not to create race conditions or break replay game and tutorial features. It should be used only in reaction to a user action in the interface.
- url: the url of the action to perform. For a game, it must be: "/<mygame>/<mygame>/myAction.html"
- parameters: an array of parameter to send to the game server.
- Note that "lock: true" must always be specified in this list of parameters in order the interface can be locked during the server call.
- Note: Restricted parameter names (please don't use them):
- "action"
- "module"
- "class"
- obj_callback: must be set to "this".
- callback: a function to trigger when the server returns and everything went fine (not used, as all data handling is done via notifications).
- callback_error: (optional and rarely used) a function to trigger when the server returns an error. if no error this function is called with parameter value false.
- ajax_method: (optional and rarely used) if you need to send large amounts of data (over 2048 bytes), you can set this parameter to 'post' (all lower-case) to send a POST request as opposed to the default GET. This works, but was not officially documented, so only use if you really need to.
Usage:
this.ajaxcall( '/mygame/mygame/myaction.html', { lock: true, arg1: myarg1, arg2: myarg2, ... }, this, function( result ) { // Do some stuff after a successful call // NB : usually not needed as changes must be handled by notifications // You should NOT modify the interface in a callback or it will most likely break the framework replays (or make it inaccurate) // You should NOT make another ajaxcall in a callback in order not to create race conditions } );
Note: to reduce the boilerplate code you can define your own wrapper, which will do checking, locking and allow to skip parameters, for example
ajaxcallwrapper: function(action, args, handler) { if (!args) { args = []; } args.lock = true; if (this.checkAction(action)) { this.ajaxcall("/" + this.game_name + "/" + this.game_name + "/" + action + ".html", args, this, (result) => { }, handler); } },
This can be called like this which is a lot more compact
this.ajaxcallwrapper('playDraw'); this.ajaxcallwrapper('playMove', {card: id})
this.isInterfaceLocked()
When using "lock: true" in ajax call you can use this function to check if interface is in lock state (it will be locked during server call and notification processing). This check can be used to block some other interactions which do not result in ajaxcall or if you want to suppress errors.
Translations
See Translations
Notifications
When something happens on the server side, your game interface Javascript logic received a notification.
Here's how you can handle these notifications on the client side.
Subscribe to notifications
Your Javascript "setupNotifications" method is the place where you can subscribe to notifications from your PHP code.
Here's how you associate one of your Javascript method to a notification "playDisc" (from Reversi example):
// In setupNotifications method: dojo.subscribe( 'playDisc', this, "notif_playDisc" );
Note: the "playDisc" corresponds to the name of the notification you define it in your PHP code, in your "notifyAllPlayers" or "notifyPlayer" method.
Then, you have to define your "notif_playDisc" method:
notif_playDisc: function( notif ) { // Remove current possible moves (makes the board more clear) dojo.query( '.possibleMove' ).removeClass( 'possibleMove' ); this.addDiscOnBoard( notif.args.x, notif.args.y, notif.args.player_id ); },
In a notification handler like our "notif_playDisc" method, you can access all notifications arguments with "notif.args".
Example:
PHP
self::notifyAllPlayers( "myNotification", '', array( "myArgument" => 3 ) );
JavaScript
//You can access the "myArgument" like this: notif_myNotification: function( notif ) { alert( "myArgument = " + notif.args.myArgument ); }
The notification Object received by client
When sending a notification on your PHP, the client side will receive an Object with the following attributes:
- args : This is the arguments that you passed on your notification method on php
- bIsTableMsg : Boolean, is true when you use NotifyAllPlayers method (false otherwise)
- channelorig : information about table ID (formatted as : "/table/t[TABLE_NUMBER]")
- gamenameorig : name of the game
- log: the log information as written in PHP function
- move_id : ID of the move associated with the notification
- table_id : ID of the table
- time : UNIX GMT time
- type : name of the notification
- uid : identifier of the notification
Note that those information were inferred from observation on console log. If an Admin can confirm/correct (and remove this line), you're welcome :)
Synchronous notifications
When several notifications are received by your game interface, these notifications are processed immediately, one after the other, in the same exact order they have been generated in your PHP game logic.
However, sometimes, you need to give some time to the players to figure out what happened on the game before jumping to the next notification. Indeed, in many games, there are a lot of automatic actions, and the computer is going to resolve all these actions very fast if you don't tell it not to do so.
As an example, for Reversi, when someone is playing a disc, we want to wait 500 milliseconds before doing anything else in order the opponent player can figure out what move has been played.
Here's how we do this, right after our subscription:
dojo.subscribe( 'playDisc', this, "notif_playDisc" ); this.notifqueue.setSynchronous( 'playDisc', 500 ); // Wait 500 milliseconds after executing the playDisc handler
It is also possible to control the delay timing dynamically (e.g., using notification args). As an example, maybe your notification 'cardPlayed' should pause for a different amount of time depending on the number or type of cards played.
For this case, use setSynchronous without specifying the duration and use setSynchronousDuration within the notification callback.
- NOTE: If you forget to invoke setSynchronousDuration, the game will remain paused forever!
setupNotifications: function () { dojo.subscribe( 'cardPlayed', this, 'notif_cardPlayed' ); this.notifqueue.setSynchronous( 'cardPlayed' ); // wait time is dynamic ... }, notif_cardPlayed: function (notif) { // MUST call setSynchronousDuration // Example 1: From notification args (PHP) this.notifqueue.setSynchronousDuration(notif.args.duration); ... // Or, example 2: Match the duration to a Dojo animation var anim = dojo.fx.combine([ ... ]); anim.play(); this.notifqueue.setSynchronousDuration(anim.duration); },
WARNING: combining synchronous and ignored notifications
You must be careful when combining dynamic synchronous durations (as described above) with ignored notifications. If you have a conditionally ignored notification like this (see below section):
this.notifqueue.setIgnoreNotificationCheck( 'myNotif', (notif) => (notif.args.player_id == this.player_id) /* or any other condition */ )
then you CANNOT do
this.notifqueue.setSynchronous('myNotif');
as, when the ignored check passes, the notification handler, in which `this.notifqueue.setSychronousDuration` is called, is never called and so the duration is never set and interface locking results.
The workaround is to set a "dummy" time:
this.notifqueue.setSynchronous('myNotif', 5000);
whose value is irrelevant but must be large enough to cover the time before the notification handler is called. The large value never actually comes into play because the notification is either ignored, or the synchronous duration is reset to a sensible value inside the handler.
Ignoring notifications
Sometimes you need to ignore some notification on client side. You don't want them to be shown in game log and you don't want them to be handled.
The most common use case is when a player gets private information. They will receive a specific notification (such as "You received Ace of Heart"), while other players would receive more generic notification ("Player received a card").
In X.game.php
$this->notifyAllPlayers("dealCard", clienttranslate('${player_name} received a card'), [ 'player_id' => $playerId, 'player_name' => $this->getActivePlayerName() ]); $this->notifyPlayer($playerId, "dealCardPrivate", clienttranslate('You received ${cardName}'), [ "type" => $card["type"], "cardName" => $this->getCardName($card["type"]) ]);
The problem with this approach is that the active player will receive two notifications:
- Player1 received a card
- You received Ace of Hearts
Hence, notification ignoring. Similar to setting a synchronous notification above, you can set up a check whether a notification should be ignored:
this.notifqueue.setIgnoreNotificationCheck( 'dealCard', (notif) => (notif.args.player_id == this.player_id) );
NOTE: You can think that it would be possible to send such notification to all players except active just by using notifyPlayer and it seems to work. The problem however is that table spectators would miss such notification and their user interface (and game log) wouldn't be updated. Since there is no way to send notification just to spectators, ignoring the notification (or "filtering") is the only reasonable solution.
setIgnoreNotificationCheck(notificationId, predicate) This method will set a check whether any of notifications of specific type should be ignored.
The parameters are:
- notificationId: before dispatching any notification of this type, the framework will call predicate to check whether notification should be ignored
- predicate (notif => boolean): a function that will receive notif object and will return true if this specific notification should be ignored
IMPORTANT: Remember that this notification is ignored on the client side, but was still received by the client. Therefore it shouldn't contain any private information as cheaters can get it. In other words this is not a way to hide information.
IMPORTANT: When a game is reloaded with F5 or when opening a turn based game, old notifications are replayed as history notification. They are used just to update the game log and are stripped of all arguments except player_id, i18n and any argument present in message. If you use and other argument in your predicate you should preserve it as explained here.
Pre-defined notification types
tableWindow - This defines notification to display Scoring Dialogs, see below.
message - This defines notification that shows on players log and have no other effect (technically any unhandled notification will do the same but its recommended to use this keyword for consistency)
// You can call this on php side without doing anything on client side self::notifyAllPlayers( 'message', 'hello', [] );
simplePause - This notification will just delay other notifications, maybe useful if you know you need some extra time for animation or something. Requires a time parameter.
self::notifyAllPlayers( 'simplePause', '', [ 'time' => 500] ); // time is in milliseconds
Tooltips
this.addTooltip( nodeId, helpStringTranslated, actionStringTranslated, delay )
Add a simple text tooltip to the DOM node.
Specify 'helpStringTranslated' to display some information about "what is this game element?". Specify 'actionStringTranslated' to display some information about "what happens when I click on this element?".
You must specify both of the strings. You can only use one and specify an empty string () for the other one.
When you pass text directly function _() must be used for the text to be marked for translation! Except for empty string.
Parameter "delay" is optional. It is primarily used to specify a zero delay for some game element when the tooltip gives really important information for the game - but remember: no essential information must be placed in tooltips as they won't be displayed in some browsers (see Guidelines).
Example:
this.addTooltip( 'cardcount', _('Number of cards in hand'), '' );
Note: this generates static tooltip and attaches to existing dom element, if you need to generate tooltip more dynamically you have to call that method every time information about object is updated or use completely different tehnique
this.addTooltipHtml( nodeId, html, delay )
Add an HTML tooltip to the DOM node (for more elaborate content such as presenting a bigger version of a card).
this.addTooltipToClass( cssClass, helpStringTranslated, actionStringTranslated, delay )
Add a simple text tooltip to all the DOM nodes set with this cssClass. See more details above for this.addTooltip.
this.addTooltipToClass( 'meeple', _('This is A Meeple'), _('Click to tickle') );
IMPORTANT: all concerned nodes must exist and have IDs to get tooltips.
this.addTooltipHtmlToClass( cssClass, html, delay )
Add an HTML tooltip to to all the DOM nodes set with this cssClass (for more elaborate content such as presenting a bigger version of a card).
IMPORTANT: all concerned nodes must exist and have IDs to get tooltips.
this.removeTooltip( nodeId )
Remove a tooltip from the DOM node with given id.
force tooltip to open
If you want to force tooltip to open in reaction to some other action, i.e. click you can do this
this.tooltips[id].open(id)
where id is the id of the tooltip node where tooltip was installed.
Dialogs, warning messages, confirmation dialogs, ...
Warning messages
Sometimes, there is something important that is happening in the game and you have to make sure all players get the message. Most of the time, the evolution of the game situation or the game log is enough, but sometimes you need something more visible.
Ex: someone fulfills one of the end of the game conditions, so this is the last turn.
this.showMessage( msg, type )
showMessage shows a message in a big rectangular area on the top of the screen of the current player.
- "msg" is the string to display. It should be translated.
- "type" can be set to "info", "error", or "only_to_log". If set to "info", the message will be an informative message on a white background. If set to "error", the message will be an error message on a red background. If set to "only_to_log", the message will be added to the game log but will not popup at the top of the screen.
Important: the normal way to inform players about the progression of the game is the game log. "showMessage" is intrusive and should not be used often.
Confirmation dialog
confirmationDialog( message, yesHandler, noHandler )
When an important action with a lot of consequences is triggered by the player, you may want to propose a confirmation dialog.
CAREFUL: the general guideline of BGA is to AVOID the use of confirmation dialogs. Confirmation dialogs slow down the game and bother players. The players know that they have to pay attention to each move when they are playing online.
The situations where you should use a confirmation dialog are the following:
- It must not happen very often during a game.
- It must be linked to an action that can really "kill a game" if the player does not pay attention.
- It must be something that can be done by mistake (ex: a link on the action status bar).
How to display a confirmation dialog:
this.confirmationDialog(_('Are you sure you want to bake the pie?'), () => { this.bakeThePie(); }); return; // nothing should be called or done after calling this, all action must be done in the handler
Multiple choice dialog
You can use this dialog to give user a choice with small amount of options:
var keys = [1,5,10]; this.multipleChoiceDialog( _('How many bugs to fix?'), keys, (choice) => { var bugchoice = keys[choice]; console.log('dialog callback with '+bugchoice); this.ajaxcall( '/mygame/mygame/fixBugs.html', { bugs: bugchoice}, this, function( result ) {} ); } );
Dialogs
As a general rule, you shouldn't use dialogs windows.
BGA guidelines specify that all game elements should be displayed on the main screen. Players can eventually scroll down to see game elements they don't need to see anytime, and you may eventually create anchors to move between game area section. Of course dialogs windows are very practical, but the thing is: all players know how to scroll down, and not all players know how to show up your dialog window. In addition, when the dialog shows up, players can't access the other game components.
Sometimes although, you need to display a dialog window. Here is how you do this:
// Create the new dialog over the play zone. You should store the handler in a member variable to access it later this.myDlg = new ebg.popindialog(); this.myDlg.create( 'myDialogUniqueId' ); this.myDlg.setTitle( _("my dialog title to translate") ); this.myDlg.setMaxWidth( 500 ); // Optional // Create the HTML of my dialog. // The best practice here is to use Javascript templates var html = this.format_block( 'jstpl_myDialogTemplate', { arg1: myArg1, arg2: myArg2, ... } ); // Show the dialog this.myDlg.setContent( html ); // Must be set before calling show() so that the size of the content is defined before positioning the dialog this.myDlg.show(); // Now that the dialog has been displayed, you can connect your method to some dialog elements // Example, if you have an "OK" button in the HTML of your dialog: dojo.connect( $('my_ok_button'), 'onclick', this, function(evt){ evt.preventDefault(); this.myDlg.destroy(); } );
If necessary, you can remove the default top right corner 'close' icon, or replace the function called when it is clicked:
// Removes the default close icon this.myDlg.hideCloseIcon();
// Replace the function call when it's clicked this.myDlg.replaceCloseCallback( function() { ... } );
Scoring dialogs
Sometimes at the end of a round you want to display a big table that details the points wins in each section of the game.
Example: in Hearts game, we display at the end of each round the number of "heart" cards collected by each player, the player who collected the Queen of Spades, and the total number of points loose by each player.
Scoring dialogs are managed entirely on PHP side, but they are described here as their effects are visible only on client side.
Displaying a scoring dialog is quite simple and is using a special notification type: "tableWindow":
// on PHP side: $this->notifyAllPlayers( "tableWindow", '', array( "id" => 'finalScoring', "title" => clienttranslate("Title of the scoring dialog"), "table" => $table ) );
The "table" argument is a 2 dimensional PHP array that describe the table you want to display, line by line and column by column.
Example: display an 3x3 array of strings
$table = array( array( "one", "two", "three" ), // This is my first line array( "four", "five", "six" ), // This is my second line array( "seven", "height", "nine" ) // This is my third line );
As you can see above, in each "cell" of your array you can display a simple string value. But you can also display a complex value with a template and associated arguments like this:
$table = array( array( "one", "two", array( "str" => clienttranslate("a string with an ${argument}"), "args" => array( 'argument' => 'argument_value' ) ) ), array( "four", "five", "six" ), array( "seven", "height", "nine" ) );
This is especially useful when you want to display player names with colors. Example from "Hearts":
$firstRow = array( '' ); foreach( $players as $player_id => $player ) { $firstRow[] = array( 'str' => '${player_name}', 'args' => array( 'player_name' => $player['player_name'] ), 'type' => 'header' ); } $table[] = $firstRow;
You can also use three extra attributes in the parameter array for the notification:
$this->notifyAllPlayers( "tableWindow", '', array( "id" => 'finalScoring', "title" => clienttranslate("Title of the scoring dialog"), "table" => $table, "header" => array('str' => clienttranslate('Table header with parameter ${number}'), 'args' => array( 'number' => 3 ), ), "footer" => '<div>Some footer</div>', "closing" => clienttranslate( "Closing button label" ) ) );
- header: the content for this parameter will display before the table (also, the html will be parsed and player names will be colored according to the current game colors).
- footer: the content for this parameter will display after the table (no parsing for coloring the player names)
- closing: if this parameter is used, a button will be displayed with this label at the bottom of the popup and will allow players to close it (more easily than by clicking the top right 'cross' icon).
Scoring animated display
Sometimes (Terra Mystica final scoring for example), you may want to display a score value over an element to make the scoring easier to follow for the players. You can do it with:
this.displayScoring( anchor_id, color, score, duration, offset_x, offset_y );
anchor_id: ID of the element to place the animated score onto (without the '#')
color: hexadecimal RGB representation of the color (should be the color of the scoring player), but without a leading '#'. For instance, 'ff0000' for red.
score: numeric score to display, prefixed by a '+' or '-'
duration: animation duration in milliseconds
offset_x and offset_y: if both offset_x and offset_y are defined and not null, apply the following offset (in pixels) to the scoring animation. Note that the score is centered in the anchor, so the offsets might have to be negative if you calculate the position.
Note: if you want to display successively each score, you can use this.notifqueue.setSynchronous() function.
Speech bubble
For better interactivity in some games (Love Letter for example), you may use comic book style speech bubbles to express the players voices. This is done with:
this.showBubble(anchor_id, text, delay, duration, custom_class)
text - what to put in bubble, can be html actually not just text
delay - in milliseconds is optional (default 0)
duration - in milliseconds is optional (default 3000)
custom_class - extra class to add to bubble is optional, if you need to override the default bubble style
Warning: if your bubble could overlap other active elements of the interface (buttons in particular), as it stays in place even after disappearing, you should use a custom class to give it the style "pointer-events: none;" in order to intercept click events.
Note: If you want this visually, but want to take complete control over this bubble and its animation (for example to make it permanent) you can just use div with 'discussion_bubble' class on it, and content of div is what will be shown.
Update players score
The column player_score from the player table is automatically loaded into this.scoreCtrl and therefore into the stars location on the player board. This occurs sometime after the <gamename>.js setup() function. However this score must be updated as the game progresses through player notifications (notifs).
Increase a player score (with a positive or negative number):
this.scoreCtrl[ player_id ].incValue( score_delta );
Set a player score to a specific value:
this.scoreCtrl[ player_id ].setValue( new_score );
Set a player score to a specific value with animation :
this.scoreCtrl[ player_id ].toValue( new_score );
Typical usage would be (that will process 'score' notification):
setupNotifications : function() { ... dojo.subscribe('score', this, "notif_score"); }, notif_score: function(notif) { this.scoreCtrl[notif.args.player_id].setValue(notif.args.player_score); },
Players panels
Adding stuff to player's panel
At first, create a new "JS template" string in your template (tpl) file:
(from Gomoku example)
var jstpl_player_board = '\<div class="cp_board">\ <div id="stoneicon_p${id}" class="gmk_stoneicon gmk_stoneicon_${color}"></div><span id="stonecount_p${id}">0</span>\ </div>';
Then, you add this piece of code in your JS file to add this template to each player panel:
// Setting up player boards for( var player_id in gamedatas.players ) { var player = gamedatas.players[player_id]; // Setting up players boards if needed var player_board_div = $('player_board_'+player_id); dojo.place( this.format_block('jstpl_player_board', player ), player_board_div ); }
(Note: the code above is of course from your "setup" function in your Javascript).
Very often, you have to distinguish current player and others players. In this case, you just have to create another JS template (ex: jstpl_otherplayer_board) and use it when "player_id" is different than "this.player_id".
Player's panel disabling/enabling
this.disablePlayerPanel( player_id )
Disable given player panel (the panel background become gray).
Usually, this is used to signal that this played passes, or will be inactive during a while.
Note that the only effect of this is visual. There are no consequences on the behaviour of the panel itself.
this.enablePlayerPanel( player_id )
Enable a player panel that has been disabled before.
this.enableAllPlayerPanels()
Enable all player panels that has been disabled before.
Image loading
See also Game_art:_img_directory.
Be careful: by default, ALL images of your img directory are loaded on a player's browser when he loads the game. For this reason, don't let in your img directory images that are not useful, otherwise it's going to slowdown the game load.
dontPreloadImage( image_file_name )
Using dontPreloadImage, you tell the interface to not preload a specific image in your img directory.
Example of use:
this.dontPreloadImage( 'cards.png' );
This is particularly useful if for example you have 2 different themes for a game. To accelerate the loading of the game, you can specify to not preload images corresponding to the other theme.
Another example of use: in "Gosu" game with Kamakor extension, you play with 5 sets of cards among 10 available. Cards images are organized by sets, and we only preload the images corresponding to the 5 current sets with ensureSpecificGameImageLoading( image_file_names_array ).
// By default, do not preload anything this.dontPreloadImage( 'cards.png' ); this.dontPreloadImage( 'clan1.png' ); this.dontPreloadImage( 'clan2.png' ); this.dontPreloadImage( 'clan3.png' ); this.dontPreloadImage( 'clan4.png' ); this.dontPreloadImage( 'clan5.png' ); this.dontPreloadImage( 'clan6.png' ); this.dontPreloadImage( 'clan7.png' ); this.dontPreloadImage( 'clan8.png' ); this.dontPreloadImage( 'clan9.png' ); this.dontPreloadImage( 'clan10.png' ); var to_preload = []; for( i in this.gamedatas.clans ) { var clan_id = this.gamedatas.clans[i]; to_preload.push( 'clan'+clan_id+'.png' ); } if( to_preload.length == 5 ) { this.ensureSpecificGameImageLoading( to_preload ); }
Note: You don't need to specify to not preload game box images (game_box.png, game_box75.png...) since they are not preloaded by default.
Other useful stuff
dojo.hitch
With dojo.hitch, you can create a callback function that will run with your game object context whatever happen.
Typical example: display a BGA confirmation dialog with a callback function created with dojo.hitch:
this.confirmationDialog( _('Are you sure you want to make this?'), dojo.hitch( this, function() { this.ajaxcall( '/mygame/mygame/makeThis.html', { lock:true }, this, function( result ) {} ); } ) );
In the example above, using dojo.hitch, we ensure that the "this" object will be set when the callback is called.
NOTE: In modern JS there are lambdas that eliminate need for that, the example above will look like this
this.confirmationDialog( _('Are you sure you want to make this?'), () => { this.ajaxcall( '/mygame/mygame/makeThis.html', { lock:true }, this, (result) => {} ); } );
- updateCounters(counters)
- Useful for updating game counters in the player panel (such as resources).
- 'counters' arg is an associative array [counter_name_value => [ 'counter_name' => counter_name_value, 'counter_value' => counter_value_value], ... ]
- All counters must be referenced in this.gamedatas.counters and will be updated.
- DOM objects referenced by 'counter_name' will have their innerHTML updated with 'counter_value'.
- onScreenWidthChange()
- This function can be overridden in your game to manage some resizing on the client side when the browser window is resized. This function is also triggered at load time, so it can be used to adapt to the :viewport size at the start of the game too.
- updatePageTitle()
- This function allows to update the current page title and turn description according to the game state. If the current game state description this.gamedatas.gamestate.descriptionmyturn is modified before :calling the function, it allows to update the turn description without changing state.
Example from Terra Mystica:
onClickFavorTile: function( evt ) { ... if ( ... ) { this.gamedatas.gamestate.descriptionmyturn = _('Special action: ') + _('Advance 1 space on a Cult track'); this.updatePageTitle(); this.removeActionButtons(); this.addActionButton( ... ); ... return; } ... }
BGA GUI components
BGA framework provides some useful ready-to-use components for the game interface:
Studio#BGA_Studio_game_components_reference
Note that each time you are using an additional component, you must declare it at the top of your Javascript file in the list of modules used.
Example if you are using "ebg.stock":
define([ "dojo","dojo/_base/declare", "ebg/core/gamegui", "ebg/counter", "ebg/stock" /// <=== we are using ebg.stock module ],
BGA Buttons
this.addActionButton( id, label, method, (opt)destination, (opt)blinking, (opt)color )
You can use this method to add an action button in the main action status bar.
Arguments:
- id: an element ID that should be unique in your HTML DOM document.
- label: the text of the button. Should be translatable (use _() function).
- method: the name of your method that must be triggered when the player clicks on this button.
- destination (optional): deprecated, do not use this. Use null as value if you need to specify other arguments.
- blinking (optional): if set to true, the button is going blink to catch player's attention. Please don't abuse of blinking button.
- color: could be blue (default), red or gray.
You should only use this method in your "onUpdateActionButtons" method. Usually, you use it like this (from Hearts example):
onUpdateActionButtons: function( stateName, args ) { if (this.isCurrentPlayerActive()) { switch( stateName ) { case 'giveCards': this.addActionButton( 'giveCards_button', _('Give selected cards'), 'onGiveCards' ); break; } } },
In the example above, we are adding a "Give selected cards" button in the case we are on game state "giveCards". When player clicks on this button, it triggers our "onGiveCards" method.
Example using blinking red button:
this.addActionButton( 'commit_button', _('Confirm'), 'onConfirm', null, true, 'red');
Note: at least in studio example above will make button huge, because it sets it display of blinking things to block, if you don't like it you have to change css display value of the button to inline-block (the id of the button is the first argument, i.e 'commit_button' in example above)
buttons with images
You can use the same method, but add extra class to a button to disable the padding and style it, i.e.
this.addActionButton( 'button_brick', '<div class="brick"></div>', ()=>{... on brick ...}, null, null, 'gray'); dojo.addClass('button_brick','bgaimagebutton');
where
.bgaimagebutton { padding: 0px 12px; min-height: 28px; border: none; }
If you use this a lot, you can define a helper function, i.e.
/** * This method can be used instead of addActionButton, to add a button which is an image (i.e. resource). Can be useful when player * need to make a choice of resources or tokens. */ addImageActionButton: function(id, div, handler, bcolor, tooltip) { if (typeof bcolor == "undefined") { bcolor = "gray"; } // this will actually make a transparent button id color = gray this.addActionButton(id, div, handler, '', false, bcolor); // remove boarder, for images it better without dojo.style(id, "border", "none"); // but add shadow style (box-shadow, see css) dojo.addClass(id, "shadow bgaimagebutton"); // you can also add addition styles, such as background if (tooltip) { dojo.attr(id, "title", tooltip); } return $(id); },
buttons outside of action bar
You can create a custom button, but the BGA framework provides a standard button that requires only .css classes: bgabutton and bgabutton_${color}.
Examples:
<a href="#" id="my_button_id" class="bgabutton bgabutton_blue"><span>My blue button</span></a>
<a href="#" id="my_button_id" class="bgabutton bgabutton_gray"><span>My gray button</span></a>
<a href="#" id="my_button_id" class="bgabutton bgabutton_red"><span>My red button</span></a>
<a href="#" id="my_button_id" class="bgabutton bgabutton_red bgabutton_big"><span>My big red button</span></a>
Note: To see it in action, check out Coloretto.
Note: You can also create button using addActionButton() method, then move anywhere
this.addActionButton( 'commit_button', _('Confirm'), 'onConfirm', null, true, 'red'); dojo.place('commit_button','player_board');
Disabling: You can disable the bgabutton by adding the css class disabled in you js. The disabled button is still visible but is grey and not clickable. For example in the onUpdateActionButtons :
this.addActionButton( 'play_button_id', _('Play 1 to 3 cards'), 'playFunctionButton', null, false, 'blue' ); //Create a blue button if (Condition == true) { dojo.addClass( 'play_button_id', 'disabled');//disable the button }
Handler with arguments
If you want to call the handled with arguments, you can use arrow functions, like this:
this.addActionButton( 'commit_button', _('Confirm'), () => onConfirm(selectedCardId), null, true, 'red');
Sounds
Add a custom sound and make it load with your interface:
Add this in your template (.tpl) file:
<audio id="audiosrc_<gamename>_<yoursoundname>" src="{GAMETHEMEURL}img/<gamename>_<yoursoundname>.mp3" preload="none" autobuffer></audio> <audio id="audiosrc_o_<gamename>_<yoursoundname>" src="{GAMETHEMEURL}img/<gamename>_<yoursoundname>.ogg" preload="none" autobuffer></audio>
Note: this is a requirement to provide both a mp3 and a ogg file with the names <gamename>_<yoursoundname>[.ogg][.mp3]
.
Play the sound (from your .js file):
playSound('<gamename>_<yoursoundname>');
Disable the standard "move" sound for this move (to replace it with your custom sound):
Add this to your notification handler:
this.disableNextMoveSound();
Note: it only disable the sound for the next move.