Overview
If the Stock component works for you then great, you don't need this article. If you start tweaking it and you are doing something slightly more complex and you cannot find an answer, it may be best to not use it. This article describes how to replace Stock. What is Stock? It's a component that does a lot of stuff at once:
- It generates in-line attributes of an object the represents graphics and dimensions for "cards" (can be anything really).
- It also manages the layout of a single container of such cards, and cards within it - including animation.
- It also manages selections in that container.
Recipe to get rid of Stock (the gist):
- Create all cards as divs in the template file (with the help of view.php as needed)
- Create generic css classes for the cards defining images, sizing, etc. Create/generate a css class for each card type with background positioning - there are scripts in sharedcode project to generate grid positioning or you can use some online css generator (which is what Stock does under the hood)
- Create a parent div for card placement and use dojo.place to move cards there. In css use your preferred layout for this, including margins, etc.
- Tricky one: When cards are moved during animations you have to use a special method; none of animation methods from BGA parent classes will work, as the card will not have absolute positioning. You can use methods from the sharedcode project or create your own (the trick is that during animation the object has to have absolute positioning, which is removed after the animation).
Presentation
In modern JS the objects that we want to render will always be represented by div elements in the dom. This div will have a unique id, classes, and also custom data attributes which can be used for styling and selection.
Here is an example of some sort of card 1 of type card_21. Note that you probably don't need both card_21 as a class and as a data-num, you can use either for selection or styling.
<div id="card_21_1" class="card card_21" data-num="21"></div>
This is the 10 of hearts and king of clubs inside a hand in a game
<div id="game" class="classic_deck"> <div id="hand" class="hand"> <div id="card_H_10" class="card" data-suit='H' data-rank="10"> </div> <div id="card_C_K" class="card" data-suit='C' data-rank="K"> </div> </div> </div>
If you prefer not to use custom attributes you can do this:
<div id="card_H_10" class="card_H_10 suit_H rank_10"> </div>
Card id, does not have to be like this either, I use this one because it maps to my db, but if you are using the Deck component for cards in a "deck" table, you can use something like:
<div id="deck_33" class="card_H_10 suit_H rank_10"> </div>
where 33 is id which maps to database id of deck table.
Now we can use css to show graphics and defined size. Note unlike Stock - the size should not be bound in this component, your cards can have different sizes (for example in tooltips vs. in the hand).
We will use the BGA card stock https://x.boardgamearena.net/data/others/cards/FULLREZ_CARDS_ORIGINAL_NORMAL.jpg.
It has 15 columns.
.card { background-image: url('https://x.boardgamearena.net/data/others/cards/FULLREZ_CARDS_ORIGINAL_NORMAL.jpg'); /* don't do full url in your game, copy this file inside img folder */ background-size: 1500% auto; /* this mean size of background is 15 times bigger than size of card, because its sprite */ border-radius: 5%; width: 10em; height: 13.5em; box-shadow: 0.1em 0.1em 0.2em 0.1em #555; } .card[data-rank="10"] { /* 10 is column number 10 - 2 because we start from 0 and first card is sprite is 2. The multiplier is (15 - 1) is because we have 15 columns. -1 is because % in CSS is weird like that. */ background-position-x: calc(100% / (15 - 1) * (10 - 2)); } .card[data-rank="K"] { /* King will be number 13 in rank */ background-position-x: calc(100% / (15 - 1) * (13 - 2)); } .card[data-suit="H"] { /* Hears row position is 1 (because we count from 0). Multiplier (4 - 1) is because we have 4 rows and -1 is because % in CSS is weird like that. */ background-position-y: calc(100% / (4 - 1) * (1)); } .card[data-suit="C"] { /* Clubs row position is 2 */ background-position-y: calc(100% / (4 - 1) * (2)); }
Now if you want to see these cards, we can put them in some specific component for example hand, and there we can define layout and sizing just for hand
.hand .card { font-size: 2.4rem; display: inline-block; }
Working example: https://codepen.io/VictoriaLa/pen/rNwgWrB
To finish the deck you have to finish css for all rows/columns which whould be only 10-20 records, really easy and repetitive! If you really cannot write that much css manually you can also generate it using php code (this is side kick command line - does not go to your game)
<?php // this simple script generates sprite css // it has no params - fix inline to change what it generates $from=1; // from index $to=40+4; // to index $maxcol=4; // number of columns in the sprite $scol=$maxcol-1; $srow=((int)(($to-$from)/$maxcol)); for ($num=$from;$num<=$to;$num++) { $index=$num-$from; $row=(int)($index/$maxcol); $col=$index%$maxcol; echo ".tech_E_$num { background-position: calc(100% / $scol * $col) calc(100% / $srow * $row);}\n"; } ?>
And now if you want to user to have preferece for what deck style to use its just matter of change class from "classic_deck" to "fancy_deck" in our game parent div and adding in CSS
.fancy_deck .card { background-image: url('https://x.boardgamearena.net/data/others/cards/FULLREZ_CARDS_DESIGN_COLORED.jpg'); }
Generating Dom Elements
There are multiple ways to generate dom elements, few of the methods described below - they are alternatives, i.e. you only pick one of those.
Static - basic
You basically write all your cards in .tpl file. Just cut & paste you template and fix few classes. That is easiest! In your js code you never create new element, you just move existing element inside dome. If you need a copy for tooltips, logs or button you can clone one of them change id and add/remove class if needed.
<div id="limbo" class="limbo"> <div id="card_H_10" class="card" data-suit='H' data-rank="10"> </div> <div id="card_C_K" class="card" data-suit='C' data-rank="K"> </div> ... rince and repeat ... </div>
Static - bga template engine
in .tpl
in view.php
$this->page->begin_block($template, "deck_cards"); $ranks = [2,3,4,5,6,7,8,9,10,'J','Q','K','A']; $suits = ['S','H','C','D']; foreach ($ranks as $rank) { foreach ($suits as $suit) { $this->page->insert_block("deck_cards", [ 'SUIT' => "$suit, 'RANK' => "$rank, ]); }
}
Dynamic using .tpl thingy
In the js section of .tpl
var jstpl_card = '<div id="card_{SUIT}_{RANK}" class="card card_{SUIT}_{RANK}" data-suit="{SUIT}" data-rank="{RANK}"></div>';
in js:
let cardDiv = this.format_block('jstpl_card', { SUIT : 'H', RANK : 'A' }); // this in js code somewhere before placing it, then use dojo.place to place it
Dynamic using dojo
let suit = 'H'; let rank = 'A'; let cardNode = dojo.create('div', {id: `card_${suit}_${rank}`, class: `card card_${suit}_${rank}`, dataSuit: suit, dataRank: rank}); dojo.place(cardNode, 'hand'); // place in hand for example
Technically you can do it all in one line as "place" is 3rd parameter of dojo.create
Dynamic not using dojo
let suit = 'H'; let rank = 'A'; let div = document.createElement('div'); div.id = `card_${suit}_${rank}`; div.className = `card card_${suit}_${rank}`; div.setAttribute('data-suit',suit); div.setAttribute('data-rank',rank); //console.log(div); $('hand').appendChild(div);
As you can see this is not most compact method, so you if you do that a lot probably create a function that generates this (which also would set a click handler and tooltip for example)
Layout
You can use all web resources and power of css to do any sort of fancy layout for your cards, you not bound to any predefined layouts. The simplest would be display: inline-block as we already did above. You can also add margins to overlap cards for example.
More advanced layouts can be done with flex or grid:
.tableau { position: relative; width: 50em; height: 30em; outline: dashed 1px green; transform: rotate(-90deg) scale(0.5); padding: 1em; display: flex; justify-content: center; flex-direction: row; flex-wrap: wrap; } .tableau .card{ margin-right: -2em; }
In this example cards on tableu are smaller, rotated 90 degrees and overlaping.
https://codepen.io/VictoriaLa/pen/PojvWEV
Selection and Click Handlers
To hookup selection or clicking, you have to manually hook up handler to all cards, which is easy
document.querySelectorAll(".card").forEach(node=>node.addEventListener("click", (event) => { var target = event.target; target.classList.toggle('selected'); } }));
And you define class for selected in css
.selected { outline: dashed blue 0.1em; }
Now if you need to do some action based on selection this can be in your "do something" button handler (this example below will create array of ids of cards)
const card_ids = Array.from(document.querySelectorAll('.card.selected')).map(x=>x.id);
If you need single selection, just add a call to remove all previous 'selected' from all card, before adding it.
If you don't need selection you can hook ajax call directly to handler above.
Animation
The animation is the only tricky part because it does not come for free. The proper animation function is pretty lengthy so I won't even put it here. Basically you would have to reparent our object without visual movement and then you can run animation on positions, and after it is done remove absolute positioning.
The simplier method is moving object itself, you can see code and example in https://github.com/elaskavaia/bga-sharedcode/blob/master/sharedcode.js (slideToObjectRelative).
And more complex method moves "phantom" object on oversurface (which is neither parent of original object nor destination), code can found here (to use this your need 2 js methods for movement and classes for oversurface and oversurfacew > * from css):
https://codepen.io/VictoriaLa/pen/PojvWEV