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