1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #include "bladerunner/dialogue_menu.h"
24 
25 #include "bladerunner/bladerunner.h"
26 #include "bladerunner/font.h"
27 #include "bladerunner/game_constants.h"
28 #include "bladerunner/mouse.h"
29 #include "bladerunner/savefile.h"
30 #include "bladerunner/settings.h"
31 #include "bladerunner/shape.h"
32 #include "bladerunner/text_resource.h"
33 
34 #include "common/debug.h"
35 #include "common/rect.h"
36 #include "common/util.h"
37 
38 namespace BladeRunner {
39 
DialogueMenu(BladeRunnerEngine * vm)40 DialogueMenu::DialogueMenu(BladeRunnerEngine *vm) {
41 	_vm = vm;
42 	reset();
43 	_textResource = new TextResource(_vm);
44 	_shapes = new Shapes(_vm);
45 
46 	_screenX = 0;
47 	_screenY = 0;
48 	_maxItemWidth = 0;
49 	_fadeInItemIndex = 0;
50 }
51 
~DialogueMenu()52 DialogueMenu::~DialogueMenu() {
53 	delete _textResource;
54 	delete _shapes;
55 }
56 
loadResources()57 bool DialogueMenu::loadResources() {
58 	bool r = _textResource->open("DLGMENU");
59 	if (!r) {
60 		error("Failed to load dialogue menu text");
61 	}
62 	r = _shapes->load("DIALOG.SHP");
63 	if (!r) {
64 		error("Failed to load dialogue menu shapes");
65 	}
66 	return r;
67 }
68 
show()69 bool DialogueMenu::show() {
70 	int x, y;
71 
72 	_vm->_mouse->getXY(&x, &y);
73 
74 	return showAt(x, y);
75 }
76 
showAt(int x,int y)77 bool DialogueMenu::showAt(int x, int y) {
78 	if (_isVisible) {
79 		return false;
80 	}
81 
82 	_isVisible = true;
83 	_selectedItemIndex = 0;
84 	_centerX = x;
85 	_centerY = y;
86 	calculatePosition(x, y);
87 
88 	return true;
89 }
90 
hide()91 bool DialogueMenu::hide() {
92 	_waitingForInput = false;
93 	if (!_isVisible) {
94 		return false;
95 	}
96 
97 	_isVisible = false;
98 	return true;
99 }
100 
clearList()101 bool DialogueMenu::clearList() {
102 	_selectedItemIndex = -1;
103 	_listSize = 0;
104 	return true;
105 }
106 
addToList(int answer,bool done,int priorityPolite,int priorityNormal,int prioritySurly)107 bool DialogueMenu::addToList(int answer, bool done, int priorityPolite, int priorityNormal, int prioritySurly) {
108 	if (_listSize >= kMaxItems) {
109 		return false;
110 	}
111 	if (getAnswerIndex(answer) != -1) {
112 		return false;
113 	}
114 
115 #if BLADERUNNER_ORIGINAL_BUGS
116 // Original uses incorrect spelling for entry id 1020: DRAGONFLY JEWERLY
117 	const Common::String &text = _textResource->getText(answer);
118 #else
119 // fix spelling or entry id 1020 to DRAGONFLY JEWELRY in English version
120 	const char *answerTextCP = _textResource->getText(answer);
121 	if (_vm->_language == Common::EN_ANY && answer == 1020 && strcmp(answerTextCP, "DRAGONFLY JEWERLY") == 0) {
122 		answerTextCP = "DRAGONFLY JEWELRY";
123 	}
124 	const Common::String &text = answerTextCP;
125 #endif // BLADERUNNER_ORIGINAL_BUGS
126 	if (text.empty() || text.size() >= 50) {
127 		return false;
128 	}
129 
130 	int index = _listSize++;
131 	_items[index].text = text;
132 	_items[index].answerValue = answer;
133 	_items[index].colorIntensity = 0;
134 	_items[index].isDone = done;
135 	_items[index].priorityPolite = priorityPolite;
136 	_items[index].priorityNormal = priorityNormal;
137 	_items[index].prioritySurly  = prioritySurly;
138 
139 	// CHECK(madmoose): BLADE.EXE calls this needlessly
140 	// calculatePosition();
141 
142 	return true;
143 }
144 
145 /**
146 * Aux function - used in cut content mode to re-use some NeverRepeatOnceSelected dialogue options for different characters
147 */
clearNeverRepeatWasSelectedFlag(int answer)148 bool DialogueMenu::clearNeverRepeatWasSelectedFlag(int answer) {
149 	int foundIndex = -1;
150 	for (int i = 0; i != _neverRepeatListSize; ++i) {
151 		if (answer == _neverRepeatValues[i]) {
152 			foundIndex = i;
153 			break;
154 		}
155 	}
156 
157 	if (foundIndex >= 0 && _neverRepeatWasSelected[foundIndex]) {
158 		_neverRepeatWasSelected[foundIndex] = false;
159 		return true;
160 	}
161 	return false;
162 }
163 
addToListNeverRepeatOnceSelected(int answer,int priorityPolite,int priorityNormal,int prioritySurly)164 bool DialogueMenu::addToListNeverRepeatOnceSelected(int answer, int priorityPolite, int priorityNormal, int prioritySurly) {
165 	int foundIndex = -1;
166 	for (int i = 0; i != _neverRepeatListSize; ++i) {
167 		if (answer == _neverRepeatValues[i]) {
168 			foundIndex = i;
169 			break;
170 		}
171 	}
172 
173 	if (foundIndex >= 0 && _neverRepeatWasSelected[foundIndex]) {
174 		return true;
175 	}
176 
177 	if (foundIndex == -1) {
178 		_neverRepeatValues[_neverRepeatListSize] = answer;
179 		_neverRepeatWasSelected[_neverRepeatListSize] = false;
180 		++_neverRepeatListSize;
181 
182 		assert(_neverRepeatListSize <= 100);
183 	}
184 
185 	return addToList(answer, false, priorityPolite, priorityNormal, prioritySurly);
186 }
187 
removeFromList(int answer)188 bool DialogueMenu::removeFromList(int answer) {
189 	int index = getAnswerIndex(answer);
190 	if (index < 0) {
191 		return false;
192 	}
193 	if (index < _listSize - 1) {
194 		for (int i = index; i < _listSize; ++i) {
195 			_items[index] = _items[index + 1];
196 		}
197 	}
198 	--_listSize;
199 
200 	calculatePosition();
201 	return true;
202 }
203 
queryInput()204 int DialogueMenu::queryInput() {
205 	if (!_isVisible || _listSize == 0) {
206 		return -1;
207 	}
208 
209 	int answer = -1;
210 	if (_listSize == 1) {
211 		_selectedItemIndex = 0;
212 		answer = _items[_selectedItemIndex].answerValue;
213 	} else if (_listSize == 2) {
214 #if BLADERUNNER_ORIGINAL_BUGS
215 		if (_items[0].isDone) {
216 			_selectedItemIndex = 1;
217 			answer = _items[_selectedItemIndex].answerValue;
218 		} else if (_items[1].isDone) {
219 			_selectedItemIndex = 0;
220 			answer = _items[_selectedItemIndex].answerValue;
221 		}
222 #else
223 		// In User Choice mode, avoid auto-select of last option
224 		// In this mode, player should still have agency to skip the last (non- "DONE")
225 		// question instead of automatically asking it because the other remaining option is "DONE"
226 		if (_vm->_settings->getPlayerAgenda() != kPlayerAgendaUserChoice) {
227 			if (_items[0].isDone) {
228 				_selectedItemIndex = 1;
229 				answer = _items[_selectedItemIndex].answerValue;
230 			} else if (_items[1].isDone) {
231 				_selectedItemIndex = 0;
232 				answer = _items[_selectedItemIndex].answerValue;
233 			}
234 		}
235 #endif // BLADERUNNER_ORIGINAL_BUGS
236 	}
237 
238 	if (answer == -1) {
239 		int agenda = _vm->_settings->getPlayerAgenda();
240 		if (agenda == kPlayerAgendaUserChoice) {
241 			_waitingForInput = true;
242 			do {
243 				while (!_vm->playerHasControl()) {
244 					_vm->playerGainsControl();
245 				}
246 
247 				while (_vm->_mouse->isDisabled()) {
248 					_vm->_mouse->enable();
249 				}
250 
251 				_vm->gameTick();
252 			} while (_vm->_gameIsRunning && _waitingForInput);
253 		} else if (agenda == kPlayerAgendaErratic) {
254 			int tries = 0;
255 			bool searching = true;
256 			int i;
257 			do {
258 				i = _vm->_rnd.getRandomNumber(_listSize - 1);
259 				if (!_items[i].isDone) {
260 					searching = false;
261 				} else if (++tries > 1000) {
262 					searching = false;
263 					i = 0;
264 				}
265 			} while (searching);
266 			_selectedItemIndex = i;
267 		} else {
268 			int priority = -1;
269 			for (int i = 0; i < _listSize; ++i) {
270 				int priorityCompare = -1;
271 				if (agenda == kPlayerAgendaPolite) {
272 					priorityCompare = _items[i].priorityPolite;
273 				} else if (agenda == kPlayerAgendaNormal) {
274 					priorityCompare = _items[i].priorityNormal;
275 				} else if (agenda == kPlayerAgendaSurly) {
276 					priorityCompare = _items[i].prioritySurly;
277 				}
278 				if (priority < priorityCompare) {
279 					priority = priorityCompare;
280 					_selectedItemIndex = i;
281 				}
282 			}
283 		}
284 	}
285 
286 	answer = _items[_selectedItemIndex].answerValue;
287 	for (int i = 0; i != _neverRepeatListSize; ++i) {
288 		if (answer == _neverRepeatValues[i]) {
289 			_neverRepeatWasSelected[i] = true;
290 			break;
291 		}
292 	}
293 
294 	// debug("DM Query Input: %d %s", answer, _items[_selectedItemIndex].text.c_str());
295 
296 	return answer;
297 }
298 
listSize() const299 int DialogueMenu::listSize() const {
300 	return _listSize;
301 }
302 
isVisible() const303 bool DialogueMenu::isVisible() const {
304 	return _isVisible;
305 }
306 
isOpen() const307 bool DialogueMenu::isOpen() const {
308 	return _isVisible || _waitingForInput;
309 }
310 
tick(int x,int y)311 void DialogueMenu::tick(int x, int y) {
312 	if (!_isVisible || _listSize == 0) {
313 		return;
314 	}
315 
316 	int line = (y - (_screenY + kBorderSize)) / kLineHeight;
317 	line = CLIP(line, 0, _listSize - 1);
318 
319 	_selectedItemIndex = line;
320 }
321 
draw(Graphics::Surface & s)322 void DialogueMenu::draw(Graphics::Surface &s) {
323 	if (!_isVisible || _listSize == 0) {
324 		return;
325 	}
326 
327 	int fadeInItemIndex = _fadeInItemIndex;
328 	if (fadeInItemIndex < listSize()) {
329 		++_fadeInItemIndex;
330 	}
331 
332 	for (int i = 0; i != _listSize; ++i) {
333 		int targetColorIntensity = 0;
334 		if (i == _selectedItemIndex) {
335 			targetColorIntensity = 31;
336 		} else {
337 			targetColorIntensity = 16;
338 		}
339 		if (i > fadeInItemIndex) {
340 			targetColorIntensity = 0;
341 		}
342 
343 		if (_items[i].colorIntensity < targetColorIntensity) {
344 			_items[i].colorIntensity += 4;
345 			if (_items[i].colorIntensity > targetColorIntensity) {
346 				_items[i].colorIntensity = targetColorIntensity;
347 			}
348 		} else if (_items[i].colorIntensity > targetColorIntensity) {
349 			_items[i].colorIntensity -= 2;
350 			if (_items[i].colorIntensity < targetColorIntensity) {
351 				_items[i].colorIntensity = targetColorIntensity;
352 			}
353 		}
354 	}
355 
356 	const int x1 = _screenX;
357 	const int y1 = _screenY;
358 	const int x2 = _screenX + kBorderSize + _maxItemWidth;
359 	const int y2 = _screenY + kBorderSize + _listSize * kLineHeight;
360 
361 	darkenRect(s, x1 + 8, y1 + 8, x2 + 2, y2 + 2);
362 
363 	int x = x1 + kBorderSize;
364 	int y = y1 + kBorderSize;
365 
366 	Common::Point mouse = _vm->getMousePos();
367 	if (mouse.x >= x && mouse.x < x2) {
368 		s.vLine(mouse.x, y1 + 8, y2 + 2, s.format.RGBToColor(64, 64, 64));
369 	}
370 	if (mouse.y >= y && mouse.y < y2) {
371 		s.hLine(x1 + 8, mouse.y, x2 + 2, s.format.RGBToColor(64, 64, 64));
372 	}
373 
374 	_shapes->get(0)->draw(s, x1, y1);
375 	_shapes->get(3)->draw(s, x2, y1);
376 	_shapes->get(2)->draw(s, x1, y2);
377 	_shapes->get(5)->draw(s, x2, y2);
378 
379 	for (int i = 0; i != _listSize; ++i) {
380 		_shapes->get(1)->draw(s, x1, y);
381 		_shapes->get(4)->draw(s, x2, y);
382 		uint32 color = s.format.RGBToColor((_items[i].colorIntensity / 2) * (256 / 32), (_items[i].colorIntensity / 2) * (256 / 32), _items[i].colorIntensity * (256 / 32));
383 		_vm->_mainFont->drawString(&s, _items[i].text, x, y, s.w, color);
384 		y += kLineHeight;
385 	}
386 	for (; x != x2; ++x) {
387 		_shapes->get(6)->draw(s, x, y1);
388 		_shapes->get(7)->draw(s, x, y2);
389 	}
390 }
391 
getAnswerIndex(int answer) const392 int DialogueMenu::getAnswerIndex(int answer) const {
393 	for (int i = 0; i != _listSize; ++i) {
394 		if (_items[i].answerValue == answer) {
395 			return i;
396 		}
397 	}
398 
399 	return -1;
400 }
401 
getText(int id) const402 const char *DialogueMenu::getText(int id) const {
403 	return _textResource->getText((uint32)id);
404 }
405 
calculatePosition(int unusedX,int unusedY)406 void DialogueMenu::calculatePosition(int unusedX, int unusedY) {
407 	_maxItemWidth = 0;
408 	for (int i = 0; i != _listSize; ++i) {
409 		_maxItemWidth = MAX(_maxItemWidth, _vm->_mainFont->getStringWidth(_items[i].text));
410 	}
411 	_maxItemWidth += 2;
412 
413 	int w = kBorderSize + _shapes->get(4)->getWidth() + _maxItemWidth;
414 	int h = kBorderSize + _shapes->get(7)->getHeight() + kLineHeight * _listSize;
415 
416 	_screenX = _centerX - w / 2;
417 	_screenY = _centerY - h / 2;
418 
419 	_screenX = CLIP(_screenX, 0, 640 - w);
420 	_screenY = CLIP(_screenY, 0, 480 - h);
421 
422 	_fadeInItemIndex = 0;
423 }
424 
mouseUp()425 void DialogueMenu::mouseUp() {
426 	_waitingForInput = false;
427 }
428 
waitingForInput() const429 bool DialogueMenu::waitingForInput() const {
430 	return _waitingForInput;
431 }
432 
save(SaveFileWriteStream & f)433 void DialogueMenu::save(SaveFileWriteStream &f) {
434 	f.writeBool(_isVisible);
435 	f.writeBool(_waitingForInput);
436 	f.writeInt(_selectedItemIndex);
437 	f.writeInt(_listSize);
438 
439 	f.writeInt(_neverRepeatListSize);
440 	for (int i = 0; i < 100; ++i) {
441 		f.writeInt(_neverRepeatValues[i]);
442 	}
443 	for (int i = 0; i < 100; ++i) {
444 		f.writeBool(_neverRepeatWasSelected[i]);
445 	}
446 	for (int i = 0; i < 10; ++i) {
447 		f.writeStringSz(_items[i].text, 50);
448 		f.writeInt(_items[i].answerValue);
449 		f.writeInt(_items[i].colorIntensity);
450 		f.writeInt(_items[i].priorityPolite);
451 		f.writeInt(_items[i].priorityNormal);
452 		f.writeInt(_items[i].prioritySurly);
453 		f.writeInt(_items[i].isDone);
454 	}
455 }
456 
load(SaveFileReadStream & f)457 void DialogueMenu::load(SaveFileReadStream &f) {
458 	_isVisible = f.readBool();
459 	_waitingForInput = f.readBool();
460 	_selectedItemIndex = f.readInt();
461 	_listSize = f.readInt();
462 
463 #if 0
464 	/* fix for duplicated non-repeated entries in the save game */
465 	f.readInt();
466 	_neverRepeatListSize = 0;
467 	int answer[100];
468 	bool selected[100];
469 	for (int i = 0; i < 100; ++i) {
470 		_neverRepeatValues[i] = -1;
471 		answer[i] = f.readInt();
472 	}
473 	for (int i = 0; i < 100; ++i) {
474 		_neverRepeatWasSelected[i] = false;
475 		selected[i] = f.readBool();
476 	}
477 	for (int i = 0; i < 100; ++i) {
478 		int found = false;
479 		bool value = false;
480 
481 		for (int j = 0; j < 100; ++j) {
482 			if (_neverRepeatValues[j] == answer[i]) {
483 				found = true;
484 			}
485 			if (answer[j] == answer[i]) {
486 				value |= selected[j];
487 			}
488 		}
489 
490 		if (!found) {
491 			_neverRepeatValues[_neverRepeatListSize] = answer[i];
492 			_neverRepeatWasSelected[_neverRepeatListSize] = value;
493 			++_neverRepeatListSize;
494 		}
495 	}
496 #else
497 	_neverRepeatListSize = f.readInt();
498 	for (int i = 0; i < 100; ++i) {
499 		_neverRepeatValues[i] = f.readInt();
500 	}
501 	for (int i = 0; i < 100; ++i) {
502 		_neverRepeatWasSelected[i] = f.readBool();
503 	}
504 #endif
505 
506 	for (int i = 0; i < 10; ++i) {
507 		_items[i].text = f.readStringSz(50);
508 		_items[i].answerValue = f.readInt();
509 		_items[i].colorIntensity = f.readInt();
510 		_items[i].priorityPolite = f.readInt();
511 		_items[i].priorityNormal = f.readInt();
512 		_items[i].prioritySurly = f.readInt();
513 		_items[i].isDone = f.readInt();
514 	}
515 }
516 
clear()517 void DialogueMenu::clear() {
518 	_isVisible = false;
519 	_waitingForInput = false;
520 	_selectedItemIndex = 0;
521 	_listSize = 0;
522 	for (int i = 0; i != kMaxItems; ++i) {
523 		_items[i].text.clear();
524 		_items[i].answerValue = -1;
525 		_items[i].isDone = 0;
526 		_items[i].priorityPolite = -1;
527 		_items[i].priorityNormal = -1;
528 		_items[i].prioritySurly = -1;
529 		_items[i].colorIntensity = 0;
530 	}
531 	_neverRepeatListSize = 0;
532 	for (int i = 0; i != kMaxRepeatHistory; ++i) {
533 		_neverRepeatValues[i]      = -1;
534 		_neverRepeatWasSelected[i] = false;
535 	}
536 	_centerX = 0;
537 	_centerY = 0;
538 }
539 
reset()540 void DialogueMenu::reset() {
541 	clear();
542 	_textResource = nullptr;
543 }
544 
darkenRect(Graphics::Surface & s,int x1,int y1,int x2,int y2)545 void DialogueMenu::darkenRect(Graphics::Surface &s, int x1, int y1, int x2, int y2) {
546 	x1 = MAX(x1, 0);
547 	y1 = MAX(y1, 0);
548 	x2 = MIN(x2, 640);
549 	y2 = MIN(y2, 480);
550 
551 	if (x1 < x2 && y1 < y2) {
552 		for (int y = y1; y != y2; ++y) {
553 			for (int x = x1; x != x2; ++x) {
554 				void *p = s.getBasePtr(CLIP(x, 0, s.w - 1), CLIP(y, 0, s.h - 1));
555 				uint8 r, g, b;
556 				s.format.colorToRGB(READ_UINT32(p), r, g, b);
557 				r /= 4;
558 				g /= 4;
559 				b /= 4;
560 				drawPixel(s, p, s.format.RGBToColor(r, g, b));
561 			}
562 		}
563 	}
564 }
565 
566 } // End of namespace BladeRunner
567