-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
310 lines (283 loc) · 11.4 KB
/
Copy pathapp.js
File metadata and controls
310 lines (283 loc) · 11.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
/**
* Switch the first letter of the given string to uppercase.
* @param {String} s String to process.
* @returns {String} A new string with first character in upper case.
*/
const capitalized = function (s) {
if (s.length === 0) return '';
if (s.length === 1) return s.charAt(0).toUpperCase();
return s.charAt(0).toUpperCase() + s.slice(1);
}
/**
* Replace special characters in the given string
* by the provided replacement string.
* @param {String} s String to process.
* @param {String} replacement Replacement string
* @returns {String} A new string
*/
const replaceSpecialCharacters = function (s, replacement) {
return s.replace(/[&\/\\#, +()$~%.'":*?<>{}]/g, replacement);
}
/**
* Convert a String to kebabCase (e.g. "kebab-case").
* @param {String} s String to process
* @returns {String} The processed string
*/
const kebabCased = function (s) {
return replaceSpecialCharacters(s, '-').toLowerCase();
}
/**
* Convert a String to snakeCase (eg. "snake_case")
* @param {String} s String to process
* @param {Boolean} uppercased True to output an uppercased string (UPPER_SNAKE_CASE)
* @returns {String} The processed string
*/
const snakeCased = function (s, uppercased) {
let newString = replaceSpecialCharacters(s, '_');
return uppercased ? newString.toUpperCase() : newString.toLowerCase();
}
/**
* Shuffles an array.
* (from : https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array)
* @param {Array} array The array to shuffle.
* @returns {Array} The array, shuffled.
*/
const shuffle = function(array) {
let currentIndex = array.length, randomIndex;
// While there remain elements to shuffle.
while (currentIndex != 0) {
// Pick a remaining element.
randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex--;
// And swap it with the current element.
[array[currentIndex], array[randomIndex]] = [
array[randomIndex], array[currentIndex]];
}
return array;
}
/**
* Shuffles a given Object's keys and return them in an Array.
* @param {Object} object The object whose keys we want to shuffle.
* @returns {Array} Object keys, shuffled.
*/
const shuffledKeys = function(object) {
// Randomly pick a character's key
const keys = Object.keys(object);
return shuffle(keys);
}
/**
* Main application
* Needs to be initialized using app.init().
*/
const app = {
debug: false, // Allows debug info to appear on top of cards when hovering
options: [
{
caption: 'Player\'s color',
type: 'radio',
choices: [
{ caption: 'Blue', callback: () => { app.setPlayerColor('blue'); }, checked: true },
{ caption: 'Red', callback: () => { app.setPlayerColor('red'); } },
{ caption: 'Green', callback: () => { app.setPlayerColor('green'); } },
{ caption: 'Purple', callback: () => { app.setPlayerColor('purple'); } }
]
}
],
playerColor: '',
cardsData: {},
cardsSpriteColumns: 8,
cardsSpriteRows: 3,
/**
* Generates the options panel using the app.options info
*/
initOptions() {
// Filling the options pannel
const optionsPanelElem = document.getElementById('options-panel');
for (appOption of app.options) {
// Main option container
const optionItemElem = document.createElement('li');
optionItemElem.className = 'options-item';
optionsPanelElem.appendChild(optionItemElem);
// Option caption
const optionItemCaptionElem = document.createElement('p');
optionItemElem.appendChild(optionItemCaptionElem);
optionItemCaptionElem.innerText = appOption.caption;
optionItemCaptionElem.className = 'options-item__title';
// Option radio switch container
const optionItemRadioSwitchElem = document.createElement('div');
optionItemElem.appendChild(optionItemRadioSwitchElem);
optionItemRadioSwitchElem.className = 'radio-switch';
optionItemRadioSwitchElem.style.setProperty('--switch-positions', appOption.choices.length);
// Selection feedback element
const optionSelectionElem = document.createElement('div');
optionItemRadioSwitchElem.appendChild(optionSelectionElem);
optionSelectionElem.className = 'radio-switch__selection';
// Adding one input per choice
appOption.choices.forEach((optionChoice, index) => {
// Radio input
const choiceInputElem = document.createElement('input');
choiceInputElem.setAttribute('type', 'radio');
choiceInputElem.setAttribute('id', kebabCased(`${appOption.caption} ${optionChoice.caption}`));
choiceInputElem.setAttribute('name', kebabCased(appOption.caption));
optionItemRadioSwitchElem.appendChild(choiceInputElem);
choiceInputElem.className = 'radio-switch__input'
// Radio label
const choiceLabelElem = document.createElement('label');
choiceLabelElem.setAttribute('for', kebabCased(`${appOption.caption} ${optionChoice.caption}`));
optionItemRadioSwitchElem.appendChild(choiceLabelElem);
choiceLabelElem.className = 'radio-switch__label';
choiceLabelElem.innerText = optionChoice.caption;
// Input click listener
choiceInputElem.addEventListener('click', () => {
optionItemRadioSwitchElem.style.setProperty('--current-index', index);
// Callback
optionChoice.callback();
});
// If option is checked
if (optionChoice.checked) {
choiceInputElem.setAttribute('checked', true);
optionChoice.callback(); // we trigger the callback at startup
}
});
};
// Options button callback
document.getElementById('options__button').addEventListener('click', (event) => {
optionsPanelElem.classList.toggle('options-panel--hidden');
event.target.classList.toggle('options__button--activated');
})
},
/**
* Sets the current player's color and update the whole game style.
* @param {String} color Color
*/
setPlayerColor(color) {
app.playerColor = color;
app.updateGameStyle();
},
/**
* Updates the stylesheet based on app properties.
*/
updateGameStyle() {
const rootElem = document.querySelector(':root');
// Set the sprite columns and rows
rootElem.style.setProperty('--cards-sprite-rows', app.cardsSpriteRows);
rootElem.style.setProperty('--cards-sprite-columns', app.cardsSpriteColumns);
// Set the player's color variable
rootElem.style.setProperty('--player-color-raw', `var(--player-color-raw-${app.playerColor})`);
},
/**
* Randomly pick a secret card and add it to the board.
*/
drawSecretCard() {
const boardElem = document.getElementById('board');
const characterKey = shuffledKeys(app.cardsData.characters)[0];
// Create the card
boardElem.appendChild(app.createCard(characterKey, true));
},
/**
* Creates the full card HTML element.
* @param {String} characterKey Character object key (the name of the character in lowercase)
* @param {Boolean} isSecretCard True if the card to create is the secret card.
* @param {Number} index Index of the card on the board.
* @returns {HTMLElement} The card container element.
*/
createCard(characterKey, isSecretCard, index=null) {
// DEBUG - Card details (on hover)
const addDetails = (cardElem) => {
const cardFrontDetailsElem = document.createElement('div');
cardElem.appendChild(cardFrontDetailsElem);
cardFrontDetailsElem.className = 'card-front-details';
characterSpecs = app.cardsData.characters[characterKey]['specs'];
for (spec of app.cardsData.specsDisplayOrder) {
specsOptions = app.cardsData.specs[spec];
const cardBackSpecLineElem = document.createElement('p');
cardFrontDetailsElem.appendChild(cardBackSpecLineElem);
// Details line
cardBackSpecLineElem.innerText = `${capitalized(spec)} = `;
// Checking that the character has the spec, otherwise give it a 0 value (default)
const charSpecValue = characterSpecs.hasOwnProperty(spec) ? characterSpecs[spec] : 0;
// Checking that the character's spec value is within the spec values range
if (charSpecValue <= specsOptions.length) {
cardBackSpecLineElem.innerText += `${specsOptions[charSpecValue]}`;
} else {
cardBackSpecLineElem.innerText += 'UNKNOWN';
}
}
// Show/hide cards details when hovering
cardElem.addEventListener('mouseover', () => {
cardFrontDetailsElem.style.display = 'block';
})
cardElem.addEventListener('mouseleave', () => {
cardFrontDetailsElem.style.display = 'none';
})
}
// Card container
const cardContainerElem = document.createElement('div');
cardContainerElem.className = 'card-container';
cardContainerElem.style.cursor = 'auto';
cardContainerElem.classList.add( isSecretCard ? 'card-container--secret' : 'card-container--board');
// Car inner (for the flipping effect)
const cardElem = document.createElement('div');
cardContainerElem.appendChild(cardElem);
cardElem.className = 'card card--inactive';
// Trigger the 'show' animation at start and then allows player to click on the cards
const flipTimeout = isSecretCard ? 1000 : 300 + (index * 10);
setTimeout(() => {
cardElem.classList.remove('card--inactive');
cardContainerElem.style.cursor = 'pointer';
// When cardElem is clicked, toggle the cardInnerElem active class
cardContainerElem.addEventListener('click', () => {
cardElem.classList.toggle('card--inactive');
})
}, flipTimeout);
// Card front (character face)
const cardFrontElem = document.createElement('div');
cardElem.appendChild(cardFrontElem);
cardFrontElem.className = 'card-front';
[column, row] = app.cardsData.characters[characterKey]['spriteCoordinates'];
cardFrontElem.style.backgroundPosition = `${100 * column / (app.cardsSpriteColumns - 1)}% ${100 * row / (app.cardsSpriteRows - 1)}%`;
// Card front title (character name)
const cardTitleElem = document.createElement('div');
cardFrontElem.appendChild(cardTitleElem);
cardTitleElem.className = 'card-front__title';
cardTitleElem.innerText = capitalized(characterKey);
// Card details (debug-only)
if (app.debug) addDetails(cardFrontElem);
// Card back
const cardBackElem = document.createElement('div');
cardElem.appendChild(cardBackElem);
cardBackElem.className = 'card-back';
if (!isSecretCard) cardBackElem.classList.add(`card-back--${app.playerColor}`);
const cardBackImageElem = document.createElement('div');
cardBackElem.appendChild(cardBackImageElem);
cardBackImageElem.className = 'card-back__image';
return cardContainerElem;
},
/**
* Clears and redraws the game board.
*/
redrawBoard() {
// Drawing the board
const boardElem = document.getElementById('board');
boardElem.innerHTML = '';
// Adding the cards
shuffledKeys(app.cardsData.characters).forEach((charKey, index) => {
boardElem.appendChild(app.createCard(charKey, false, index));
});
},
/**
* Init the game.
*/
async init() {
app.initOptions();
// Read cards data
app.cardsData = await fetch("./data/cards.json").then(response => response.json());
// Edit the CSS depending on the app options
app.updateGameStyle();
// Redraw board
app.redrawBoard();
// Pick the player's secret card
app.drawSecretCard();
}
}
document.addEventListener('DOMContentLoaded', app.init);