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 "ultima/ultima4/controllers/combat_controller.h"
24 #include "ultima/ultima4/controllers/read_choice_controller.h"
25 #include "ultima/ultima4/controllers/read_dir_controller.h"
26 #include "ultima/ultima4/controllers/ztats_controller.h"
27 #include "ultima/ultima4/map/annotation.h"
28 #include "ultima/ultima4/map/dungeon.h"
29 #include "ultima/ultima4/map/location.h"
30 #include "ultima/ultima4/map/mapmgr.h"
31 #include "ultima/ultima4/map/movement.h"
32 #include "ultima/ultima4/map/tileset.h"
33 #include "ultima/ultima4/core/debugger.h"
34 #include "ultima/ultima4/core/settings.h"
35 #include "ultima/ultima4/core/utils.h"
36 #include "ultima/ultima4/events/event_handler.h"
37 #include "ultima/ultima4/game/context.h"
38 #include "ultima/ultima4/game/creature.h"
39 #include "ultima/ultima4/game/death.h"
40 #include "ultima/ultima4/game/game.h"
41 #include "ultima/ultima4/game/item.h"
42 #include "ultima/ultima4/game/names.h"
43 #include "ultima/ultima4/game/object.h"
44 #include "ultima/ultima4/game/player.h"
45 #include "ultima/ultima4/game/portal.h"
46 #include "ultima/ultima4/game/spell.h"
47 #include "ultima/ultima4/views/stats.h"
48 #include "ultima/ultima4/game/weapon.h"
49 #include "ultima/ultima4/gfx/screen.h"
50 #include "ultima/shared/std/containers.h"
51 #include "ultima/ultima4/ultima4.h"
52 #include "common/system.h"
53
54 namespace Ultima {
55 namespace Ultima4 {
56
57 CombatController *g_combat;
58
59 /**
60 * Returns true if 'map' points to a Combat Map
61 */
isCombatMap(Map * punknown)62 bool isCombatMap(Map *punknown) {
63 CombatMap *ps;
64 if ((ps = dynamic_cast<CombatMap *>(punknown)) != nullptr)
65 return true;
66 else
67 return false;
68 }
69
70 /**
71 * Returns a CombatMap pointer to the map
72 * passed, or a CombatMap pointer to the current map
73 * if no arguments were passed.
74 *
75 * Returns nullptr if the map provided (or current map)
76 * is not a combat map.
77 */
getCombatMap(Map * punknown)78 CombatMap *getCombatMap(Map *punknown) {
79 Map *m = punknown ? punknown : g_context->_location->_map;
80 if (!isCombatMap(m))
81 return nullptr;
82 else
83 return dynamic_cast<CombatMap *>(m);
84 }
85
86 /**
87 * CombatController class implementation
88 */
CombatController()89 CombatController::CombatController() : _map(nullptr) {
90 init();
91
92 g_context->_party->addObserver(this);
93 }
94
CombatController(CombatMap * m)95 CombatController::CombatController(CombatMap *m) : _map(m) {
96 init();
97
98 g_game->setMap(_map, true, nullptr, this);
99 g_context->_party->addObserver(this);
100 }
101
CombatController(MapId id)102 CombatController::CombatController(MapId id) : _map(nullptr) {
103 init();
104
105 _map = getCombatMap(mapMgr->get(id));
106 g_game->setMap(_map, true, nullptr, this);
107 g_context->_party->addObserver(this);
108 _forceStandardEncounterSize = false;
109 }
110
~CombatController()111 CombatController::~CombatController() {
112 g_context->_party->deleteObserver(this);
113 g_combat = nullptr;
114 }
115
init()116 void CombatController::init() {
117 g_combat = this;
118
119 _focus = 0;
120 Common::fill(&_creatureTable[0], &_creatureTable[AREA_CREATURES],
121 (const Creature *)nullptr);
122 _creature = nullptr;
123
124 _camping = false;
125 _forceStandardEncounterSize = false;
126 _placePartyOnMap = false;
127 _placeCreaturesOnMap = false;
128 _winOrLose = false;
129 _showMessage = false;
130 _exitDir = DIR_NONE;
131 }
132
setActive()133 void CombatController::setActive() {
134 MetaEngine::setKeybindingMode(KBMODE_COMBAT);
135 g_context->_horseSpeed = 0;
136 }
137
138 // Accessor Methods
isCamping() const139 bool CombatController::isCamping() const {
140 return _camping;
141 }
isWinOrLose() const142 bool CombatController::isWinOrLose() const {
143 return _winOrLose;
144 }
getExitDir() const145 Direction CombatController::getExitDir() const {
146 return _exitDir;
147 }
getFocus() const148 byte CombatController::getFocus() const {
149 return _focus;
150 }
getMap() const151 CombatMap *CombatController::getMap() const {
152 return _map;
153 }
getCreature() const154 Creature *CombatController::getCreature() const {
155 return _creature;
156 }
getParty()157 PartyMemberVector *CombatController::getParty() {
158 return &_party;
159 }
getCurrentPlayer()160 PartyMember *CombatController::getCurrentPlayer() {
161 return _party[_focus];
162 }
163
setExitDir(Direction d)164 void CombatController::setExitDir(Direction d) {
165 _exitDir = d;
166 }
setCreature(Creature * m)167 void CombatController::setCreature(Creature *m) {
168 _creature = m;
169 }
setWinOrLose(bool worl)170 void CombatController::setWinOrLose(bool worl) {
171 _winOrLose = worl;
172 }
showCombatMessage(bool show)173 void CombatController::showCombatMessage(bool show) {
174 _showMessage = show;
175 }
176
init(class Creature * m)177 void CombatController::init(class Creature *m) {
178 int i;
179
180 _creature = m;
181 _placeCreaturesOnMap = (m == nullptr) ? false : true;
182 _placePartyOnMap = true;
183 _winOrLose = true;
184 _map->setDungeonRoom(false);
185 _map->setAltarRoom(VIRT_NONE);
186 _showMessage = true;
187 _camping = false;
188
189 /* initialize creature info */
190 for (i = 0; i < AREA_CREATURES; i++) {
191 _creatureTable[i] = nullptr;
192 }
193
194 for (i = 0; i < AREA_PLAYERS; i++) {
195 _party.push_back(nullptr);
196 }
197
198 /* fill the creature table if a creature was provided to create */
199 fillCreatureTable(m);
200
201 /* initialize focus */
202 _focus = 0;
203 }
204
initDungeonRoom(int room,Direction from)205 void CombatController::initDungeonRoom(int room, Direction from) {
206 int i;
207 init(nullptr);
208
209 assertMsg(g_context->_location->_prev->_context & CTX_DUNGEON, "Error: called initDungeonRoom from non-dungeon context");
210 {
211 Dungeon *dng = dynamic_cast<Dungeon *>(g_context->_location->_prev->_map);
212 assert(dng);
213
214 DngRoom &dngRoom = dng->_rooms[room];
215
216 /* load the dungeon room properties */
217 _winOrLose = false;
218 _map->setDungeonRoom(true);
219 _exitDir = DIR_NONE;
220
221 /* FIXME: this probably isn't right way to see if you're entering an altar room... but maybe it is */
222 if ((g_context->_location->_prev->_map->_id != MAP_ABYSS) && (room == 0xF)) {
223 /* figure out which dungeon room they're entering */
224 if (g_context->_location->_prev->_coords.x == 3)
225 _map->setAltarRoom(VIRT_LOVE);
226 else if (g_context->_location->_prev->_coords.x <= 2)
227 _map->setAltarRoom(VIRT_TRUTH);
228 else
229 _map->setAltarRoom(VIRT_COURAGE);
230 }
231
232 /* load in creatures and creature start coordinates */
233 for (i = 0; i < AREA_CREATURES; i++) {
234 if (dng->_rooms[room]._creatureTiles[i] > 0) {
235 _placeCreaturesOnMap = true;
236 _creatureTable[i] = creatureMgr->getByTile(dng->_rooms[room]._creatureTiles[i]);
237 }
238 _map->creature_start[i].x = dng->_rooms[room]._creatureStart[i].x;
239 _map->creature_start[i].y = dng->_rooms[room]._creatureStart[i].y;
240 }
241
242 // Validate direction
243 switch (from) {
244 case DIR_WEST:
245 case DIR_NORTH:
246 case DIR_EAST:
247 case DIR_SOUTH:
248 break;
249 case DIR_ADVANCE:
250 case DIR_RETREAT:
251 default:
252 error("Invalid 'from' direction passed to initDungeonRoom()");
253 }
254
255 for (i = 0; i < AREA_PLAYERS; i++) {
256 _map->player_start[i].x = dngRoom._partyStart[i][from].x;
257 _map->player_start[i].y = dngRoom._partyStart[i][from].y;
258 }
259 }
260 }
261
applyCreatureTileEffects()262 void CombatController::applyCreatureTileEffects() {
263 CreatureVector creatures = _map->getCreatures();
264 CreatureVector::iterator i;
265
266 for (i = creatures.begin(); i != creatures.end(); i++) {
267 Creature *m = *i;
268 TileEffect effect = _map->tileTypeAt(m->getCoords(), WITH_GROUND_OBJECTS)->getEffect();
269 m->applyTileEffect(effect);
270 }
271 }
272
begin()273 void CombatController::begin() {
274 bool partyIsReadyToFight = false;
275
276 /* place party members on the map */
277 if (_placePartyOnMap)
278 placePartyMembers();
279
280 /* place creatures on the map */
281 if (_placeCreaturesOnMap)
282 placeCreatures();
283
284 /* if we entered an altar room, show the name */
285 if (_map->isAltarRoom()) {
286 g_screen->screenMessage("\nThe Altar Room of %s\n", getBaseVirtueName(_map->getAltarRoom()));
287 g_context->_location->_context = static_cast<LocationContext>(g_context->_location->_context | CTX_ALTAR_ROOM);
288 }
289
290 /* if there are creatures around, start combat! */
291 if (_showMessage && _placeCreaturesOnMap && _winOrLose)
292 g_screen->screenMessage("\n%c****%c COMBAT %c****%c\n", FG_GREY, FG_WHITE, FG_GREY, FG_WHITE);
293
294 /* FIXME: there should be a better way to accomplish this */
295 if (!_camping) {
296 g_music->playMapMusic();
297 }
298
299 /* Set focus to the first active party member, if there is one */
300 for (int i = 0; i < AREA_PLAYERS; i++) {
301 if (setActivePlayer(i)) {
302 partyIsReadyToFight = true;
303 break;
304 }
305 }
306
307 if (!_camping && !partyIsReadyToFight)
308 g_context->_location->_turnCompleter->finishTurn();
309
310 eventHandler->pushController(this);
311 }
312
end(bool adjustKarma)313 void CombatController::end(bool adjustKarma) {
314 eventHandler->popController();
315
316 /* The party is dead -- start the death sequence */
317 if (g_context->_party->isDead()) {
318 /* remove the creature */
319 if (_creature)
320 g_context->_location->_map->removeObject(_creature);
321
322 g_death->start(5);
323
324 } else {
325 /* need to get this here because when we exit to the parent map, all the monsters are cleared */
326 bool won = isWon();
327
328 g_game->exitToParentMap();
329 g_music->playMapMusic();
330
331 if (_winOrLose) {
332 if (won) {
333 if (_creature) {
334 if (_creature->isEvil())
335 g_context->_party->adjustKarma(KA_KILLED_EVIL);
336 awardLoot();
337 }
338
339 g_screen->screenMessage("\nVictory!\n\n");
340 } else if (!g_context->_party->isDead()) {
341 /* minus points for fleeing from evil creatures */
342 if (adjustKarma && _creature && _creature->isEvil()) {
343 g_screen->screenMessage("\nBattle is lost!\n\n");
344 g_context->_party->adjustKarma(KA_FLED_EVIL);
345 } else if (adjustKarma && _creature && _creature->isGood())
346 g_context->_party->adjustKarma(KA_FLED_GOOD);
347 }
348 }
349
350 /* exiting a dungeon room */
351 if (_map->isDungeonRoom()) {
352 g_screen->screenMessage("Leave Room!\n");
353 if (_map->isAltarRoom()) {
354 PortalTriggerAction action = ACTION_NONE;
355
356 /* when exiting altar rooms, you exit to other dungeons. Here it goes... */
357 switch (_exitDir) {
358 case DIR_NORTH:
359 action = ACTION_EXIT_NORTH;
360 break;
361 case DIR_EAST:
362 action = ACTION_EXIT_EAST;
363 break;
364 case DIR_SOUTH:
365 action = ACTION_EXIT_SOUTH;
366 break;
367 case DIR_WEST:
368 action = ACTION_EXIT_WEST;
369 break;
370 case DIR_NONE:
371 break;
372 case DIR_ADVANCE:
373 case DIR_RETREAT:
374 default:
375 error("Invalid exit dir %d", _exitDir);
376 break;
377 }
378
379 if (action != ACTION_NONE)
380 usePortalAt(g_context->_location, g_context->_location->_coords, action);
381 } else {
382 g_screen->screenMessage("\n");
383 }
384
385 if (_exitDir != DIR_NONE) {
386 g_ultima->_saveGame->_orientation = _exitDir; /* face the direction exiting the room */
387 // XXX: why north, shouldn't this be orientation?
388 g_context->_location->move(DIR_NORTH, false); /* advance 1 space outside of the room */
389 }
390 }
391
392 /* remove the creature */
393 if (_creature)
394 g_context->_location->_map->removeObject(_creature);
395
396 /* Make sure finishturn only happens if a new combat has not begun */
397 if (!eventHandler->getController()->isCombatController())
398 g_context->_location->_turnCompleter->finishTurnAfterCombatEnds();
399 }
400
401 delete this;
402 }
403
fillCreatureTable(const Creature * creature)404 void CombatController::fillCreatureTable(const Creature *creature) {
405 int i, j;
406
407 if (creature != nullptr) {
408 const Creature *baseCreature = creature, *current;
409 int numCreatures = initialNumberOfCreatures(creature);
410
411 if (baseCreature->getId() == PIRATE_ID)
412 baseCreature = creatureMgr->getById(ROGUE_ID);
413
414 for (i = 0; i < numCreatures; i++) {
415 current = baseCreature;
416
417 /* find a free spot in the creature table */
418 do {
419 j = xu4_random(AREA_CREATURES) ;
420 } while (_creatureTable[j] != nullptr);
421
422 /* see if creature is a leader or leader's leader */
423 if (creatureMgr->getById(baseCreature->getLeader()) != baseCreature && /* leader is a different creature */
424 i != (numCreatures - 1)) { /* must have at least 1 creature of type encountered */
425
426 if (xu4_random(32) == 0) /* leader's leader */
427 current = creatureMgr->getById(creatureMgr->getById(baseCreature->getLeader())->getLeader());
428 else if (xu4_random(8) == 0) /* leader */
429 current = creatureMgr->getById(baseCreature->getLeader());
430 }
431
432 /* place this creature in the creature table */
433 _creatureTable[j] = current;
434 }
435 }
436 }
437
initialNumberOfCreatures(const Creature * creature) const438 int CombatController::initialNumberOfCreatures(const Creature *creature) const {
439 int ncreatures;
440 Map *map = g_context->_location->_prev ? g_context->_location->_prev->_map : g_context->_location->_map;
441
442 /* if in an unusual combat situation, generally we stick to normal encounter sizes,
443 (such as encounters from sleeping in an inn, etc.) */
444 if (_forceStandardEncounterSize || map->isWorldMap() || (g_context->_location->_prev && g_context->_location->_prev->_context & CTX_DUNGEON)) {
445 ncreatures = xu4_random(8) + 1;
446
447 if (ncreatures == 1) {
448 if (creature && creature->getEncounterSize() > 0)
449 ncreatures = xu4_random(creature->getEncounterSize()) + creature->getEncounterSize() + 1;
450 else
451 ncreatures = 8;
452 }
453
454 while (ncreatures > 2 * g_ultima->_saveGame->_members) {
455 ncreatures = xu4_random(16) + 1;
456 }
457 } else {
458 if (creature && creature->getId() == GUARD_ID)
459 ncreatures = g_ultima->_saveGame->_members * 2;
460 else
461 ncreatures = 1;
462 }
463
464 return ncreatures;
465 }
466
isWon() const467 bool CombatController::isWon() const {
468 CreatureVector creatures = _map->getCreatures();
469 if (creatures.size())
470 return false;
471 return true;
472 }
473
isLost() const474 bool CombatController::isLost() const {
475 PartyMemberVector party = _map->getPartyMembers();
476 if (party.size())
477 return false;
478 return true;
479 }
480
moveCreatures()481 void CombatController::moveCreatures() {
482 Creature *m;
483 CreatureVector creatures = _map->getCreatures();
484
485 // IMPORTANT: We need to keep regenerating the creatures list,
486 // because monsters may be removed if a jinxed monster kills another
487 for (int i = 0; i < (int)creatures.size(); ++i) {
488 m = creatures[i];
489 m->act(this);
490
491 creatures = _map->getCreatures();
492 if (i < (int)creatures.size() && creatures[i] != m) {
493 // Don't skip a later creature when an earlier one flees
494 --i;
495 }
496 }
497 }
498
placeCreatures()499 void CombatController::placeCreatures() {
500 int i;
501
502 for (i = 0; i < AREA_CREATURES; i++) {
503 const Creature *m = _creatureTable[i];
504 if (m)
505 _map->addCreature(m, _map->creature_start[i]);
506 }
507 }
508
placePartyMembers()509 void CombatController::placePartyMembers() {
510 int i;
511 // The following line caused a crash upon entering combat (MSVC8 binary)
512 // party.clear();
513
514 for (i = 0; i < g_context->_party->size(); i++) {
515 PartyMember *p = g_context->_party->member(i);
516 p->setFocus(false); // take the focus off of everyone
517
518 /* don't place dead party members */
519 if (p->getStatus() != STAT_DEAD) {
520 /* add the party member to the map */
521 p->setCoords(_map->player_start[i]);
522 p->setMap(_map);
523 _map->_objects.push_back(p);
524 _party[i] = p;
525 }
526 }
527 }
528
setActivePlayer(int player)529 bool CombatController::setActivePlayer(int player) {
530 PartyMember *p = _party[player];
531
532 if (p && !p->isDisabled()) {
533 if (_party[_focus])
534 _party[_focus]->setFocus(false);
535
536 p->setFocus();
537 _focus = player;
538
539 g_screen->screenMessage("\n%s with %s\n\020", p->getName().c_str(), p->getWeapon()->getName().c_str());
540 g_context->_stats->highlightPlayer(_focus);
541 return true;
542 }
543
544 return false;
545 }
546
awardLoot()547 void CombatController::awardLoot() {
548 Coords coords = _creature->getCoords();
549 const Tile *ground = g_context->_location->_map->tileTypeAt(coords, WITHOUT_OBJECTS);
550
551 /* add a chest, if the creature leaves one */
552 if (_creature->leavesChest() &&
553 ground->isCreatureWalkable() &&
554 (!(g_context->_location->_context & CTX_DUNGEON) || ground->isDungeonFloor())) {
555 MapTile chest = g_context->_location->_map->_tileSet->getByName("chest")->getId();
556 g_context->_location->_map->addObject(chest, chest, coords);
557 }
558 /* add a ship if you just defeated a pirate ship */
559 else if (_creature->getTile().getTileType()->isPirateShip()) {
560 MapTile ship = g_context->_location->_map->_tileSet->getByName("ship")->getId();
561 ship.setDirection(_creature->getTile().getDirection());
562 g_context->_location->_map->addObject(ship, ship, coords);
563 }
564 }
565
attackHit(Creature * attacker,Creature * defender)566 bool CombatController::attackHit(Creature *attacker, Creature *defender) {
567 assertMsg(attacker != nullptr, "attacker must not be nullptr");
568 assertMsg(defender != nullptr, "defender must not be nullptr");
569
570 int attackValue = xu4_random(0x100) + attacker->getAttackBonus();
571 int defenseValue = defender->getDefense();
572
573 return attackValue > defenseValue;
574 }
575
attackAt(const Coords & coords,PartyMember * attacker,int dir,int range,int distance)576 bool CombatController::attackAt(const Coords &coords, PartyMember *attacker, int dir, int range, int distance) {
577 const Weapon *weapon = attacker->getWeapon();
578 bool wrongRange = weapon->rangeAbsolute() && (distance != range);
579
580 MapTile hittile = _map->_tileSet->getByName(weapon->getHitTile())->getId();
581 MapTile misstile = _map->_tileSet->getByName(weapon->getMissTile())->getId();
582
583 // Check to see if something hit
584 Creature *creature = _map->creatureAt(coords);
585
586 /* If we haven't hit a creature, or the weapon's range is absolute
587 and we're testing the wrong range, stop now! */
588 if (!creature || wrongRange) {
589
590 /* If the weapon is shown as it travels, show it now */
591 if (weapon->showTravel()) {
592 GameController::flashTile(coords, misstile, 1);
593 }
594
595 // no target found
596 return false;
597 }
598
599 /* Did the weapon miss? */
600 if ((g_context->_location->_prev->_map->_id == MAP_ABYSS && !weapon->isMagic()) || /* non-magical weapon in the Abyss */
601 !attackHit(attacker, creature)) { /* player naturally missed */
602 g_screen->screenMessage("Missed!\n");
603
604 /* show the 'miss' tile */
605 GameController::flashTile(coords, misstile, 1);
606 } else { /* The weapon hit! */
607
608 /* show the 'hit' tile */
609 GameController::flashTile(coords, misstile, 1);
610 soundPlay(SOUND_NPC_STRUCK, false, -1); // NPC_STRUCK, melee hit
611 GameController::flashTile(coords, hittile, 3);
612
613 /* apply the damage to the creature */
614 if (!attacker->dealDamage(creature, attacker->getDamage())) {
615 creature = nullptr;
616 GameController::flashTile(coords, hittile, 1);
617 }
618 }
619
620 return true;
621 }
622
rangedAttack(const Coords & coords,Creature * attacker)623 bool CombatController::rangedAttack(const Coords &coords, Creature *attacker) {
624 MapTile hittile = _map->_tileSet->getByName(attacker->getHitTile())->getId();
625 MapTile misstile = _map->_tileSet->getByName(attacker->getMissTile())->getId();
626
627 Creature *target = isCreature(attacker) ? _map->partyMemberAt(coords) : _map->creatureAt(coords);
628
629 /* If we haven't hit something valid, stop now */
630 if (!target) {
631 GameController::flashTile(coords, misstile, 1);
632 return false;
633 }
634
635 /* Get the effects of the tile the creature is using */
636 TileEffect effect = hittile.getTileType()->getEffect();
637
638 /* Monster's ranged attacks never miss */
639
640 GameController::flashTile(coords, misstile, 1);
641 /* show the 'hit' tile */
642 GameController::flashTile(coords, hittile, 3);
643
644 /* These effects happen whether or not the opponent was hit */
645 switch (effect) {
646 case EFFECT_ELECTRICITY:
647 /* FIXME: are there any special effects here? */
648 soundPlay(SOUND_PC_STRUCK, false);
649 g_screen->screenMessage("\n%s %cElectrified%c!\n", target->getName().c_str(), FG_BLUE, FG_WHITE);
650 attacker->dealDamage(target, attacker->getDamage());
651 break;
652
653 case EFFECT_POISON:
654 case EFFECT_POISONFIELD:
655 /* see if the player is poisoned */
656 if ((xu4_random(2) == 0) && (target->getStatus() != STAT_POISONED)) {
657 // POISON_EFFECT, ranged hit
658 soundPlay(SOUND_POISON_EFFECT, false);
659 g_screen->screenMessage("\n%s %cPoisoned%c!\n", target->getName().c_str(), FG_GREEN, FG_WHITE);
660 target->addStatus(STAT_POISONED);
661 }
662 // else g_screen->screenMessage("Failed.\n");
663 break;
664
665 case EFFECT_SLEEP:
666 /* see if the player is put to sleep */
667 if (xu4_random(2) == 0) {
668 // SLEEP, ranged hit, plays even if sleep failed or PC already asleep
669 soundPlay(SOUND_SLEEP, false);
670 g_screen->screenMessage("\n%s %cSlept%c!\n", target->getName().c_str(), FG_PURPLE, FG_WHITE);
671 target->putToSleep();
672 }
673 // else g_screen->screenMessage("Failed.\n");
674 break;
675
676 case EFFECT_LAVA:
677 case EFFECT_FIRE:
678 /* FIXME: are there any special effects here? */
679 soundPlay(SOUND_PC_STRUCK, false);
680 g_screen->screenMessage("\n%s %c%s Hit%c!\n", target->getName().c_str(), FG_RED,
681 effect == EFFECT_LAVA ? "Lava" : "Fiery", FG_WHITE);
682 attacker->dealDamage(target, attacker->getDamage());
683 break;
684
685 default:
686 /* show the appropriate 'hit' message */
687 // soundPlay(SOUND_PC_STRUCK, false);
688 if (hittile == g_tileSets->findTileByName("magic_flash")->getId())
689 g_screen->screenMessage("\n%s %cMagical Hit%c!\n", target->getName().c_str(), FG_BLUE, FG_WHITE);
690 else
691 g_screen->screenMessage("\n%s Hit!\n", target->getName().c_str());
692 attacker->dealDamage(target, attacker->getDamage());
693 break;
694 }
695 GameController::flashTile(coords, hittile, 1);
696 return true;
697 }
698
rangedMiss(const Coords & coords,Creature * attacker)699 void CombatController::rangedMiss(const Coords &coords, Creature *attacker) {
700 /* If the creature leaves a tile behind, do it here! (lava lizard, etc) */
701 const Tile *ground = _map->tileTypeAt(coords, WITH_GROUND_OBJECTS);
702 if (attacker->leavesTile() && ground->isWalkable())
703 _map->_annotations->add(coords, _map->_tileSet->getByName(attacker->getHitTile())->getId());
704 }
705
returnWeaponToOwner(const Coords & coords,int distance,int dir,const Weapon * weapon)706 bool CombatController::returnWeaponToOwner(const Coords &coords, int distance, int dir, const Weapon *weapon) {
707 MapCoords new_coords = coords;
708
709 MapTile misstile = _map->_tileSet->getByName(weapon->getMissTile())->getId();
710
711 /* reverse the direction of the weapon */
712 Direction returnDir = dirReverse(dirFromMask(dir));
713
714 for (int i = distance; i > 1; i--) {
715 new_coords.move(returnDir, _map);
716
717 GameController::flashTile(new_coords, misstile, 1);
718 }
719 gameUpdateScreen();
720
721 return true;
722 }
723
finishTurn()724 void CombatController::finishTurn() {
725 PartyMember *player = getCurrentPlayer();
726 int quick;
727
728 /* return to party overview */
729 g_context->_stats->setView(STATS_PARTY_OVERVIEW);
730
731 if (isWon() && _winOrLose) {
732 end(true);
733 return;
734 }
735
736 /* make sure the player with the focus is still in battle (hasn't fled or died) */
737 if (player) {
738 /* apply effects from tile player is standing on */
739 player->applyEffect(g_context->_location->_map->tileTypeAt(player->getCoords(), WITH_GROUND_OBJECTS)->getEffect());
740 }
741
742 quick = (*g_context->_aura == Aura::QUICKNESS) && player && (xu4_random(2) == 0) ? 1 : 0;
743
744 /* check to see if the player gets to go again (and is still alive) */
745 if (!quick || player->isDisabled()) {
746
747 do {
748 g_context->_location->_map->_annotations->passTurn();
749
750 /* put a sleeping person in place of the player,
751 or restore an awakened member to their original state */
752 if (player) {
753 if (player->getStatus() == STAT_SLEEPING && (xu4_random(8) == 0))
754 player->wakeUp();
755
756 /* remove focus from the current party member */
757 player->setFocus(false);
758
759 /* eat some food */
760 g_context->_party->adjustFood(-1);
761 }
762
763 /* put the focus on the next party member */
764 _focus++;
765
766 /* move creatures and wrap around at end */
767 if (_focus >= g_context->_party->size()) {
768
769 /* reset the focus to the avatar and start the party's turn over again */
770 _focus = 0;
771
772 gameUpdateScreen();
773 EventHandler::sleep(50); /* give a slight pause in case party members are asleep for awhile */
774
775 /* adjust moves */
776 g_context->_party->endTurn();
777
778 /* count down our aura (if we have one) */
779 g_context->_aura->passTurn();
780
781 /**
782 * ====================
783 * HANDLE CREATURE STUFF
784 * ====================
785 */
786
787 /* first, move all the creatures */
788 moveCreatures();
789
790 /* then, apply tile effects to creatures */
791 applyCreatureTileEffects();
792
793 /* check to see if combat is over */
794 if (isLost()) {
795 end(true);
796 return;
797 }
798
799 /* end combat immediately if the enemy has fled */
800 else if (isWon() && _winOrLose) {
801 end(true);
802 return;
803 }
804 }
805
806 /* get the next party member */
807 player = getCurrentPlayer();
808
809 } while (!player ||
810 player->isDisabled() || /* dead or sleeping */
811 ((g_context->_party->getActivePlayer() >= 0) && /* active player is set */
812 (_party[g_context->_party->getActivePlayer()]) && /* and the active player is still in combat */
813 !_party[g_context->_party->getActivePlayer()]->isDisabled() && /* and the active player is not disabled */
814 (g_context->_party->getActivePlayer() != _focus)));
815 } else {
816 g_context->_location->_map->_annotations->passTurn();
817 }
818
819 #if 0
820 if (focus != 0) {
821 getCurrentPlayer()->act();
822 finishTurn();
823 } else setActivePlayer(focus);
824 #else
825 /* display info about the current player */
826 setActivePlayer(_focus);
827 #endif
828 }
829
movePartyMember(MoveEvent & event)830 void CombatController::movePartyMember(MoveEvent &event) {
831 /* active player left/fled combat */
832 if ((event._result & MOVE_EXIT_TO_PARENT) && (g_context->_party->getActivePlayer() == _focus)) {
833 g_context->_party->setActivePlayer(-1);
834 /* assign active player to next available party member */
835 for (int i = 0; i < g_context->_party->size(); i++) {
836 if (_party[i] && !_party[i]->isDisabled()) {
837 g_context->_party->setActivePlayer(i);
838 break;
839 }
840 }
841 }
842
843 g_screen->screenMessage("%s\n", getDirectionName(event._dir));
844 if (event._result & MOVE_MUST_USE_SAME_EXIT) {
845 soundPlay(SOUND_ERROR); // ERROR move, all PCs must use the same exit
846 g_screen->screenMessage("All must use same exit!\n");
847 } else if (event._result & MOVE_BLOCKED) {
848 soundPlay(SOUND_BLOCKED); // BLOCKED move
849 g_screen->screenMessage("%cBlocked!%c\n", FG_GREY, FG_WHITE);
850 } else if (event._result & MOVE_SLOWED) {
851 soundPlay(SOUND_WALK_SLOWED); // WALK_SLOWED move
852 g_screen->screenMessage("%cSlow progress!%c\n", FG_GREY, FG_WHITE);
853 } else if (_winOrLose && getCreature()->isEvil() && (event._result & (MOVE_EXIT_TO_PARENT | MOVE_MAP_CHANGE))) {
854 soundPlay(SOUND_FLEE); // FLEE move
855 } else {
856 soundPlay(SOUND_WALK_COMBAT); // WALK_COMBAT move
857 }
858 }
859
keybinder(KeybindingAction action)860 void CombatController::keybinder(KeybindingAction action) {
861 MetaEngine::executeAction(action);
862 }
863
attack(Direction dir,int distance)864 void CombatController::attack(Direction dir, int distance) {
865 g_screen->screenMessage("Dir: ");
866
867 ReadDirController dirController;
868 #ifdef IOS_ULTIMA4
869 U4IOS::IOSDirectionHelper directionPopup;
870 #endif
871 if (dir == DIR_NONE) {
872 eventHandler->pushController(&dirController);
873 dir = dirController.waitFor();
874 if (dir == DIR_NONE)
875 return;
876 }
877 g_screen->screenMessage("%s\n", getDirectionName(dir));
878
879 PartyMember *attacker = getCurrentPlayer();
880
881 const Weapon *weapon = attacker->getWeapon();
882 int range = weapon->getRange();
883 if (weapon->canChooseDistance()) {
884 g_screen->screenMessage("Range: ");
885
886 if (distance == -1) {
887 int choice = ReadChoiceController::get("123456789");
888 distance = choice - '0';
889 }
890
891 if (distance >= 1 && distance <= weapon->getRange()) {
892 range = distance;
893 g_screen->screenMessage("%d\n", range);
894 } else {
895 return;
896 }
897 }
898
899 // the attack was already made, even if there is no valid target
900 // so play the attack sound
901 soundPlay(SOUND_PC_ATTACK, false); // PC_ATTACK, melee and ranged
902
903
904 Std::vector<Coords> path = gameGetDirectionalActionPath(MASK_DIR(dir), MASK_DIR_ALL,
905 attacker->getCoords(), 1, range,
906 weapon->canAttackThroughObjects() ? nullptr : &Tile::canAttackOverTile,
907 false);
908
909 bool foundTarget = false;
910 int targetDistance = path.size();
911 Coords targetCoords(attacker->getCoords());
912 if (path.size() > 0)
913 targetCoords = path.back();
914
915 distance = 1;
916 for (Std::vector<Coords>::iterator i = path.begin(); i != path.end(); i++) {
917 if (attackAt(*i, attacker, MASK_DIR(dir), range, distance)) {
918 foundTarget = true;
919 targetDistance = distance;
920 targetCoords = *i;
921 break;
922 }
923 distance++;
924 }
925
926 // is weapon lost? (e.g. dagger)
927 if (weapon->loseWhenUsed() ||
928 (weapon->loseWhenRanged() && (!foundTarget || targetDistance > 1))) {
929 if (!attacker->loseWeapon())
930 g_screen->screenMessage("Last One!\n");
931 }
932
933 // does weapon leave a tile behind? (e.g. flaming oil)
934 const Tile *ground = _map->tileTypeAt(targetCoords, WITHOUT_OBJECTS);
935 if (!weapon->leavesTile().empty() && ground->isWalkable())
936 _map->_annotations->add(targetCoords, _map->_tileSet->getByName(weapon->leavesTile())->getId());
937
938 /* show the 'miss' tile */
939 if (!foundTarget) {
940 GameController::flashTile(targetCoords, weapon->getMissTile(), 1);
941 /* This goes here so messages are shown in the original order */
942 g_screen->screenMessage("Missed!\n");
943 }
944
945 // does weapon returns to its owner? (e.g. magic axe)
946 if (weapon->returns())
947 returnWeaponToOwner(targetCoords, targetDistance, MASK_DIR(dir), weapon);
948 }
949
update(Party * party,PartyEvent & event)950 void CombatController::update(Party *party, PartyEvent &event) {
951 if (event._type == PartyEvent::PLAYER_KILLED)
952 g_screen->screenMessage("\n%c%s is Killed!%c\n", FG_RED, event._player->getName().c_str(), FG_WHITE);
953 }
954
955 /*-------------------------------------------------------------------*/
956
CombatMap()957 CombatMap::CombatMap() : Map(), _dungeonRoom(false), _altarRoom(VIRT_NONE), _contextual(false) {}
958
getCreatures()959 CreatureVector CombatMap::getCreatures() {
960 ObjectDeque::iterator i;
961 CreatureVector creatures;
962 for (i = _objects.begin(); i != _objects.end(); i++) {
963 if (isCreature(*i) && !isPartyMember(*i))
964 creatures.push_back(dynamic_cast<Creature *>(*i));
965 }
966 return creatures;
967 }
968
getPartyMembers()969 PartyMemberVector CombatMap::getPartyMembers() {
970 ObjectDeque::iterator i;
971 PartyMemberVector party;
972 for (i = _objects.begin(); i != _objects.end(); i++) {
973 if (isPartyMember(*i))
974 party.push_back(dynamic_cast<PartyMember *>(*i));
975 }
976 return party;
977 }
978
partyMemberAt(Coords coords)979 PartyMember *CombatMap::partyMemberAt(Coords coords) {
980 PartyMemberVector party = getPartyMembers();
981 PartyMemberVector::iterator i;
982
983 for (i = party.begin(); i != party.end(); i++) {
984 if ((*i)->getCoords() == coords)
985 return *i;
986 }
987 return nullptr;
988 }
989
creatureAt(Coords coords)990 Creature *CombatMap::creatureAt(Coords coords) {
991 CreatureVector creatures = getCreatures();
992 CreatureVector::iterator i;
993
994 for (i = creatures.begin(); i != creatures.end(); i++) {
995 if ((*i)->getCoords() == coords)
996 return *i;
997 }
998 return nullptr;
999 }
1000
mapForTile(const Tile * groundTile,const Tile * transport,Object * obj)1001 MapId CombatMap::mapForTile(const Tile *groundTile, const Tile *transport, Object *obj) {
1002 bool fromShip = false,
1003 toShip = false;
1004 Object *objUnder = g_context->_location->_map->objectAt(g_context->_location->_coords);
1005
1006 static Std::map<const Tile *, MapId, Std::PointerHash> tileMap;
1007 if (!tileMap.size()) {
1008 tileMap[g_tileSets->get("base")->getByName("horse")] = MAP_GRASS_CON;
1009 tileMap[g_tileSets->get("base")->getByName("swamp")] = MAP_MARSH_CON;
1010 tileMap[g_tileSets->get("base")->getByName("grass")] = MAP_GRASS_CON;
1011 tileMap[g_tileSets->get("base")->getByName("brush")] = MAP_BRUSH_CON;
1012 tileMap[g_tileSets->get("base")->getByName("forest")] = MAP_FOREST_CON;
1013 tileMap[g_tileSets->get("base")->getByName("hills")] = MAP_HILL_CON;
1014 tileMap[g_tileSets->get("base")->getByName("dungeon")] = MAP_DUNGEON_CON;
1015 tileMap[g_tileSets->get("base")->getByName("city")] = MAP_GRASS_CON;
1016 tileMap[g_tileSets->get("base")->getByName("castle")] = MAP_GRASS_CON;
1017 tileMap[g_tileSets->get("base")->getByName("town")] = MAP_GRASS_CON;
1018 tileMap[g_tileSets->get("base")->getByName("lcb_entrance")] = MAP_GRASS_CON;
1019 tileMap[g_tileSets->get("base")->getByName("bridge")] = MAP_BRIDGE_CON;
1020 tileMap[g_tileSets->get("base")->getByName("balloon")] = MAP_GRASS_CON;
1021 tileMap[g_tileSets->get("base")->getByName("bridge_pieces")] = MAP_BRIDGE_CON;
1022 tileMap[g_tileSets->get("base")->getByName("shrine")] = MAP_GRASS_CON;
1023 tileMap[g_tileSets->get("base")->getByName("chest")] = MAP_GRASS_CON;
1024 tileMap[g_tileSets->get("base")->getByName("brick_floor")] = MAP_BRICK_CON;
1025 tileMap[g_tileSets->get("base")->getByName("moongate")] = MAP_GRASS_CON;
1026 tileMap[g_tileSets->get("base")->getByName("moongate_opening")] = MAP_GRASS_CON;
1027 tileMap[g_tileSets->get("base")->getByName("dungeon_floor")] = MAP_GRASS_CON;
1028 }
1029 static Std::map<const Tile *, MapId, Std::PointerHash> dungeontileMap;
1030 if (!dungeontileMap.size()) {
1031 dungeontileMap[g_tileSets->get("dungeon")->getByName("brick_floor")] = MAP_DNG0_CON;
1032 dungeontileMap[g_tileSets->get("dungeon")->getByName("up_ladder")] = MAP_DNG1_CON;
1033 dungeontileMap[g_tileSets->get("dungeon")->getByName("down_ladder")] = MAP_DNG2_CON;
1034 dungeontileMap[g_tileSets->get("dungeon")->getByName("up_down_ladder")] = MAP_DNG3_CON;
1035 // dungeontileMap[g_tileSets->get("dungeon")->getByName("chest")] = MAP_DNG4_CON;
1036 // chest tile doesn't work that well
1037 dungeontileMap[g_tileSets->get("dungeon")->getByName("dungeon_door")] = MAP_DNG5_CON;
1038 dungeontileMap[g_tileSets->get("dungeon")->getByName("secret_door")] = MAP_DNG6_CON;
1039 }
1040
1041 if (g_context->_location->_context & CTX_DUNGEON) {
1042 if (dungeontileMap.find(groundTile) != dungeontileMap.end())
1043 return dungeontileMap[groundTile];
1044
1045 return MAP_DNG0_CON;
1046 }
1047
1048 if (transport->isShip() || (objUnder && objUnder->getTile().getTileType()->isShip()))
1049 fromShip = true;
1050 if (obj->getTile().getTileType()->isPirateShip())
1051 toShip = true;
1052
1053 if (fromShip && toShip)
1054 return MAP_SHIPSHIP_CON;
1055
1056 /* We can fight creatures and townsfolk */
1057 if (obj->getType() != Object::UNKNOWN) {
1058 const Tile *tileUnderneath = g_context->_location->_map->tileTypeAt(obj->getCoords(), WITHOUT_OBJECTS);
1059
1060 if (toShip)
1061 return MAP_SHORSHIP_CON;
1062 else if (fromShip && tileUnderneath->isWater())
1063 return MAP_SHIPSEA_CON;
1064 else if (tileUnderneath->isWater())
1065 return MAP_SHORE_CON;
1066 else if (fromShip && !tileUnderneath->isWater())
1067 return MAP_SHIPSHOR_CON;
1068 }
1069
1070 if (tileMap.find(groundTile) != tileMap.end())
1071 return tileMap[groundTile];
1072
1073 return MAP_BRICK_CON;
1074 }
1075
1076 } // End of namespace Ultima4
1077 } // End of namespace Ultima
1078