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  * Copyright 2020 Google
22  *
23  */
24 #include "common/file.h"
25 #include "hadesch/video.h"
26 #include "hadesch/pod_image.h"
27 #include "hadesch/tag_file.h"
28 #include "hadesch/hadesch.h"
29 #include "common/system.h"
30 #include "video/smk_decoder.h"
31 #include "audio/decoders/aiff.h"
32 #include "hadesch/pod_file.h"
33 #include "hadesch/baptr.h"
34 #include "common/translation.h"
35 
36 namespace Hadesch {
37 
38 static const int kHeroBeltMinY = 378;
39 static const int kHeroBeltMaxY = 471;
40 
41 static const TranscribedSound powerSounds[3][2] = {
42 	{
43 	    { "g0280nc0", _s("That's where hero power of strength will be stored when you earn it") },
44 	    { "g0280ng0", _s("The power of strength will let you overcome obstacles but you'll need a complete set of hero powers to use it") }
45 	},
46 	{
47 	    { "g0280nb0", _s("That's where hero power of stealth will be stored when you earn it") },
48 	    { "g0280nf0", _s("The power of stealth allows you to sneak past things but you'll need a complete set of hero powers to use it") }
49 	},
50 	{
51 	    { "g0280ne0", _s("That's where hero power of wisdom will be stored when you earn it") },
52 	    { "g0280nh0", _s("The power of wisdom will let you outwit and avoid deception but you'll need a complete set of hero powers to use it") }
53 	},
54 };
55 
loadImageArray(const Common::String & name)56 static Common::Array <PodImage> loadImageArray(const Common::String &name) {
57 	Common::SharedPtr<Common::SeekableReadStream> rs(g_vm->getWdPodFile()->getFileStream(name + ".pod"));
58 	PodFile pf2(name);
59 	pf2.openStore(rs);
60 	return pf2.loadImageArray();
61 }
62 
loadImage(const Common::String & name)63 static PodImage loadImage(const Common::String &name) {
64 	return loadImageArray(name)[0];
65 }
66 
67 static const struct {
68 	const char *background;
69 	const char *iconNames;
70 	const char *icons;
71 
72 	// TODO: figure out how the original handles
73 	// cursors.
74 	const char *iconCursors;
75 	const char *iconCursorsBright;
76 
77 	const char *scrollBg;
78 	const char *scrollBgHades;
79 	const char *scrollTextCrete;
80 	const char *scrollTextTroyFemale;
81 	const char *scrollTextTroyMale;
82 	const char *scrollTextMedusa;
83 	const char *scrollTextHades;
84 	const char *powerImageWisdom;
85 	const char *powerImageStrength;
86 	const char *powerImageStealth;
87 } assetTable[3] = {
88 	// Warm
89 	{
90 		"g0010oa0",
91 		"g0091ba0",
92 		"g0091bb0",
93 		"g0091bc0",
94 		"g0093bc0",
95 		"g0015oy0",
96 		"g0015oy3",
97 		"g0015td0",
98 		"g0015te0",
99 		"g0015te3",
100 		"g0015tf0",
101 		"g0015tg0",
102 		"g0290oa0",
103 		"g0290ob0",
104 		"g0290oc0"
105 	},
106 
107 	// Cold
108 	{
109 		"g0010ob0",
110 		"g0092ba0",
111 		"g0092bb0",
112 		"g0091bc0",
113 		"g0093bc0",
114 		"g0015oy2",
115 		"g0015oy5",
116 		"g0015td2",
117 		"g0015te2",
118 		"g0015te5",
119 		"g0015tf2",
120 		"g0015tg2",
121 		"g0092oa0",
122 		"g0092ob0",
123 		"g0092oc0"
124 	},
125 
126 	// Cool
127 	{
128 		"g0010oc0",
129 		"g0093ba0",
130 		"g0093bb0",
131 		"g0091bc0",
132 		"g0093bc0",
133 		"g0015oy1",
134 		"g0015oy4",
135 		"g0015td1",
136 		"g0015te1",
137 		"g0015te4",
138 		"g0015tf1",
139 		"g0015tg1",
140 		"g0093oa0",
141 		"g0093ob0",
142 		"g0093oc0"
143 	}
144 };
145 
HeroBelt()146 HeroBelt::HeroBelt() {
147 	for (int i = 0; i < 3; i++) {
148 		_background[i] = loadImage(assetTable[i].background);
149 		_iconNames[i] = loadImageArray(assetTable[i].iconNames);
150 		_icons[i] = loadImageArray(assetTable[i].icons);
151 		_iconCursors[i] = loadImageArray(assetTable[i].iconCursors);
152 		_iconCursorsBright[i] = loadImageArray(assetTable[i].iconCursorsBright);
153 		_powerImages[0][i] = loadImageArray(assetTable[i].powerImageStrength);
154 		_powerImages[1][i] = loadImageArray(assetTable[i].powerImageStealth);
155 		_powerImages[2][i] = loadImageArray(assetTable[i].powerImageWisdom);
156 
157 		_scrollBg[i] = loadImage(assetTable[i].scrollBg);
158 		_scrollBgHades[i] = loadImage(assetTable[i].scrollBgHades);
159 		_scrollTextCrete[i] = loadImage(assetTable[i].scrollTextCrete);
160 		_scrollTextTroyFemale[i] = loadImage(assetTable[i].scrollTextTroyFemale);
161 		_scrollTextTroyMale[i] = loadImage(assetTable[i].scrollTextTroyMale);
162 		_scrollTextMedusa[i] = loadImage(assetTable[i].scrollTextMedusa);
163 		_scrollTextHades[i] = loadImage(assetTable[i].scrollTextHades);
164 	}
165 
166 	_branchOfLife = loadImageArray("v7150ba0");
167 	_overHeroBelt = false;
168 	_heroBeltY = kHeroBeltMaxY;
169 	_bottomEdge = false;
170 	_heroBeltSpeed = 0;
171 	_edgeStartTime = 0;
172 	_animateItem = kNone;
173 	_animateItemTargetSlot = -1;
174 	_hotZone = -1;
175 	_holdingItem = kNone;
176 	_holdingSlot = -1;
177 	_highlightTextIdx = -1;
178 	_showScroll = false;
179 	_hotZones.readHotzones(
180 		Common::SharedPtr<Common::SeekableReadStream>(g_vm->getWdPodFile()->getFileStream("HeroBelt.HOT")),
181 		true);
182 }
183 
computeHotZone(int time,Common::Point mousePos,bool mouseEnabled)184 void HeroBelt::computeHotZone(int time, Common::Point mousePos, bool mouseEnabled) {
185 	bool wasBottomEdge = _bottomEdge;
186 
187 	_overHeroBelt = false;
188 	_bottomEdge = false;
189 
190 	_mousePos = mousePos;
191 
192 	if (!mouseEnabled) {
193 		return;
194 	}
195 
196 	_bottomEdge = mousePos.y > 460;
197 	_overHeroBelt = (_bottomEdge && _heroBeltSpeed < 0) || mousePos.y > _heroBeltY;
198 
199 	if (!wasBottomEdge && _bottomEdge)
200 		_edgeStartTime = time;
201 
202 	_currentTime = time;
203 
204 	int wasHotZone = _hotZone;
205 	_hotZone = _hotZones.pointToIndex(mousePos);
206 	if (_hotZone >= 0 && wasHotZone < 0) {
207 		_startHotTime = time;
208 	}
209 
210 	computeHighlight();
211 }
212 
isInFrieze()213 static bool isInFrieze() {
214 	Persistent *persistent = g_vm->getPersistent();
215 	return (persistent->_currentRoomId == kMinotaurPuzzle && persistent->_quest != kCreteQuest)
216 		|| (persistent->_currentRoomId == kTrojanHorsePuzzle && persistent->_quest != kTroyQuest)
217 		|| (persistent->_currentRoomId == kMedusaPuzzle && persistent->_quest != kMedusaQuest)
218 		|| (persistent->_currentRoomId == kFerrymanPuzzle && persistent->_quest != kRescuePhilQuest)
219 		|| (persistent->_currentRoomId == kMonsterPuzzle && persistent->_quest != kRescuePhilQuest);
220 }
221 
render(Common::SharedPtr<GfxContext> context,int time,Common::Point viewPoint)222 void HeroBelt::render(Common::SharedPtr<GfxContext> context, int time, Common::Point viewPoint) {
223 	Persistent *persistent = g_vm->getPersistent();
224 	Common::Point beltPoint;
225 
226 	// TODO: use beltopen hotzone instead?
227 	if (persistent->_currentRoomId == kMonsterPuzzle) {
228 		_heroBeltY = kHeroBeltMinY;
229 	} else {
230 		if (_bottomEdge && _heroBeltY == kHeroBeltMaxY
231 		    && (time > _edgeStartTime + 500)) {
232 			_heroBeltSpeed = -10;
233 		}
234 
235 		if (!_overHeroBelt && _heroBeltY != kHeroBeltMaxY
236 		    && _animateItemTargetSlot == -1) {
237 			_showScroll = false;
238 			_heroBeltSpeed = +10;
239 		}
240 
241 		if (_heroBeltSpeed != 0) {
242 			_heroBeltY += _heroBeltSpeed;
243 			if (_heroBeltY <= kHeroBeltMinY) {
244 				_heroBeltY = kHeroBeltMinY;
245 				_heroBeltSpeed = 0;
246 			}
247 			if (_heroBeltY >= kHeroBeltMaxY) {
248 				_heroBeltY = kHeroBeltMaxY;
249 				_heroBeltSpeed = 0;
250 			}
251 		}
252 	}
253 
254 	_currentTime = time;
255 
256 	beltPoint = Common::Point(0, _heroBeltY) + viewPoint;
257 
258 	_background[_colour].render(context, beltPoint);
259 
260 	if (_animateItem != kNone) {
261 		if (_currentTime > _animateItemStartTime + _animItemTime) {
262 			_animateItem = kNone;
263 			_animateItemTargetSlot = -1;
264 			_animItemCallbackEvent();
265 		}
266 	}
267 
268 	if (_animateItem != kNone) {
269 		Common::Point target = computeSlotPoint(_animateItemTargetSlot, true);
270 		Common::Point delta1 = target - _animateItemStartPoint;
271 		int elapsed = _currentTime - _animateItemStartTime;
272 		double fraction = ((elapsed + 0.0) / _animItemTime);
273 		Common::Point deltaPartial = fraction * delta1;
274 		Common::Point cur = _animateItemStartPoint + deltaPartial;
275 		_iconCursors[_colour][_animateItem - 1].render(
276 			context, cur + viewPoint);
277 	}
278 
279 	if (persistent->_currentRoomId == kMonsterPuzzle) {
280 		_icons[_colour][kNumberI].render(
281 				context,
282 				computeSlotPoint(0, false) + viewPoint
283 				+ Common::Point(0, 4));
284 		_icons[_colour][kNumberII].render(
285 				context,
286 				computeSlotPoint(1, false) + viewPoint);
287 		_icons[_colour][kNumberIII].render(
288 				context,
289 				computeSlotPoint(2, false) + viewPoint
290 				+ Common::Point(0, 4));
291 		for (unsigned i = 0; i < 3; i++) {
292 			_icons[_colour][_thunderboltFrame].render(
293 				context,
294 				computeSlotPoint(3 + i, false) + viewPoint);
295 		}
296 
297 		_branchOfLife[_branchOfLifeFrame].render(
298 			context, beltPoint);
299 
300 	} else {
301 		for (int i = 0; i < inventorySize; i++) {
302 			if (i == _animateItemTargetSlot || i == _holdingSlot
303 			    || persistent->_inventory[i] == kNone) {
304 				continue;
305 			}
306 			_icons[_colour][persistent->_inventory[i] - 1].render(
307 				context,
308 				computeSlotPoint(i, false) + viewPoint);
309 		}
310 	}
311 
312 	_icons[_colour][persistent->_hintsAreEnabled ? kActiveHints : kInactiveHints].render(
313 		context,
314 		computeSlotPoint(6, false) + viewPoint);
315 
316 	if (isInFrieze())
317 		_icons[_colour][kReturnToWall].render(
318 			context,
319 			computeSlotPoint(7, false) + viewPoint);
320 	else
321 		_icons[_colour][kQuestScroll].render(
322 			context,
323 			computeSlotPoint(7, false) + viewPoint);
324 
325 	_icons[_colour][kOptionsButton].render(
326 		context,
327 		computeSlotPoint(8, false) + viewPoint);
328 
329 	if (_highlightTextIdx >= 0) {
330 		_iconNames[_colour][_highlightTextIdx].render(
331 			context, beltPoint);
332 	}
333 
334 	for (unsigned i = 0; i < ARRAYSIZE(persistent->_powerLevel); i++)
335 		if (persistent->_powerLevel[i] > 0) {
336 			_powerImages[i][_colour][(int)i == _selectedPower].render(
337 				context, beltPoint);
338 		}
339 
340 	if (_showScroll) {
341 		PodImage *bg = _scrollBg, *text = nullptr;
342 		switch (persistent->_quest) {
343 		case kCreteQuest:
344 			text = _scrollTextCrete;
345 			break;
346 		case kTroyQuest:
347 			if (persistent->_gender == kMale)
348 				text = _scrollTextTroyMale;
349 			else
350 				text = _scrollTextTroyFemale;
351 			break;
352 		case kMedusaQuest:
353 			text = _scrollTextMedusa;
354 			break;
355 		case kRescuePhilQuest:
356 			text = _scrollTextHades;
357 			bg = _scrollBgHades;
358 			break;
359 		case kEndGame:
360 		case kNoQuest:
361 		case kNumQuests:
362 			break;
363 		}
364 		bg[_colour].render(context, viewPoint);
365 		if (text != nullptr)
366 			text[_colour].render(
367 				context, viewPoint);
368 	}
369 }
370 
isPositionOverHeroBelt(Common::Point mousePos) const371 bool HeroBelt::isPositionOverHeroBelt(Common::Point mousePos) const {
372 	return mousePos.y > _heroBeltY;
373 }
374 
handleClick(Common::Point mousePos)375 void HeroBelt::handleClick(Common::Point mousePos) {
376 	Persistent *persistent = g_vm->getPersistent();
377 	Common::String q = _hotZones.pointToName(mousePos);
378 	debug("handling belt click on <%s>", q.c_str());
379 	for (int i = 0; i < inventorySize; i++) {
380 		if (q == inventoryName(i)) {
381 			if (_holdingItem != kNone) {
382 				if (persistent->_inventory[i] != kNone &&
383 				    _holdingSlot != i)
384 					return;
385 				persistent->_inventory[_holdingSlot] = kNone;
386 				persistent->_inventory[i] = _holdingItem;
387 				_holdingItem = kNone;
388 				_holdingSlot = -1;
389 				return;
390 			}
391 			if (i == _animateItemTargetSlot
392 			    || persistent->_inventory[i] == kNone)
393 				return;
394 			_holdingItem = persistent->_inventory[i];
395 			_holdingSlot = i;
396 			return;
397 		}
398 	}
399 
400 	if (q == "quest scroll") {
401 		if (isInFrieze())
402 			g_vm->moveToRoom(kWallOfFameRoom);
403 		else
404 			_showScroll = true;
405 	}
406 
407 	if (q == "hints") {
408 		persistent->_hintsAreEnabled = !persistent->_hintsAreEnabled;
409 	}
410 
411 	if (q == "options") {
412 		g_vm->enterOptions();
413 	}
414 
415 	if (q == "strength")
416 		clickPower(kPowerStrength);
417 	if (q == "stealth")
418 		clickPower(kPowerStealth);
419 	if (q == "wisdom")
420 		clickPower(kPowerWisdom);
421 }
422 
clickPower(HeroPower pwr)423 void HeroBelt::clickPower(HeroPower pwr) {
424 	Persistent *persistent = g_vm->getPersistent();
425 	Common::SharedPtr<VideoRoom> room = g_vm->getVideoRoom();
426 
427 	if (persistent->_currentRoomId == kMonsterPuzzle) {
428 		_selectedPower = pwr;
429 		return;
430 	}
431 
432 	if (persistent->_quest == kRescuePhilQuest)
433 		return;
434 
435 	room->playSpeech(powerSounds[pwr][!!persistent->_powerLevel[pwr]]);
436 }
437 
computeHighlight()438 void HeroBelt::computeHighlight() {
439 	Common::String hotZone = _hotZones.indexToName(_hotZone);
440 	Persistent *persistent = g_vm->getPersistent();
441 	for (int i = 0; i < inventorySize; i++) {
442 		if (hotZone == inventoryName(i)) {
443 			if (persistent->_currentRoomId == kMonsterPuzzle) {
444 				_highlightTextIdx = i < 3 ? kNumberI + i : kLightning1 + i - 3;
445 				return;
446 			} else {
447 				if (persistent->_inventory[i] != kNone &&
448 				    _holdingSlot != i) {
449 					_highlightTextIdx = persistent->_inventory[i] - 1;
450 					return;
451 				}
452 			}
453 		}
454 	}
455 
456 	if (hotZone == "quest scroll") {
457 		// TODO: what's with 30 and 28?
458 		if (isInFrieze())
459 			_highlightTextIdx = kReturnToWall;
460 		else if (persistent->_quest == kRescuePhilQuest)
461 			_highlightTextIdx = kHadesScroll;
462 		else
463 			_highlightTextIdx = kQuestScroll;
464 		return;
465 	}
466 
467 	if (hotZone == "stealth") {
468 		_highlightTextIdx = kPowerOfStealth;
469 		return;
470 	}
471 
472 	if (hotZone == "strength") {
473 		_highlightTextIdx = kPowerOfStrength;
474 		return;
475 	}
476 
477 	if (hotZone == "wisdom") {
478 		_highlightTextIdx = kPowerOfWisdom;
479 		return;
480 	}
481 
482 	if (hotZone == "hints") {
483 		_highlightTextIdx = persistent->_hintsAreEnabled ? kActiveHints : kInactiveHints;
484 		return;
485 	}
486 
487 	if (hotZone == "options") {
488 		_highlightTextIdx = kOptionsButton;
489 		return;
490 	}
491 
492 	_highlightTextIdx = -1;
493 }
494 
placeToInventory(InventoryItem item,EventHandlerWrapper callbackEvent)495 void HeroBelt::placeToInventory(InventoryItem item, EventHandlerWrapper callbackEvent) {
496 	Persistent *persistent = g_vm->getPersistent();
497 	unsigned i;
498 	for (i = 0; i < inventorySize; i++) {
499 		if (persistent->_inventory[i] == kNone)
500 			break;
501 	}
502 
503 	if (i == inventorySize) {
504 		debug("Out of inventory space");
505 		return;
506 	}
507 
508 	persistent->_inventory[i] = item;
509 	_animateItem = item;
510 	_animItemCallbackEvent = callbackEvent;
511 	_animateItemStartPoint = _mousePos;
512 	_animateItemTargetSlot = i;
513 	_animateItemStartTime = _currentTime;
514 	_animItemTime = 2000;
515 	_heroBeltSpeed = -10;
516 }
517 
removeFromInventory(InventoryItem item)518 void HeroBelt::removeFromInventory(InventoryItem item) {
519 	Persistent *persistent = g_vm->getPersistent();
520 	for (unsigned i = 0; i < inventorySize; i++) {
521 		if (persistent->_inventory[i] == item) {
522 			persistent->_inventory[i] = kNone;
523 		}
524 	}
525 
526 	if (_holdingItem == item) {
527 		_holdingItem = kNone;
528 		_holdingSlot = -1;
529 	}
530 
531 	if (_animateItem == item) {
532 		_animateItem = kNone;
533 		_animateItemTargetSlot = -1;
534 	}
535 }
536 
clearHold()537 void HeroBelt::clearHold() {
538 	_holdingItem = kNone;
539 	_holdingSlot = -1;
540 }
541 
computeSlotPoint(int slot,bool fullyExtended)542 Common::Point HeroBelt::computeSlotPoint(int slot, bool fullyExtended) {
543 	Common::Point ret = Common::Point(19, 35);
544 	ret += Common::Point(0, (fullyExtended ? kHeroBeltMinY : _heroBeltY));
545 	ret += Common::Point(39 * slot, slot % 2 ? 4 : 0);
546 	if (slot >= 6) {
547 		ret += Common::Point(253, 0);
548 	}
549 	return ret;
550 }
551 
getCursor(int time)552 int HeroBelt::getCursor(int time) {
553 	Common::String q = _hotZones.indexToName(_hotZone);
554 	Persistent *persistent = g_vm->getPersistent();
555 	if (q == "")
556 		return 0;
557 	for (unsigned i = 0; i < inventorySize; i++) {
558 		if (q == inventoryName(i)) {
559 			if ((int) i != _animateItemTargetSlot
560 			    && persistent->_inventory[i] != kNone)
561 				return (time - _startHotTime) / kDefaultSpeed % 3;
562 			return 0;
563 		}
564 	}
565 
566 	return (time - _startHotTime) / kDefaultSpeed % 3;
567 }
568 
inventoryName(int slot)569 Common::String HeroBelt::inventoryName(int slot) {
570 	return Common::String::format("inventory%d", slot);
571 }
572 
isHoldingItem() const573 bool HeroBelt::isHoldingItem() const {
574 	return _holdingItem != kNone;
575 }
576 
getHoldingItemCursor(int cursorAnimationFrame) const577 const Graphics::Cursor *HeroBelt::getHoldingItemCursor(int cursorAnimationFrame) const {
578 	if ((cursorAnimationFrame / 2) % 2 == 1)
579 		return &_iconCursorsBright[_colour][_holdingItem - 1];
580 	else
581 		return &_iconCursors[_colour][_holdingItem - 1];
582 }
583 
584 }
585