1 #include <stdint.h>
2 #include <stdio.h>
3 #include <math.h> // round()
4 #include "ft2_keyboard.h"
5 #include "ft2_config.h"
6 #include "ft2_video.h"
7 #include "ft2_gui.h"
8 #include "ft2_pattern_ed.h"
9 #include "ft2_bmp.h"
10 #include "ft2_tables.h"
11 #include "ft2_structs.h"
12 
13 #define STAGES_BMP_WIDTH 530
14 #define NI_MAXLEVEL 30
15 
16 static const char *NI_HelpText[] =
17 {
18 	"Player 1 uses cursor keys to control movement.",
19 	"Player 2 uses the following keys:",
20 	"",
21 	"                  (W=Up)",
22 	"  (A=Left) (S=Down) (D=Right)",
23 	"",
24 	"The \"Wrap\" option controls whether it's possible to walk through",
25 	"the screen edges or not. Turn it on and use your brain to get",
26 	"the maximum out of this feature.",
27 	"The \"Surround\" option turns Nibbles into a completely different",
28 	"game. Don't change this option during play! (you'll see why)",
29 	"We wish you many hours of fun playing this game."
30 };
31 #define NIBBLES_HELP_LINES (sizeof (NI_HelpText) / sizeof (char *))
32 
33 typedef struct
34 {
35 	int16_t length;
36 	uint8_t data[8];
37 } nibblesBuffer_t;
38 
39 typedef struct
40 {
41 	uint8_t x, y;
42 } nibblesCoord_t;
43 
44 static const char nibblesCheatCode1[] = "skip", nibblesCheatCode2[] = "triton";
45 static char nibblesCheatBuffer[16];
46 
47 static const char convHexTable2[10] = { 7, 8, 9, 10, 11, 12, 13, 16, 17, 18 };
48 static const uint8_t NI_Speeds[4] = { 12, 8, 6, 4 };
49 static bool NI_EternalLives;
50 static uint8_t NI_CheatIndex, NI_CurSpeed, NI_CurTick60Hz, NI_CurSpeed60Hz, NI_Screen[51][23], NI_Level;
51 static int16_t NI_P1Dir, NI_P2Dir, NI_P1Len, NI_P2Len, NI_Number, NI_NumberX, NI_NumberY, NI_P1NoClear, NI_P2NoClear;
52 static uint16_t NI_P1Lives, NI_P2Lives;
53 static int32_t NI_P1Score, NI_P2Score;
54 static nibblesCoord_t NI_P1[256], NI_P2[256];
55 static nibblesBuffer_t nibblesBuffer[2];
56 
57 /* Non-FT2 feature: Check if either the Desktop or Buttons palette color
58 ** is so close to black that the player would have troubles seeing the walls
59 ** when playing Nibbles.
60 */
61 // ------------------------------------------------------------------------
62 // ------------------------------------------------------------------------
rgb24ToLuminosity(uint32_t rgb24)63 static uint8_t rgb24ToLuminosity(uint32_t rgb24)
64 {
65 	const uint8_t R = RGB32_R(rgb24);
66 	const uint8_t G = RGB32_G(rgb24);
67 	const uint8_t B = RGB32_B(rgb24);
68 
69 	// get highest channel value
70 	uint8_t hi = 0;
71 	if (hi < R) hi = R;
72 	if (hi < G) hi = G;
73 	if (hi < B) hi = B;
74 
75 	// get lowest channel value
76 	uint8_t lo = 255;
77 	if (lo > R) lo = R;
78 	if (lo > G) lo = G;
79 	if (lo > B) lo = B;
80 
81 	return (hi + lo) >> 1; // 0..255
82 }
83 
wallColorsAreCloseToBlack(void)84 static bool wallColorsAreCloseToBlack(void)
85 {
86 #define LUMINOSITY_THRESHOLD 4
87 
88 	const uint8_t wallColor1L = rgb24ToLuminosity(video.palette[PAL_DESKTOP]);
89 	const uint8_t wallColor2L = rgb24ToLuminosity(video.palette[PAL_BUTTONS]);
90 
91 	/* Since the rest of the wall colors are based on lower and higher
92 	** contrast values from these two primary colors, we don't really
93 	** need to check them all.
94 	*/
95 
96 	if (wallColor1L <= LUMINOSITY_THRESHOLD || wallColor2L <= LUMINOSITY_THRESHOLD)
97 		return true;
98 
99 	return false;
100 }
101 // ------------------------------------------------------------------------
102 // ------------------------------------------------------------------------
103 
redrawNibblesScreen(void)104 static void redrawNibblesScreen(void)
105 {
106 	if (!editor.NI_Play)
107 		return;
108 
109 	for (int16_t x = 0; x < 51; x++)
110 	{
111 		for (int16_t y = 0; y < 23; y++)
112 		{
113 			const int16_t xs = 152 + (x * 8);
114 			const int16_t ys = 7 + (y * 7);
115 
116 			const uint8_t c = NI_Screen[x][y];
117 			if (c < 16)
118 			{
119 				if (config.NI_Grid)
120 				{
121 					fillRect(xs + 0, ys + 0, 8 - 0, 7 - 0, PAL_BUTTON2);
122 					fillRect(xs + 1, ys + 1, 8 - 1, 7 - 1, c);
123 				}
124 				else
125 				{
126 					fillRect(xs, ys, 8, 7, c);
127 				}
128 			}
129 			else
130 			{
131 				charOut(xs + 2, ys, PAL_FORGRND, convHexTable2[NI_Number]);
132 			}
133 		}
134 	}
135 
136 	// fix wrongly rendered grid
137 	if (config.NI_Grid)
138 	{
139 		vLine(560,   7, 161, PAL_BUTTON2);
140 		hLine(152, 168, 409, PAL_BUTTON2);
141 	}
142 	else
143 	{
144 		// if we turned grid off, clear lines
145 		vLine(560,   7, 161, PAL_BCKGRND);
146 		hLine(152, 168, 409, PAL_BCKGRND);
147 	}
148 }
149 
nibblesAddBuffer(int16_t bufNum,uint8_t value)150 static void nibblesAddBuffer(int16_t bufNum, uint8_t value)
151 {
152 	nibblesBuffer_t *n = &nibblesBuffer[bufNum];
153 	if (n->length < 8)
154 	{
155 		n->data[n->length] = value;
156 		n->length++;
157 	}
158 }
159 
nibblesBufferFull(int16_t bufNum)160 static bool nibblesBufferFull(int16_t bufNum)
161 {
162 	return (nibblesBuffer[bufNum].length > 0);
163 }
164 
nibblesGetBuffer(int16_t bufNum)165 static int16_t nibblesGetBuffer(int16_t bufNum)
166 {
167 	nibblesBuffer_t *n = &nibblesBuffer[bufNum];
168 	if (n->length > 0)
169 	{
170 		const int16_t dataOut = n->data[0];
171 		memmove(&n->data[0], &n->data[1], 7);
172 		n->length--;
173 
174 		return dataOut;
175 	}
176 
177 	return -1;
178 }
179 
nibblesGetLevel(int16_t levelNum)180 static void nibblesGetLevel(int16_t levelNum)
181 {
182 	const int32_t readX = 1 + ((51+2) * (levelNum % 10));
183 	const int32_t readY = 1 + ((23+2) * (levelNum / 10));
184 
185 	const uint8_t *stagePtr = &bmp.nibblesStages[(readY * STAGES_BMP_WIDTH) + readX];
186 
187 	for (int32_t y = 0; y < 23; y++)
188 	{
189 		for (int32_t x = 0; x < 51; x++)
190 			NI_Screen[x][y] = stagePtr[x];
191 
192 		stagePtr += STAGES_BMP_WIDTH;
193 	}
194 }
195 
nibblesCreateLevel(int16_t levelNum)196 static void nibblesCreateLevel(int16_t levelNum)
197 {
198 	if (levelNum >= NI_MAXLEVEL)
199 		levelNum = NI_MAXLEVEL-1;
200 
201 	nibblesGetLevel(levelNum);
202 
203 	int32_t x1 = 0;
204 	int32_t x2 = 0;
205 	int32_t y1 = 0;
206 	int32_t y2 = 0;
207 
208 	for (int32_t y = 0; y < 23; y++)
209 	{
210 		for (int32_t x = 0; x < 51; x++)
211 		{
212 			if (NI_Screen[x][y] == 1 || NI_Screen[x][y] == 3)
213 			{
214 				const uint8_t c = NI_Screen[x][y];
215 
216 				if (c == 3)
217 				{
218 					x1 = x;
219 					y1 = y;
220 				}
221 
222 				if (c == 1)
223 				{
224 					x2 = x;
225 					y2 = y;
226 				}
227 
228 				NI_Screen[x][y] = 0;
229 			}
230 		}
231 	}
232 
233 	const int32_t readX = (51 + 2) * (levelNum % 10);
234 	const int32_t readY = (23 + 2) * (levelNum / 10);
235 
236 	NI_P1Dir = bmp.nibblesStages[(readY * 530) + (readX + 1)];
237 	NI_P2Dir = bmp.nibblesStages[(readY * 530) + (readX + 0)];
238 
239 	NI_P1Len = 5;
240 	NI_P2Len = 5;
241 	NI_P1NoClear = 0;
242 	NI_P2NoClear = 0;
243 	NI_Number = 0;
244 	nibblesBuffer[0].length = 0;
245 	nibblesBuffer[1].length = 0;
246 
247 	for (int32_t i = 0; i < 256; i++)
248 	{
249 		NI_P1[i].x = (uint8_t)x1;
250 		NI_P1[i].y = (uint8_t)y1;
251 		NI_P2[i].x = (uint8_t)x2;
252 		NI_P2[i].y = (uint8_t)y2;
253 	}
254 }
255 
nibbleWriteLevelSprite(int16_t xOut,int16_t yOut,int16_t levelNum)256 static void nibbleWriteLevelSprite(int16_t xOut, int16_t yOut, int16_t levelNum)
257 {
258 	const int32_t readX = (51 + 2) * (levelNum % 10);
259 	const int32_t readY = (23 + 2) * (levelNum / 10);
260 
261 	const uint8_t *src = (const uint8_t *)&bmp.nibblesStages[(readY * 530) + readX];
262 	uint32_t *dst = &video.frameBuffer[(yOut * SCREEN_W) + xOut];
263 
264 	for (int32_t y = 0; y < 23+2; y++)
265 	{
266 		for (int32_t x = 0; x < 51+2; x++)
267 			dst[x] = video.palette[src[x]];
268 
269 		src += 530;
270 		dst += SCREEN_W;
271 	}
272 
273 	// overwrite start position pixels
274 	video.frameBuffer[(yOut * SCREEN_W) + (xOut + 0)] = video.palette[PAL_FORGRND];
275 	video.frameBuffer[(yOut * SCREEN_W) + (xOut + 1)] = video.palette[PAL_FORGRND];
276 }
277 
highScoreTextOutClipX(uint16_t x,uint16_t y,uint8_t paletteIndex,uint8_t shadowPaletteIndex,const char * textPtr,uint16_t clipX)278 static void highScoreTextOutClipX(uint16_t x, uint16_t y, uint8_t paletteIndex, uint8_t shadowPaletteIndex, const char *textPtr, uint16_t clipX)
279 {
280 	assert(textPtr != NULL);
281 
282 	uint16_t currX = x;
283 	for (uint16_t i = 0; i < 22; i++)
284 	{
285 		const char ch = textPtr[i];
286 		if (ch == '\0')
287 			break;
288 
289 		charOutClipX(currX + 1, y + 1, shadowPaletteIndex, ch, clipX); // shadow
290 		charOutClipX(currX, y, paletteIndex, ch, clipX); // foreground
291 
292 		currX += charWidth(ch);
293 		if (currX >= clipX)
294 			break;
295 	}
296 }
297 
nibblesHighScore(void)298 void nibblesHighScore(void)
299 {
300 	if (editor.NI_Play)
301 	{
302 		okBox(0, "Nibbles message", "The highscore table is not available during play.");
303 		return;
304 	}
305 
306 	clearRect(152, 7, 409, 162);
307 
308 	bigTextOut(160, 10, PAL_FORGRND, "Fasttracker Nibbles Highscore");
309 	for (int16_t i = 0; i < 5; i++)
310 	{
311 		highScoreTextOutClipX(160, 42 + (26 * i), PAL_FORGRND, PAL_DSKTOP2, config.NI_HighScore[i].name, 160 + 70);
312 		hexOutShadow(160 + 76, 42 + (26 * i), PAL_FORGRND, PAL_DSKTOP2, config.NI_HighScore[i].score, 8);
313 		nibbleWriteLevelSprite(160 + 136, (42 - 9) + (26 * i), config.NI_HighScore[i].level);
314 
315 		highScoreTextOutClipX(360, 42 + (26 * i), PAL_FORGRND, PAL_DSKTOP2, config.NI_HighScore[i+5].name, 360 + 70);
316 		hexOutShadow(360 + 76, 42 + (26 * i), PAL_FORGRND, PAL_DSKTOP2, config.NI_HighScore[i+5].score, 8);
317 		nibbleWriteLevelSprite(360 + 136, (42 - 9) + (26 * i), config.NI_HighScore[i+5].level);
318 	}
319 }
320 
setNibbleDot(uint8_t x,uint8_t y,uint8_t c)321 static void setNibbleDot(uint8_t x, uint8_t y, uint8_t c)
322 {
323 	const uint16_t xs = 152 + (x * 8);
324 	const uint16_t ys = 7 + (y * 7);
325 
326 	if (config.NI_Grid)
327 	{
328 		fillRect(xs + 0, ys + 0, 8 - 0, 7 - 0, PAL_BUTTON2);
329 		fillRect(xs + 1, ys + 1, 8 - 1, 7 - 1, c);
330 	}
331 	else
332 	{
333 		fillRect(xs, ys, 8, 7, c);
334 	}
335 
336 	NI_Screen[x][y] = c;
337 }
338 
nibblesGenNewNumber(void)339 static void nibblesGenNewNumber(void)
340 {
341 	while (true)
342 	{
343 		const int16_t x = rand() % 51;
344 		const int16_t y = rand() % 23;
345 
346 		bool blockIsSuitable;
347 
348 		if (y < 22)
349 			blockIsSuitable = NI_Screen[x][y] == 0 && NI_Screen[x][y+1] == 0;
350 		else
351 			blockIsSuitable = NI_Screen[x][y] == 0; // FT2 bugfix: prevent look-up overflow
352 
353 		if (blockIsSuitable)
354 		{
355 			NI_Number++;
356 			NI_Screen[x][y] = (uint8_t)(16 + NI_Number);
357 			NI_NumberX = x;
358 			NI_NumberY = y;
359 
360 			const int16_t xs = 152 + (x * 8);
361 			const int16_t ys = 7 + (y * 7);
362 
363 			if (config.NI_Grid)
364 			{
365 				fillRect(xs + 0, ys + 0, 8 - 0, 7 - 0, PAL_BUTTON2);
366 				fillRect(xs + 1, ys + 1, 8 - 1, 7 - 1, PAL_BCKGRND);
367 			}
368 			else
369 			{
370 				fillRect(xs, ys, 8, 7, PAL_BCKGRND);
371 			}
372 
373 			charOut((x * 8) + 154, (y * 7) + 7, PAL_FORGRND, convHexTable2[NI_Number]);
374 			break;
375 		}
376 	}
377 }
378 
newNibblesGame(void)379 static void newNibblesGame(void)
380 {
381 	nibblesCreateLevel(NI_Level);
382 	redrawNibblesScreen();
383 
384 	setNibbleDot(NI_P1[0].x, NI_P1[0].y, 6);
385 	if (config.NI_NumPlayers == 1)
386 		setNibbleDot(NI_P2[0].x, NI_P2[0].y, 7);
387 
388 	if (!config.NI_Surround)
389 		nibblesGenNewNumber();
390 }
391 
nibblesInvalid(int16_t x,int16_t y,int16_t d)392 static bool nibblesInvalid(int16_t x, int16_t y, int16_t d)
393 {
394 	if (!config.NI_Wrap)
395 	{
396 		if ((x == 0 && d == 0) || (x == 50 && d == 2) || (y == 0 && d == 3) || (y == 22 && d == 1))
397 			return true;
398 	}
399 
400 	assert(x >= 0 && x < 51 && y >= 0 && y < 23);
401 	return (NI_Screen[x][y] >= 1 && NI_Screen[x][y] <= 15);
402 }
403 
drawScoresLives(void)404 static void drawScoresLives(void)
405 {
406 	// player 1
407 	assert(NI_P1Lives < 100);
408 	hexOutBg(89, 27, PAL_FORGRND, PAL_DESKTOP, NI_P1Score, 8);
409 	textOutFixed(131, 39, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[NI_P1Lives]);
410 
411 	// player 2
412 	assert(NI_P2Lives < 100);
413 	hexOutBg(89, 75, PAL_FORGRND, PAL_DESKTOP, NI_P2Score, 8);
414 	textOutFixed(131, 87, PAL_FORGRND, PAL_DESKTOP, dec2StrTab[NI_P2Lives]);
415 }
416 
nibblesDecLives(int16_t l1,int16_t l2)417 static void nibblesDecLives(int16_t l1, int16_t l2)
418 {
419 	char name[21+1];
420 	int16_t i, k;
421 	highScoreType *h;
422 
423 	if (!NI_EternalLives)
424 	{
425 		NI_P1Lives -= l1;
426 		NI_P2Lives -= l2;
427 	}
428 
429 	drawScoresLives();
430 
431 	if (l1+l2 == 2)
432 	{
433 		okBox(0, "Nibbles message", "Both players died!");
434 	}
435 	else
436 	{
437 		if (l2 == 0)
438 			okBox(0, "Nibbles message", "Player 1 died!");
439 		else
440 			okBox(0, "Nibbles message", "Player 2 died!");
441 	}
442 
443 	if (NI_P1Lives == 0 || NI_P2Lives == 0)
444 	{
445 		editor.NI_Play = false;
446 		okBox(0, "Nibbles message", "GAME OVER");
447 
448 		// prevent highscore table from showing overflowing level graphics
449 		if (NI_Level >= NI_MAXLEVEL)
450 			NI_Level = NI_MAXLEVEL-1;
451 
452 		if (NI_P1Score > config.NI_HighScore[9].score)
453 		{
454 			strcpy(name, "Unknown");
455 			inputBox(0, "Player 1 - Enter your name:", name, sizeof (name) - 1);
456 
457 			i = 0;
458 			while (NI_P1Score <= config.NI_HighScore[i].score)
459 				i++;
460 
461 			for (k = 8; k >= i; k--)
462 				memcpy(&config.NI_HighScore[k+1], &config.NI_HighScore[k], sizeof (highScoreType));
463 
464 			if (i == 0)
465 				okBox(0, "Nibbles message", "You've probably cheated!");
466 
467 			h = &config.NI_HighScore[i];
468 
469 			k = (int16_t)strlen(name);
470 			memset(h->name, 0, sizeof (h->name));
471 			memcpy(h->name, name, k);
472 			h->nameLen = (uint8_t)k;
473 
474 			h->score = NI_P1Score;
475 			h->level = NI_Level;
476 		}
477 
478 		if (NI_P2Score > config.NI_HighScore[9].score)
479 		{
480 			strcpy(name, "Unknown");
481 			inputBox(0, "Player 2 - Enter your name:", name, sizeof (name) - 1);
482 
483 			i = 0;
484 			while (NI_P2Score <= config.NI_HighScore[i].score)
485 				i++;
486 
487 			for (k = 8; k >= i; k--)
488 				memcpy(&config.NI_HighScore[k+1], &config.NI_HighScore[k], sizeof (highScoreType));
489 
490 			if (i == 0)
491 				okBox(0, "Nibbles message", "You've probably cheated!");
492 
493 			h = &config.NI_HighScore[i];
494 			k = (int16_t)strlen(name);
495 
496 			memset(h->name, 0, sizeof (h->name));
497 			memcpy(h->name, name, k);
498 			h->nameLen = (uint8_t)k;
499 
500 			h->score = NI_P2Score;
501 			h->level = NI_Level;
502 		}
503 
504 		nibblesHighScore();
505 	}
506 	else
507 	{
508 		editor.NI_Play = true;
509 		newNibblesGame();
510 	}
511 }
512 
nibblesEraseNumber(void)513 static void nibblesEraseNumber(void)
514 {
515 	if (!config.NI_Surround)
516 		setNibbleDot((uint8_t)NI_NumberX, (uint8_t)NI_NumberY, 0);
517 }
518 
nibblesNewLevel(void)519 static void nibblesNewLevel(void)
520 {
521 	char text[24];
522 
523 	sprintf(text, "Level %d finished!", NI_Level+1);
524 	okBox(0, "Nibbles message", text);
525 
526 	// cast to int16_t to simulate a bug in FT2
527 	NI_P1Score += 0x10000 + (int16_t)((12 - NI_CurSpeed) * 0x2000);
528 	if (config.NI_NumPlayers == 1)
529 		NI_P2Score += 0x10000;
530 
531 	NI_Level++;
532 
533 	if (NI_P1Lives < 99)
534 		NI_P1Lives++;
535 
536 	if (config.NI_NumPlayers == 1)
537 	{
538 		if (NI_P2Lives < 99)
539 			NI_P2Lives++;
540 	}
541 
542 	NI_Number = 0;
543 	nibblesCreateLevel(NI_Level);
544 	redrawNibblesScreen();
545 
546 	nibblesGenNewNumber();
547 }
548 
moveNibblesPlayers(void)549 void moveNibblesPlayers(void)
550 {
551 	if (ui.sysReqShown || --NI_CurTick60Hz != 0)
552 		return;
553 
554 	if (nibblesBufferFull(0))
555 	{
556 		switch (nibblesGetBuffer(0))
557 		{
558 			case 0: if (NI_P1Dir != 2) NI_P1Dir = 0; break;
559 			case 1: if (NI_P1Dir != 3) NI_P1Dir = 1; break;
560 			case 2: if (NI_P1Dir != 0) NI_P1Dir = 2; break;
561 			case 3: if (NI_P1Dir != 1) NI_P1Dir = 3; break;
562 			default: break;
563 		}
564 	}
565 
566 	if (nibblesBufferFull(1))
567 	{
568 		switch (nibblesGetBuffer(1))
569 		{
570 			case 0: if (NI_P2Dir != 2) NI_P2Dir = 0; break;
571 			case 1: if (NI_P2Dir != 3) NI_P2Dir = 1; break;
572 			case 2: if (NI_P2Dir != 0) NI_P2Dir = 2; break;
573 			case 3: if (NI_P2Dir != 1) NI_P2Dir = 3; break;
574 			default: break;
575 		}
576 	}
577 
578 	memmove(&NI_P1[1], &NI_P1[0], 255 * sizeof (nibblesCoord_t));
579 	if (config.NI_NumPlayers == 1)
580 		memmove(&NI_P2[1], &NI_P2[0], 255 * sizeof (nibblesCoord_t));
581 
582 	switch (NI_P1Dir)
583 	{
584 		case 0: NI_P1[0].x++; break;
585 		case 1: NI_P1[0].y--; break;
586 		case 2: NI_P1[0].x--; break;
587 		case 3: NI_P1[0].y++; break;
588 		default: break;
589 	}
590 
591 	if (config.NI_NumPlayers == 1)
592 	{
593 		switch (NI_P2Dir)
594 		{
595 			case 0: NI_P2[0].x++; break;
596 			case 1: NI_P2[0].y--; break;
597 			case 2: NI_P2[0].x--; break;
598 			case 3: NI_P2[0].y++; break;
599 			default: break;
600 		}
601 	}
602 
603 	if (NI_P1[0].x == 255) NI_P1[0].x = 50;
604 	if (NI_P2[0].x == 255) NI_P2[0].x = 50;
605 	if (NI_P1[0].y == 255) NI_P1[0].y = 22;
606 	if (NI_P2[0].y == 255) NI_P2[0].y = 22;
607 
608 	NI_P1[0].x %= 51;
609 	NI_P1[0].y %= 23;
610 	NI_P2[0].x %= 51;
611 	NI_P2[0].y %= 23;
612 
613 	if (config.NI_NumPlayers == 1)
614 	{
615 		if (nibblesInvalid(NI_P1[0].x, NI_P1[0].y, NI_P1Dir) && nibblesInvalid(NI_P2[0].x, NI_P2[0].y, NI_P2Dir))
616 		{
617 			nibblesDecLives(1, 1);
618 			goto NoMove;
619 		}
620 		else if (nibblesInvalid(NI_P1[0].x, NI_P1[0].y, NI_P1Dir))
621 		{
622 			nibblesDecLives(1, 0);
623 			goto NoMove;
624 		}
625 		else if (nibblesInvalid(NI_P2[0].x, NI_P2[0].y, NI_P2Dir))
626 		{
627 			nibblesDecLives(0, 1);
628 			goto NoMove;
629 		}
630 		else if (NI_P1[0].x == NI_P2[0].x && NI_P1[0].y == NI_P2[0].y)
631 		{
632 			nibblesDecLives(1, 1);
633 			goto NoMove;
634 		}
635 	}
636 	else
637 	{
638 		if (nibblesInvalid(NI_P1[0].x, NI_P1[0].y, NI_P1Dir))
639 		{
640 			nibblesDecLives(1, 0);
641 			goto NoMove;
642 		}
643 	}
644 
645 	int16_t j = 0;
646 	int16_t i = NI_Screen[NI_P1[0].x][NI_P1[0].y];
647 	if (i >= 16)
648 	{
649 		NI_P1Score += (i & 15) * 999 * (NI_Level + 1);
650 		nibblesEraseNumber(); j = 1;
651 		NI_P1NoClear = NI_P1Len >> 1;
652 	}
653 
654 	if (config.NI_NumPlayers == 1)
655 	{
656 		i = NI_Screen[NI_P2[0].x][NI_P2[0].y];
657 		if (i >= 16)
658 		{
659 			NI_P2Score += ((i & 15) * 999 * (NI_Level + 1));
660 			nibblesEraseNumber(); j = 1;
661 			NI_P2NoClear = NI_P2Len >> 1;
662 		}
663 	}
664 
665 	NI_P1Score -= 17;
666 	if (config.NI_NumPlayers == 1)
667 		NI_P2Score -= 17;
668 
669 	if (NI_P1Score < 0) NI_P1Score = 0;
670 	if (NI_P2Score < 0) NI_P2Score = 0;
671 
672 	if (!config.NI_Surround)
673 	{
674 		if (NI_P1NoClear > 0 && NI_P1Len < 255)
675 		{
676 			NI_P1NoClear--;
677 			NI_P1Len++;
678 		}
679 		else
680 		{
681 			setNibbleDot(NI_P1[NI_P1Len].x, NI_P1[NI_P1Len].y, 0);
682 		}
683 
684 		if (config.NI_NumPlayers == 1)
685 		{
686 			if (NI_P2NoClear > 0 && NI_P2Len < 255)
687 			{
688 				NI_P2NoClear--;
689 				NI_P2Len++;
690 			}
691 			else
692 			{
693 				setNibbleDot(NI_P2[NI_P2Len].x, NI_P2[NI_P2Len].y, 0);
694 			}
695 		}
696 	}
697 
698 	setNibbleDot(NI_P1[0].x, NI_P1[0].y, 6);
699 	if (config.NI_NumPlayers == 1)
700 		setNibbleDot(NI_P2[0].x, NI_P2[0].y, 5);
701 
702 	if (j == 1 && !config.NI_Surround)
703 	{
704 		if (NI_Number == 9)
705 		{
706 			nibblesNewLevel();
707 			NI_CurTick60Hz = NI_CurSpeed60Hz;
708 			return;
709 		}
710 
711 		nibblesGenNewNumber();
712 	}
713 
714 NoMove:
715 	NI_CurTick60Hz = NI_CurSpeed60Hz;
716 	drawScoresLives();
717 }
718 
showNibblesScreen(void)719 void showNibblesScreen(void)
720 {
721 	if (ui.extended)
722 		exitPatternEditorExtended();
723 
724 	hideTopScreen();
725 	ui.nibblesShown = true;
726 
727 	drawFramework(0,     0, 632,   3, FRAMEWORK_TYPE1);
728 	drawFramework(0,     3, 148,  49, FRAMEWORK_TYPE1);
729 	drawFramework(0,    52, 148,  49, FRAMEWORK_TYPE1);
730 	drawFramework(0,   101, 148,  72, FRAMEWORK_TYPE1);
731 	drawFramework(148,   3, 417, 170, FRAMEWORK_TYPE1);
732 	drawFramework(150,   5, 413, 166, FRAMEWORK_TYPE2);
733 	drawFramework(565,   3,  67, 170, FRAMEWORK_TYPE1);
734 
735 	bigTextOutShadow(4,   6,  PAL_FORGRND, PAL_DSKTOP2, "Player 1");
736 	bigTextOutShadow(4,  55,  PAL_FORGRND, PAL_DSKTOP2, "Player 2");
737 
738 	textOutShadow(4,  27,  PAL_FORGRND, PAL_DSKTOP2, "Score");
739 	textOutShadow(4,  75,  PAL_FORGRND, PAL_DSKTOP2, "Score");
740 	textOutShadow(4,  39,  PAL_FORGRND, PAL_DSKTOP2, "Lives");
741 	textOutShadow(4,  87,  PAL_FORGRND, PAL_DSKTOP2, "Lives");
742 	textOutShadow(18, 106, PAL_FORGRND, PAL_DSKTOP2, "1 player");
743 	textOutShadow(18, 120, PAL_FORGRND, PAL_DSKTOP2, "2 players");
744 	textOutShadow(20, 135, PAL_FORGRND, PAL_DSKTOP2, "Surround");
745 	textOutShadow(20, 148, PAL_FORGRND, PAL_DSKTOP2, "Grid");
746 	textOutShadow(20, 161, PAL_FORGRND, PAL_DSKTOP2, "Wrap");
747 	textOutShadow(80, 105, PAL_FORGRND, PAL_DSKTOP2, "Difficulty:");
748 	textOutShadow(93, 118, PAL_FORGRND, PAL_DSKTOP2, "Novice");
749 	textOutShadow(93, 132, PAL_FORGRND, PAL_DSKTOP2, "Average");
750 	textOutShadow(93, 146, PAL_FORGRND, PAL_DSKTOP2, "Pro");
751 	textOutShadow(93, 160, PAL_FORGRND, PAL_DSKTOP2, "Triton");
752 
753 	drawScoresLives();
754 
755 	blitFast(569, 7, bmp.nibblesLogo, 59, 91);
756 
757 	showPushButton(PB_NIBBLES_PLAY);
758 	showPushButton(PB_NIBBLES_HELP);
759 	showPushButton(PB_NIBBLES_HIGHS);
760 	showPushButton(PB_NIBBLES_EXIT);
761 
762 	checkBoxes[CB_NIBBLES_SURROUND].checked = config.NI_Surround ? true : false;
763 	checkBoxes[CB_NIBBLES_GRID].checked = config.NI_Grid ? true : false;
764 	checkBoxes[CB_NIBBLES_WRAP].checked = config.NI_Wrap ? true : false;
765 	showCheckBox(CB_NIBBLES_SURROUND);
766 	showCheckBox(CB_NIBBLES_GRID);
767 	showCheckBox(CB_NIBBLES_WRAP);
768 
769 	uncheckRadioButtonGroup(RB_GROUP_NIBBLES_PLAYERS);
770 	if (config.NI_NumPlayers == 0)
771 		radioButtons[RB_NIBBLES_1PLAYER].state = RADIOBUTTON_CHECKED;
772 	else
773 		radioButtons[RB_NIBBLES_2PLAYERS].state = RADIOBUTTON_CHECKED;
774 	showRadioButtonGroup(RB_GROUP_NIBBLES_PLAYERS);
775 
776 	uncheckRadioButtonGroup(RB_GROUP_NIBBLES_DIFFICULTY);
777 	switch (config.NI_Speed)
778 	{
779 		default:
780 		case 0: radioButtons[RB_NIBBLES_NOVICE].state  = RADIOBUTTON_CHECKED; break;
781 		case 1: radioButtons[RB_NIBBLES_AVERAGE].state = RADIOBUTTON_CHECKED; break;
782 		case 2: radioButtons[RB_NIBBLES_PRO].state     = RADIOBUTTON_CHECKED; break;
783 		case 3: radioButtons[RB_NIBBLES_MANIAC].state  = RADIOBUTTON_CHECKED; break;
784 	}
785 	showRadioButtonGroup(RB_GROUP_NIBBLES_DIFFICULTY);
786 }
787 
hideNibblesScreen(void)788 void hideNibblesScreen(void)
789 {
790 	hidePushButton(PB_NIBBLES_PLAY);
791 	hidePushButton(PB_NIBBLES_HELP);
792 	hidePushButton(PB_NIBBLES_HIGHS);
793 	hidePushButton(PB_NIBBLES_EXIT);
794 
795 	hideRadioButtonGroup(RB_GROUP_NIBBLES_PLAYERS);
796 	hideRadioButtonGroup(RB_GROUP_NIBBLES_DIFFICULTY);
797 
798 	hideCheckBox(CB_NIBBLES_SURROUND);
799 	hideCheckBox(CB_NIBBLES_GRID);
800 	hideCheckBox(CB_NIBBLES_WRAP);
801 
802 	ui.nibblesShown = false;
803 }
804 
exitNibblesScreen(void)805 void exitNibblesScreen(void)
806 {
807 	hideNibblesScreen();
808 	showTopScreen(true);
809 }
810 
811 // PUSH BUTTONS
812 
nibblesPlay(void)813 void nibblesPlay(void)
814 {
815 	if (editor.NI_Play)
816 	{
817 		if (okBox(2, "Nibbles request", "Restart current game of Nibbles?") != 1)
818 			return;
819 	}
820 
821 	if (config.NI_Surround && config.NI_NumPlayers == 0)
822 	{
823 		okBox(0, "Nibbles message", "Surround mode is not appropriate in one-player mode.");
824 		return;
825 	}
826 
827 	if (wallColorsAreCloseToBlack())
828 		okBox(0, "Nibbles warning", "The Desktop/Button colors are set to values that make the walls hard to see!");
829 
830 	assert(config.NI_Speed < 4);
831 	NI_CurSpeed = NI_Speeds[config.NI_Speed];
832 
833 	// adjust for 70Hz -> 60Hz frames
834 	NI_CurSpeed60Hz = (uint8_t)SCALE_VBLANK_DELTA(NI_CurSpeed);
835 	NI_CurTick60Hz = (uint8_t)SCALE_VBLANK_DELTA(NI_Speeds[2]);
836 
837 	editor.NI_Play = true;
838 	NI_P1Score = 0;
839 	NI_P2Score = 0;
840 	NI_P1Lives = 5;
841 	NI_P2Lives = 5;
842 	NI_Level = 0;
843 
844 	newNibblesGame();
845 }
846 
nibblesHelp(void)847 void nibblesHelp(void)
848 {
849 	if (editor.NI_Play)
850 	{
851 		okBox(0, "System message", "Help is not available during play.");
852 		return;
853 	}
854 
855 	clearRect(152, 7, 409, 162);
856 
857 	bigTextOut(160, 10, PAL_FORGRND, "Fasttracker Nibbles Help");
858 	for (uint16_t i = 0; i < NIBBLES_HELP_LINES; i++)
859 		textOut(160, 36 + (11 * i), PAL_BUTTONS, NI_HelpText[i]);
860 }
861 
nibblesExit(void)862 void nibblesExit(void)
863 {
864 	if (editor.NI_Play)
865 	{
866 		if (okBox(2, "System request", "Quit current game of Nibbles?") == 1)
867 		{
868 			editor.NI_Play = false;
869 			exitNibblesScreen();
870 		}
871 
872 		return;
873 	}
874 
875 	exitNibblesScreen();
876 }
877 
878 // RADIO BUTTONS
879 
nibblesSet1Player(void)880 void nibblesSet1Player(void)
881 {
882 	config.NI_NumPlayers = 0;
883 	checkRadioButton(RB_NIBBLES_1PLAYER);
884 }
885 
nibblesSet2Players(void)886 void nibblesSet2Players(void)
887 {
888 	config.NI_NumPlayers = 1;
889 	checkRadioButton(RB_NIBBLES_2PLAYERS);
890 }
891 
nibblesSetNovice(void)892 void nibblesSetNovice(void)
893 {
894 	config.NI_Speed = 0;
895 	checkRadioButton(RB_NIBBLES_NOVICE);
896 }
897 
nibblesSetAverage(void)898 void nibblesSetAverage(void)
899 {
900 	config.NI_Speed = 1;
901 	checkRadioButton(RB_NIBBLES_AVERAGE);
902 }
903 
nibblesSetPro(void)904 void nibblesSetPro(void)
905 {
906 	config.NI_Speed = 2;
907 	checkRadioButton(RB_NIBBLES_PRO);
908 }
909 
nibblesSetTriton(void)910 void nibblesSetTriton(void)
911 {
912 	config.NI_Speed = 3;
913 	checkRadioButton(RB_NIBBLES_MANIAC);
914 }
915 
916 // CHECK BOXES
917 
nibblesToggleSurround(void)918 void nibblesToggleSurround(void)
919 {
920 	config.NI_Surround ^= 1;
921 	checkBoxes[CB_NIBBLES_SURROUND].checked = config.NI_Surround ? true : false;
922 	showCheckBox(CB_NIBBLES_SURROUND);
923 }
924 
nibblesToggleGrid(void)925 void nibblesToggleGrid(void)
926 {
927 	config.NI_Grid ^= 1;
928 	checkBoxes[CB_NIBBLES_GRID].checked = config.NI_Grid ? true : false;
929 	showCheckBox(CB_NIBBLES_GRID);
930 
931 	if (editor.NI_Play)
932 		redrawNibblesScreen();
933 }
934 
nibblesToggleWrap(void)935 void nibblesToggleWrap(void)
936 {
937 	config.NI_Wrap ^= 1;
938 
939 	checkBoxes[CB_NIBBLES_WRAP].checked = config.NI_Wrap ? true : false;
940 	showCheckBox(CB_NIBBLES_WRAP);
941 }
942 
943 // GLOBAL FUNCTIONS
944 
nibblesKeyAdministrator(SDL_Scancode scancode)945 void nibblesKeyAdministrator(SDL_Scancode scancode)
946 {
947 	if (scancode == SDL_SCANCODE_ESCAPE)
948 	{
949 		if (okBox(2, "System request", "Quit current game of Nibbles?") == 1)
950 		{
951 			editor.NI_Play = false;
952 			exitNibblesScreen();
953 		}
954 
955 		return;
956 	}
957 
958 	switch (scancode)
959 	{
960 		// player 1
961 		case SDL_SCANCODE_RIGHT: nibblesAddBuffer(0, 0); break;
962 		case SDL_SCANCODE_UP:    nibblesAddBuffer(0, 1); break;
963 		case SDL_SCANCODE_LEFT:  nibblesAddBuffer(0, 2); break;
964 		case SDL_SCANCODE_DOWN:  nibblesAddBuffer(0, 3); break;
965 
966 		// player 2
967 		case SDL_SCANCODE_D: nibblesAddBuffer(1, 0); break;
968 		case SDL_SCANCODE_W: nibblesAddBuffer(1, 1); break;
969 		case SDL_SCANCODE_A: nibblesAddBuffer(1, 2); break;
970 		case SDL_SCANCODE_S: nibblesAddBuffer(1, 3); break;
971 
972 		default: break;
973 	}
974 }
975 
testNibblesCheatCodes(SDL_Keycode keycode)976 bool testNibblesCheatCodes(SDL_Keycode keycode) // not directly ported, but same cheatcodes
977 {
978 	const char *codeStringPtr;
979 	uint8_t codeStringLen;
980 
981 	// nibbles cheat codes can only be typed in while holding down left SHIFT+CTRL+ALT
982 	if (keyb.leftShiftPressed && keyb.leftCtrlPressed && keyb.leftAltPressed)
983 	{
984 		if (editor.NI_Play)
985 		{
986 			// during game: "S", "K", "I", "P" (skip to next level)
987 			codeStringPtr = nibblesCheatCode1;
988 			codeStringLen = sizeof (nibblesCheatCode1) - 1;
989 		}
990 		else
991 		{
992 			// not during game: "T", "R", "I", "T", "O", "N" (enable infinite lives)
993 			codeStringPtr = nibblesCheatCode2;
994 			codeStringLen = sizeof (nibblesCheatCode2) - 1;
995 		}
996 
997 		nibblesCheatBuffer[NI_CheatIndex] = (char)keycode;
998 		if (nibblesCheatBuffer[NI_CheatIndex] != codeStringPtr[NI_CheatIndex])
999 		{
1000 			NI_CheatIndex = 0; // start over again, one letter didn't match
1001 			return true;
1002 		}
1003 
1004 		if (++NI_CheatIndex == codeStringLen) // cheat code was successfully entered
1005 		{
1006 			NI_CheatIndex = 0;
1007 
1008 			if (editor.NI_Play)
1009 			{
1010 				nibblesNewLevel();
1011 			}
1012 			else
1013 			{
1014 				NI_EternalLives ^= 1;
1015 				if (NI_EternalLives)
1016 					okBox(0, "Triton productions declares:", "Eternal lives activated!");
1017 				else
1018 					okBox(0, "Triton productions declares:", "Eternal lives deactivated!");
1019 			}
1020 		}
1021 
1022 		return true; // SHIFT+CTRL+ALT held down, don't test other keys
1023 	}
1024 
1025 	return false; // SHIFT+CTRL+ALT not held down, test other keys
1026 }
1027 
pbNibbles(void)1028 void pbNibbles(void)
1029 {
1030 	showNibblesScreen();
1031 }
1032