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