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/ultima1/widgets/dungeon_monster.h"
24 #include "ultima/ultima1/maps/map.h"
25 #include "ultima/ultima1/maps/map_dungeon.h"
26 #include "ultima/ultima1/maps/map_tile.h"
27 #include "ultima/ultima1/core/resources.h"
28 #include "ultima/ultima1/game.h"
29 #include "ultima/shared/core/utils.h"
30 #include "ultima/shared/early/ultima_early.h"
31 
32 namespace Ultima {
33 namespace Ultima1 {
34 namespace Widgets {
35 
DungeonMonster(Ultima1Game * game,Maps::MapBase * map,DungeonWidgetId monsterId,int hitPoints,const Point & pt)36 DungeonMonster::DungeonMonster(Ultima1Game *game, Maps::MapBase *map, DungeonWidgetId monsterId,
37 		int hitPoints, const Point &pt) :
38 		DungeonWidget(game, map, monsterId, pt), Shared::Maps::DungeonCreature(game, map, hitPoints) {
39 	_name = getGame()->_res->DUNGEON_MONSTER_NAMES[_widgetId];
40 }
41 
DungeonMonster(Ultima1Game * game,Maps::MapBase * map)42 DungeonMonster::DungeonMonster(Ultima1Game *game, Maps::MapBase *map) :
43 		DungeonWidget(game, map), Shared::Maps::DungeonCreature(game, map) {
44 }
45 
synchronize(Common::Serializer & s)46 void DungeonMonster::synchronize(Common::Serializer &s) {
47 	DungeonWidget::synchronize(s);
48 	Creature::synchronize(s);
49 	s.syncAsUint16LE(_widgetId);
50 
51 	if (s.isLoading())
52 		_name = getGame()->_res->DUNGEON_MONSTER_NAMES[_widgetId];
53 }
54 
isBlockingView() const55 bool DungeonMonster::isBlockingView() const {
56 	return _widgetId != MONSTER_INVISIBLE_SEEKER && _widgetId != MONSTER_MIMIC
57 		&& _widgetId != MONSTER_GELATINOUS_CUBE;
58 }
59 
draw(Shared::DungeonSurface & s,uint distance)60 void DungeonMonster::draw(Shared::DungeonSurface &s, uint distance) {
61 	if (distance < 5) {
62 		if (_widgetId == MONSTER_GELATINOUS_CUBE) {
63 			s.drawWall(distance);
64 			s.drawLeftEdge(distance);
65 			s.drawRightEdge(distance);
66 		} else {
67 			DungeonWidget::draw(s, distance);
68 		}
69 	}
70 }
71 
update(bool isPreUpdate)72 void DungeonMonster::update(bool isPreUpdate) {
73 	assert(isPreUpdate);
74 	Point playerPos = _map->_playerWidget->_position;
75 	Point delta = playerPos - _position;
76 	int distance = ABS(delta.x) + ABS(delta.y);
77 
78 	if (distance == 1) {
79 		attackParty();
80 	} else if (distance < 8) {
81 		movement();
82 	}
83 }
84 
movement()85 void DungeonMonster::movement() {
86 	if (attackDistance())
87 		// Dungeon monsters don't move if they're already in attack range
88 		return;
89 
90 	Point playerPos = _map->_playerWidget->_position;
91 	Point diff = playerPos - _position;
92 
93 	if (diff.x != 0 && canMoveTo(Point(_position.x + SGN(diff.x), _position.y)))
94 		_position.x += SGN(diff.x);
95 	else if (diff.y != 0 && canMoveTo(Point(_position.x, _position.y + SGN(diff.y))))
96 		_position.y += SGN(diff.y);
97 }
98 
canMoveTo(const Point & destPos)99 Shared::Maps::MapWidget::CanMove DungeonMonster::canMoveTo(const Point &destPos) {
100 	Shared::Maps::MapWidget::CanMove result = MapWidget::canMoveTo(destPos);
101 	if (result != UNSET)
102 		return result;
103 
104 	return DungeonMonster::canMoveTo(_map, this, destPos);
105 }
106 
canMoveTo(Shared::Maps::MapBase * map,MapWidget * widget,const Point & destPos)107 Shared::Maps::MapWidget::CanMove DungeonMonster::canMoveTo(Shared::Maps::MapBase *map, MapWidget *widget, const Point &destPos) {
108 	// Get the details of the position
109 	Shared::Maps::MapTile currTile, destTile;
110 
111 	map->getTileAt(map->getPosition(), &currTile);
112 	map->getTileAt(destPos, &destTile);
113 
114 	// Can't move onto certain dungeon tile types
115 	if (destTile._isWall || destTile._isSecretDoor || destTile._isBeams)
116 		return NO;
117 
118 	// Can't move to directly adjoining doorway cells (they'd be in parralel to each other, not connected)
119 	if (destTile._isDoor && currTile._isDoor)
120 		return NO;
121 
122 	return YES;
123 }
124 
attackParty()125 void DungeonMonster::attackParty() {
126 	Ultima1Game *game = static_cast<Ultima1Game *>(_game);
127 	Point playerPos = _map->_playerWidget->_position;
128 	//Point delta = playerPos - _position;
129 	Shared::Character &c = *_game->_party;
130 	uint threshold, damage;
131 	bool isHit = true;
132 
133 	// Get tile details for both the player and the attacking creature
134 	Maps::U1MapTile playerTile,creatureTile;
135 	_map->getTileAt(playerPos, &playerTile);
136 	_map->getTileAt(_position, &creatureTile);
137 
138 	if (playerTile._isBeams || (creatureTile._isDoor && (playerTile._isDoor || playerTile._isWall || playerTile._isSecretDoor)))
139 		return;
140 
141 	// Write attack line
142 	addInfoMsg(Common::String::format(game->_res->ATTACKED_BY, _name.c_str()));
143 	_game->playFX(3);
144 
145 	threshold = (c._stamina / 2) + (c._equippedArmour * 8) + 56;
146 
147 	if (_game->getRandomNumber(1, 255) > threshold) {
148 		threshold = _game->getRandomNumber(1, 255);
149 		damage = (_widgetId * _widgetId) + _map->getLevel();
150 		if (damage > 255) {
151 			damage = _game->getRandomNumber(_widgetId + 1, 255);
152 		}
153 
154 		if (_widgetId == MONSTER_GELATINOUS_CUBE && c.isArmourEquipped()) {
155 			addInfoMsg(game->_res->ARMOR_DESTROYED);
156 			c._armour[c._equippedArmour]->decrQuantity();
157 			c.removeArmour();
158 			isHit = false;
159 		} else if (_widgetId == MONSTER_GREMLIN) {
160 			addInfoMsg(game->_res->GREMLIN_STOLE);
161 			c._food /= 2;
162 			isHit = false;
163 		} else if (_widgetId == MONSTER_MIND_WHIPPER && threshold < 128) {
164 			addInfoMsg(game->_res->MENTAL_ATTACK);
165 			c._intelligence = (c._intelligence / 2) + 5;
166 			isHit = false;
167 		} else if (_widgetId == MONSTER_THIEF) {
168 			// Thief will steal the first spare weapon player has that isn't equipped
169 			for (int weaponNum = 1; weaponNum < (int)c._weapons.size(); ++weaponNum) {
170 				if (weaponNum != c._equippedWeapon && !c._weapons[weaponNum]->empty()) {
171 					// TODO: May need to worry about word wrapping long line
172 					addInfoMsg(Common::String::format(game->_res->THIEF_STOLE,
173 
174 						Shared::isVowel(c._weapons[weaponNum]->_longName.firstChar()) ? game->_res->AN : game->_res->A
175 					));
176 					c._weapons[weaponNum]->decrQuantity();
177 					break;
178 				}
179 			}
180 		}
181 
182 		if (isHit) {
183 			addInfoMsg(Common::String::format("%s %2d %s", game->_res->HIT, damage, game->_res->DAMAGE));
184 			c._hitPoints -= damage;
185 		}
186 	} else {
187 		addInfoMsg(game->_res->MISSED);
188 	}
189 }
190 
attackMonster(uint effectNum,uint agility,uint damage)191 void DungeonMonster::attackMonster(uint effectNum, uint agility, uint damage) {
192 	Ultima1Game *game = static_cast<Ultima1Game *>(_game);
193 	Maps::MapDungeon *map = static_cast<Maps::MapDungeon *>(_map);
194 	Point currPos = map->getPosition();
195 	Maps::U1MapTile playerTile, monsTile;
196 	map->getTileAt(currPos, &playerTile);
197 	map->getTileAt(_position, &monsTile);
198 
199 	bool flag = true;
200 	if (!playerTile._isDoor) {
201 		if (!monsTile._isHallway && !monsTile._isLadderUp && !monsTile._isLadderDown)
202 			flag = false;
203 	}
204 
205 	if (game->getRandomNumber(1, 100) <= agility && !playerTile._isWall && !playerTile._isSecretDoor
206 			&& !playerTile._isBeams && flag) {
207 		// Play effect and add hit message
208 		game->playFX(effectNum);
209 		if (damage != ITS_OVER_9000)
210 			addInfoMsg(Common::String::format("%s ", game->_res->HIT), false);
211 
212 		if ((int)damage < _hitPoints) {
213 			addInfoMsg(Common::String::format("%u %s!", damage, game->_res->DAMAGE));
214 			_hitPoints -= damage;
215 		} else {
216 			addInfoMsg(Common::String::format("%s %s", _name.c_str(),
217 				damage == ITS_OVER_9000 ? game->_res->DESTROYED : game->_res->KILLED));
218 			monsterDead();
219 
220 			// Give some treasure
221 			uint amount = game->getRandomNumber(2, map->getLevel() * 3 + (uint)_widgetId + 10);
222 			addInfoMsg(game->_res->THOU_DOST_FIND);
223 			game->giveTreasure(amount, 0);
224 
225 			// Give experience
226 			Shared::Character &c = *game->_party;
227 			uint experience = game->getRandomNumber(2, map->getLevel() * map->getLevel() + 10);
228 			c._experience += experience;
229 			map->_dungeonExitHitPoints = MIN(map->_dungeonExitHitPoints + experience * 2, 9999U);
230 
231 			// Delete the monster and create a new one
232 			map->removeWidget(this);
233 			map->spawnMonster();
234 		}
235 	} else {
236 		// Attack missed
237 		addInfoMsg(game->_res->MISSED);
238 	}
239 }
240 
monsterDead()241 void DungeonMonster::monsterDead() {
242 	int index;
243 	switch (_widgetId) {
244 	case MONSTER_BALRON:
245 		index = 8;
246 		break;
247 	case MONSTER_CARRION_CREEPER:
248 		index = 4;
249 		break;
250 	case MONSTER_LICH:
251 		index = 6;
252 		break;
253 	case MONSTER_GELATINOUS_CUBE:
254 		index = 2;
255 		break;
256 	default:
257 		index = 0;
258 		break;
259 	}
260 
261 	if (index) {
262 		// Mark monster-based quests as complete if in progress
263 		Ultima1Game *game = static_cast<Ultima1Game *>(_game);
264 		game->_quests[8 - index].complete();
265 	}
266 }
267 
268 } // End of namespace Widgets
269 } // End of namespace Ultima1
270 } // End of namespace Ultima
271