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