-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathMenuModule.h
401 lines (336 loc) · 9.98 KB
/
MenuModule.h
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
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
/**
* MenuModule.h - Everything concerning the menu and its logic
*/
int menuDirection = AXIS_IDLE;
String menuItems[] = {"Start Game", "Settings", "Highscores", "About"};
int menuItemsCount = 4;
int highscoreItemsCount = 3;
volatile int selectedItem = 0;
volatile bool shouldRedrawMenu = false;
// settings menu
String settingsItems[] = {"Name:", "Difficulty:", "Contrast:", "Brightness:", "Matr. Light:", "Back to Menu"};
const int settingsItemsCount = 6;
volatile bool isEditingSetting = false;
volatile bool nameEditSetup = true;
byte index = 0;
volatile int nameEditState = SYSTEM_STATE_NAME_EDIT_LOCKED;
bool shouldRedrawNameEdit = true;
// i chose to hold the brightness settings in a short array as they're all numbers (except the name) in order to write more generic control functions
// consult the settings constants section for the corresponding indices
struct Settings {
char name[6];
short difficulty;
short brightnessArray[3];
} systemSettings;
// high score menu
struct PlayerScore {
short score;
char name[6];
} currentPlayer;
PlayerScore scores[3];
// about menu
String aboutText[] = {"Nicoleta Ciausu", "git.io/JMQEj"};
/**
* Interprets user joystick input in the way needed for the menu.
* Only read off one axis, and disable scrolling-through-items by holding the joystick.
*/
short getMenuUserInput(int axis) {
short reading = axis;
if (reading == AXIS_IDLE) {
didReadJoystick = false;
return reading;
}
if (!didReadJoystick) {
didReadJoystick = true;
return reading;
}
// do nothing - we don't want to cycle through the menu on joystick hold
return AXIS_IDLE;
}
void saveSettings() {
EEPROM.put(EEPROM_SETTINGS_MEMORY_LOCATION, systemSettings);
}
/**
* Figures out if the currently acheived score is a high score and if so saves it to the eeprom.
*/
void saveScore() {
bool shouldUpdateEEPROM = false;
for (int i = 0; i < 3; i++) {
if (currentPlayer.score > scores[i].score) {
strcpy(scores[i].name, systemSettings.name);
scores[i].score = currentPlayer.score;
shouldUpdateEEPROM = true;
break;
}
}
if (shouldUpdateEEPROM) {
EEPROM.put(EEPROM_HIGHSCORES_MEMORY_LOCATION, scores);
}
}
// change system state depending on selected setting to edit
void doSettingsAction() {
if (isEditingSetting) {
isEditingSetting = false;
saveSettings();
return;
}
switch (selectedItem) {
case settingsItemsCount - 1:
selectedItem = 0;
setMatrixImage(menuMatrixSymbol);
systemState = SYSTEM_STATE_MENU;
return;
case 0:
systemState = SYSTEM_STATE_NAME_EDIT;
if (nameEditState == SYSTEM_STATE_NAME_EDIT_UNLOCKED) {
nameEditState = SYSTEM_STATE_NAME_EDIT_LOCKED;
} else {
nameEditState = SYSTEM_STATE_NAME_EDIT_UNLOCKED;
}
shouldRedrawNameEdit = true;
return;
default:
isEditingSetting = true;
return;
}
}
/**
* Updates user menu selection, making sure not to go out of the item count bounds.
*/
void updateLcdMenu(short userInput, short itemCount) {
if (userInput == AXIS_NEGATIVE && selectedItem < (itemCount - 1)) {
shouldRedrawMenu = true;
selectedItem += 1;
}
if (userInput == AXIS_POSITIVE && selectedItem > 0) {
shouldRedrawMenu = true;
selectedItem -= 1;
}
}
/**
* Does a bit of math which figures out how to center text on a lcd.
*/
byte getCenteredTextPosition(String text) {
return (LCD_CHARACTER_LENGTH - text.length()) / 2 + 1;
}
/**
* Draws the menu according the current system state.
*/
void drawMenu(String title, bool showCaret, String option, short optionsCount, bool isEditingSetting = false) {
lcd.clear();
short titleCursorPos = getCenteredTextPosition(title);
lcd.setCursor(titleCursorPos, 0);
lcd.print(title);
lcd.setCursor(0, 1);
if (showCaret) {
lcd.print(">");
}
lcd.print(option);
if (isEditingSetting) {
lcd.setCursor(SCROLLBAR_TEXT_POS);
lcd.write(byte(ENTER_SYMBOL));
shouldRedrawMenu = false;
return;
}
short scrollbarCharacter;
if (selectedItem == 0) {
scrollbarCharacter = DOWN_SYMBOL;
} else if (selectedItem == optionsCount - 1) {
scrollbarCharacter = UP_SYMBOL;
} else {
scrollbarCharacter = BOTH_SYMBOL;
}
lcd.setCursor(SCROLLBAR_TEXT_POS);
lcd.write(byte(scrollbarCharacter));
shouldRedrawMenu = false;
}
/**
* Loop for the SYSTEM_STATE_MENU state.
* Redraw on LCD only on user action.
*/
void menuLoop() {
short userInput = getMenuUserInput(joystickY);
if (userInput) {
updateLcdMenu(userInput, menuItemsCount);
}
if (shouldRedrawMenu) {
drawMenu(MENU_TITLE_GAME, true, menuItems[selectedItem], menuItemsCount);
}
}
/**
* Changes selected letter in SYSTEM_STATE_EDIT name
*/
void changeSelection(short input) {
// keep in bounds, no wrapping back to the beginning of the name
if (input == AXIS_POSITIVE && index < NAME_LENGTH) {
index += 1;
}
if (input == AXIS_NEGATIVE && index > 0) {
index -= 1;
}
shouldRedrawNameEdit = true;
}
void scrollThroughLetter(short input) {
if (input == AXIS_POSITIVE && systemSettings.name[index] < 'Z') {
systemSettings.name[index] += 1;
}
if (input == AXIS_NEGATIVE && systemSettings.name[index] > 'A') {
systemSettings.name[index] -= 1;
}
shouldRedrawNameEdit = true;
}
void drawNameEdit() {
lcd.clear();
lcd.setCursor(NAME_TEXT_POS);
lcd.print(systemSettings.name);
lcd.setCursor(SAVE_TEXT_POS);
lcd.print(SAVE_TEXT);
// show selection: either an editable character or the Save button
if (index == NAME_LENGTH) {
lcd.setCursor(SAVE_CURSOR_POS);
} else {
lcd.setCursor(index, 1);
}
if (nameEditState == SYSTEM_STATE_NAME_EDIT_UNLOCKED) {
lcd.print(SELECTION_CARET);
} else {
lcd.write(byte(TRIANGLE_SYMBOL));
}
shouldRedrawNameEdit = false;
}
void nameEditLoop() {
short userInput = getMenuUserInput(joystickX);
if (userInput) {
if (nameEditState == SYSTEM_STATE_NAME_EDIT_UNLOCKED) {
changeSelection(userInput);
} else {
scrollThroughLetter(userInput);
}
}
if (shouldRedrawNameEdit) {
drawNameEdit();
}
}
void aboutLoop() {
if (shouldRedrawMenu) {
lcd.clear();
lcd.setCursor(ABOUT_NAME_TEXT_POS);
lcd.print(aboutText[0]);
lcd.setCursor(ABOUT_GITHUB_TEXT_POS);
lcd.print(aboutText[1]);
lcd.setCursor(SCROLLBAR_TEXT_POS);
lcd.write(byte(CLICK_SYMBOL));
shouldRedrawMenu = false;
}
}
String getScoreText(short selectedItem) {
String str = String(selectedItem + 1);
str.concat(" ");
str.concat(String(scores[selectedItem].name));
str.concat(" ");
str.concat(String(scores[selectedItem].score));
return str;
}
void highscoresLoop() {
short userInput = getMenuUserInput(joystickY);
if (userInput) {
updateLcdMenu(userInput, highscoreItemsCount);
}
if (shouldRedrawMenu) {
drawMenu(MENU_TITLE_HIGHSCORES, false, getScoreText(selectedItem), sizeof(scores) / sizeof(PlayerScore));
}
}
/**
* Composes the setting text with its value and returns it for displaying.
*/
String getSettingText(short item) {
String message = "";
message.concat(settingsItems[item]);
switch (item) {
case SETTING_NAME:
message.concat(systemSettings.name);
break;
case SETTING_DIFFICULTY:
message.concat(systemSettings.difficulty);
break;
case SETTING_BACK_TO_MENU:
break;
default:
// first 2 settings aren't related to brightness array; to avoid padding in the vector (2 0 positions) i subtract from the selection index
// the value 2 - so the first brightness setting, which is setting number 3, has a corresponding value of 1 in the brightness array.
message.concat(systemSettings.brightnessArray[item - SETTING_NUMBERED_ITEM_PADDING]);
break;
}
return message;
}
/**
* Applies settings values to the actual hardware.
*/
void applySettings() {
analogWrite(LCD_V0, systemSettings.brightnessArray[0] * SETTINGS_ADJUSTMENT_INCREMENT);
analogWrite(LCD_BACKLIGHT, systemSettings.brightnessArray[1] * SETTINGS_ADJUSTMENT_INCREMENT);
matrix.setIntensity(0, systemSettings.brightnessArray[2]);
}
/**
* For the selected setting, if user has interacted with it, update its value, and make sure not to exit bounds.
*/
void updateSetting(short userInput) {
short lowerBound, upperBound, currentValue;
// all numbered settings are bound from 1-9 except for difficulty which is 1-3. handling that here
if (selectedItem == SETTING_DIFFICULTY) {
lowerBound = SETTINGS_MIN_DIFFICULTY;
upperBound = SETTINGS_MAX_DIFFICULTY;
currentValue = systemSettings.difficulty;
} else {
lowerBound = SETTINGS_MIN_ADJUSTMENT_VALUE;
upperBound = SETTINGS_MAX_ADJUSTMENT_VALUE;
currentValue = systemSettings.brightnessArray[selectedItem - 2];
}
// don't escape bounds
if (userInput == AXIS_NEGATIVE && currentValue < upperBound) {
shouldRedrawMenu = true;
currentValue += 1;
}
if (userInput == AXIS_POSITIVE && currentValue > lowerBound) {
shouldRedrawMenu = true;
currentValue -= 1;
}
if (selectedItem == SETTING_DIFFICULTY) {
systemSettings.difficulty = currentValue;
} else {
systemSettings.brightnessArray[selectedItem - 2] = currentValue;
}
// make the settings take effect immediately
applySettings();
}
/**
* Loop for SYSTEM_STATE_MENU_SETTINGS state.
*/
void settingsLoop() {
short userInput = getMenuUserInput(joystickY);
if (userInput) {
if (isEditingSetting) {
// if adjusting setting, update its value
updateSetting(userInput);
} else {
// if scrolling through settings, change it
updateLcdMenu(userInput, settingsItemsCount);
}
}
if (shouldRedrawMenu) {
drawMenu(MENU_TITLE_SETTINGS, true, getSettingText(selectedItem), settingsItemsCount, isEditingSetting);
}
}
/**
* Game splash function, draw the welcome text and wait.
*/
void doSplash() {
setMatrixImage(happyMatrixSymbol);
lcd.clear();
lcd.setCursor(SPLASH_TEXT_POS);
lcd.print(SPLASH_TEXT);
delay(SPLASH_TIME_MS);
systemState = SYSTEM_STATE_MENU;
shouldRedrawMenu = true;
setMatrixImage(menuMatrixSymbol);
}