From d7e84be1d3dc935f47cefc0f600ced74f37e46fb Mon Sep 17 00:00:00 2001 From: Philipp Sehmisch Date: Sat, 30 Jun 2007 15:01:50 +0000 Subject: Implemented basic monster AI and fixed a stability problem caused by the spawn areas. --- src/defines.h | 2 +- src/game-server/accountconnection.cpp | 1 + src/game-server/being.cpp | 21 +++- src/game-server/being.hpp | 8 +- src/game-server/character.cpp | 2 + src/game-server/deathlistener.hpp | 5 + src/game-server/map.cpp | 47 +++++---- src/game-server/map.hpp | 22 +++- src/game-server/mapcomposite.cpp | 8 ++ src/game-server/mapreader.cpp | 3 +- src/game-server/monster.cpp | 192 +++++++++++++++++++++++++++++++--- src/game-server/monster.hpp | 64 +++++++++++- src/game-server/spawnarea.cpp | 45 +++++--- src/game-server/spawnarea.hpp | 2 + src/game-server/state.cpp | 3 +- src/game-server/thing.hpp | 16 ++- 16 files changed, 370 insertions(+), 71 deletions(-) (limited to 'src') diff --git a/src/defines.h b/src/defines.h index 73e50ed3..de5d1025 100644 --- a/src/defines.h +++ b/src/defines.h @@ -159,7 +159,7 @@ enum { GPMSG_BEINGS_MOVE = 0x0280, // { W being id, B flags [, C position] [, W*2 destination] }* GPMSG_ITEMS = 0x0281, // { W item id, W*2 position }* PGMSG_ATTACK = 0x0290, // B direction - GPMSG_BEING_ATTACK = 0x0291, // W being id + GPMSG_BEING_ATTACK = 0x0291, // W being id, B direction PGMSG_SAY = 0x02A0, // S text GPMSG_SAY = 0x02A1, // W being id, S text PGMSG_USE_ITEM = 0x0300, // L item id diff --git a/src/game-server/accountconnection.cpp b/src/game-server/accountconnection.cpp index e9ea2256..3ae5816e 100644 --- a/src/game-server/accountconnection.cpp +++ b/src/game-server/accountconnection.cpp @@ -72,6 +72,7 @@ void AccountConnection::processMessage(MessageIn &msg) std::string token = msg.readString(MAGIC_TOKEN_LENGTH); Character *ptr = new Character(msg); ptr->setSpeed(150); // TODO + ptr->fillHitpoints();// TODO: the current hit points should be saved in the database. Otherwise players could heal their characters by logging in and out again. gameHandler->mTokenCollector.addPendingConnect(token, ptr); } break; diff --git a/src/game-server/being.cpp b/src/game-server/being.cpp index 82457524..5bb608c6 100644 --- a/src/game-server/being.cpp +++ b/src/game-server/being.cpp @@ -36,11 +36,20 @@ Being::Being(int type, int id): } Being::~Being() -{} +{ + // Notify death listeners + DeathListeners::iterator i_end = mDeathListeners.end(); + DeathListeners::iterator i; + for (i = mDeathListeners.begin(); i != i_end; ++i) + { + (*i)->deleted(this); + } -void Being::damage(Damage damage) +} + +int Being::damage(Damage damage) { - if (mAction == DEAD) return; + if (mAction == DEAD) return 0; // TODO: Implement dodge chance @@ -73,6 +82,8 @@ void Being::damage(Damage damage) LOG_INFO("Being " << getPublicID() << " got hit"); if (mHitpoints == 0) die(); + + return HPloss; } void Being::die() @@ -109,8 +120,8 @@ void Being::move() void Being::performAttack(MapComposite *map) { - int SHORT_RANGE = 64; - int SMALL_ANGLE = 45; + int SHORT_RANGE = 60; + int SMALL_ANGLE = 35; Point ppos = getPosition(); int dir = getDirection(); diff --git a/src/game-server/being.hpp b/src/game-server/being.hpp index e0d064bd..3872e661 100644 --- a/src/game-server/being.hpp +++ b/src/game-server/being.hpp @@ -60,7 +60,7 @@ enum Element /** * Beings and actors directions */ -enum +enum Direction { DIRECTION_DOWN = 1, DIRECTION_UP, @@ -154,7 +154,7 @@ class Being : public MovingObject * stats, deducts the result from the hitpoints and adds the result to * the HitsTaken list. */ - void damage(Damage); + virtual int damage(Damage damage); /** * Kills the being @@ -239,15 +239,13 @@ class Being : public MovingObject int mHitpoints; /**< Hitpoints of the being */ Action mAction; - std::vector mAttributes; + std::list mDeathListeners; private: Being(Being const &rhs); Being &operator=(Being const &rhs); - std::list mDeathListeners; - Hits mHitsTaken; /**< List of punches taken since last update */ }; diff --git a/src/game-server/character.cpp b/src/game-server/character.cpp index f05ebf1e..c33300b0 100644 --- a/src/game-server/character.cpp +++ b/src/game-server/character.cpp @@ -49,6 +49,8 @@ Character::Character(MessageIn & msg): deserialize(msg); // give the player 10 weapon skill for testing purpose setAttribute(CHAR_SKILL_WEAPON_UNARMED, 10); + + setSize(16); } /** * Update the internal status. diff --git a/src/game-server/deathlistener.hpp b/src/game-server/deathlistener.hpp index b220b846..462f36da 100644 --- a/src/game-server/deathlistener.hpp +++ b/src/game-server/deathlistener.hpp @@ -42,6 +42,11 @@ class DeathListener * Called when a being died. */ virtual void died(Being *being) = 0; + + /** + * Called when a being is deleted. + */ + virtual void deleted(Being *being) = 0; }; typedef std::list DeathListeners; diff --git a/src/game-server/map.cpp b/src/game-server/map.cpp index 6ca447ae..447bc810 100644 --- a/src/game-server/map.cpp +++ b/src/game-server/map.cpp @@ -73,33 +73,34 @@ Map::setSize(int width, int height) } void -Map::setWalk(int x, int y, bool walkable) +Map::setPermWalk(int x, int y, bool walkable) { - metaTiles[x + y * width].walkable = walkable; + metaTiles[x + y * width].permWalkable = walkable; } -bool -Map::getWalk(int x, int y) +void +Map::setTempWalk(int x, int y, bool walkable) { - // If walkable, check for colliding into a being - if (!tileCollides(x, y)) + metaTiles[x + y * width].tempWalkable = walkable; +} + +void +Map::resetTempWalk() +{ + for (int i = 0; i < width * height; i++) { - /* - std::list::iterator i = beings.begin(); - while (i != beings.end()) { - Being *being = (*i); - // Collision when non-portal being is found at this location - if (being->x == x && being->y == y && being->job != 45) { - return false; - } - i++; - } - */ - return true; + metaTiles[i].tempWalkable = metaTiles[i].permWalkable; } - else { +} + +bool +Map::getWalk(int x, int y) +{ + // You can't walk outside of the map + if (x < 0 || y < 0 || x >= width || y >= height) { return false; } + return metaTiles[x + y * width].tempWalkable; } bool @@ -111,7 +112,7 @@ Map::tileCollides(int x, int y) } // Check if the tile is walkable - return !metaTiles[x + y * width].walkable; + return !metaTiles[x + y * width].permWalkable; } MetaTile* @@ -123,7 +124,7 @@ Map::getMetaTile(int x, int y) static int const basicCost = 100; std::list -Map::findPath(int startX, int startY, int destX, int destY) +Map::findPath(int startX, int startY, int destX, int destY, int maxCost) { // Path to be built up (empty by default) std::list path; @@ -194,7 +195,7 @@ Map::findPath(int startX, int startY, int destX, int destY) MetaTile *t1 = getMetaTile(curr.x, curr.y + dy); MetaTile *t2 = getMetaTile(curr.x + dx, curr.y); - if (!(t1->walkable && t2->walkable)) + if (!(t1->tempWalkable && t2->tempWalkable)) { continue; } @@ -220,7 +221,7 @@ Map::findPath(int startX, int startY, int destX, int destY) // Skip if Gcost becomes too much // Warning: probably not entirely accurate - if (Gcost > 20 * basicCost) + if (Gcost > maxCost * basicCost) { continue; } diff --git a/src/game-server/map.hpp b/src/game-server/map.hpp index a7c1fe2a..b0840b2c 100644 --- a/src/game-server/map.hpp +++ b/src/game-server/map.hpp @@ -57,7 +57,8 @@ class MetaTile int whichList; /**< No list, open list or closed list */ int parentX; /**< X coordinate of parent tile */ int parentY; /**< Y coordinate of parent tile */ - bool walkable; /**< Can beings walk on this tile */ + bool permWalkable; /**< Can beings normally walk on this tile */ + bool tempWalkable; /**< Can beings walk on this tile this tick? */ }; /** @@ -114,10 +115,22 @@ class Map getMetaTile(int x, int y); /** - * Set walkability flag for a tile + * Set permanent walkability flag for a tile */ void - setWalk(int x, int y, bool walkable); + setPermWalk(int x, int y, bool walkable); + + /** + * Set temporary walkability flag for a tile + */ + void + setTempWalk(int x, int y, bool walkable); + + /** + * Resets the temporary walkable status of all tiles to the permanent + * walkable status. + */ + void resetTempWalk(); /** * Tell if a tile is walkable or not, includes checking beings. @@ -159,8 +172,7 @@ class Map * Find a path from one location to the next. */ std::list - findPath(int startX, int startY, - int destX, int destY); + findPath(int startX, int startY, int destX, int destY, int maxCost = 20); private: int width, height; diff --git a/src/game-server/mapcomposite.cpp b/src/game-server/mapcomposite.cpp index 5ca2330b..027db924 100644 --- a/src/game-server/mapcomposite.cpp +++ b/src/game-server/mapcomposite.cpp @@ -29,6 +29,8 @@ #include "game-server/mapcomposite.hpp" #include "game-server/character.hpp" +#include "utils/logger.h" + /* TODO: Implement overlapping map zones instead of strict partitioning. Purpose: to decrease the number of zone changes, as overlapping allows for hysteresis effect and prevents an object from changing zone each server @@ -481,6 +483,7 @@ bool MapComposite::insert(Thing *ptr) zones[(pos.x / zoneDiam) + (pos.y / zoneDiam) * mapWidth].insert(obj); } + ptr->setMap(this); things.push_back(ptr); return true; } @@ -514,6 +517,8 @@ void MapComposite::remove(Thing *ptr) void MapComposite::update() { + map->resetTempWalk(); + for (int i = 0; i < mapHeight * mapWidth; ++i) { zones[i].destinations.clear(); @@ -531,6 +536,9 @@ void MapComposite::update() Point const &pos1 = obj->getOldPosition(), &pos2 = obj->getPosition(); + + map->setTempWalk(pos2.x / 32, pos2.y / 32, false); + int src = (pos1.x / zoneDiam) + (pos1.y / zoneDiam) * mapWidth, dst = (pos2.x / zoneDiam) + (pos2.y / zoneDiam) * mapWidth; if (src != dst) diff --git a/src/game-server/mapreader.cpp b/src/game-server/mapreader.cpp index cc26f03f..db16cc3e 100644 --- a/src/game-server/mapreader.cpp +++ b/src/game-server/mapreader.cpp @@ -141,6 +141,7 @@ static Map *readMap(xmlNodePtr node, std::string const &path) // Clean up tilesets tilesetFirstGids.clear(); + map->resetTempWalk(); return map; } @@ -258,5 +259,5 @@ static void setTileWithGid(Map *map, int x, int y, int gid) set = *i; } - map->setWalk(x, y, gid == set); + map->setPermWalk(x, y, gid == set); } diff --git a/src/game-server/monster.cpp b/src/game-server/monster.cpp index de277ee7..4881a9b7 100644 --- a/src/game-server/monster.cpp +++ b/src/game-server/monster.cpp @@ -22,24 +22,145 @@ #include "game-server/monster.hpp" +#include "game-server/collisiondetection.hpp" +#include "game-server/mapcomposite.hpp" + #include "utils/logger.h" Monster::Monster(): Being(OBJECT_MONSTER, 65535), - mCountDown(0) + mCountDown(0), + mAttackTime(0), + mAttackPreDelay(5), + mAttackAftDelay(10) { LOG_DEBUG("Monster spawned!"); + mAgressive = false; // TODO: get from monster database + mAgressionRange = 10; // TODO: get from monster database mAttributes.resize(NB_ATTRIBUTES_CONTROLLED, 1); // TODO: fill with the real attributes + + // some bogus values for testing monster attacks on players + setAttribute(BASE_ATTR_STRENGTH, 10); + setAttribute(MONSTER_SKILL_WEAPON, 3); + + // set positions relative to target from which the monster can attack + mAttackPositions.push_back(AttackPosition(+32, 0, DIRECTION_LEFT)); + mAttackPositions.push_back(AttackPosition(-32, 0, DIRECTION_RIGHT)); + mAttackPositions.push_back(AttackPosition(0, +32, DIRECTION_DOWN)); + mAttackPositions.push_back(AttackPosition(0, -32, DIRECTION_UP)); +} + +Monster::~Monster() +{ + // deactivate death listeners + std::map::iterator i; + for (i = mAnger.begin(); i != mAnger.end(); i++) + { + i->first->removeDeathListener(this); + } } void Monster::update() { - /* Temporary "AI" behaviour that is purely artificial and not at all - * intelligent. - */ - if (mCountDown == 0) + // if dead do nothing but rot + if (mAction == DEAD) + { + mCountDown--; + if (mCountDown <= 0) + { + raiseUpdateFlags(UPDATEFLAG_REMOVE); + } + return; + } + + // if currently attacking finish attack; + if (mAttackTime) + { + if (mAttackTime == mAttackAftDelay) + { + mAction = ATTACK; + raiseUpdateFlags(UPDATEFLAG_ATTACK); + } + mAttackTime--; + return; + } + + // check potential attack positions + Being *bestAttackTarget = NULL; + int bestTargetPriority = 0; + Point bestAttackPosition; + Direction bestAttackDirection = DIRECTION_DOWN; + + // iterate through objects nearby + for (MovingObjectIterator i(mMap->getAroundCharacterIterator(this, AROUND_AREA)); i; ++i) + { + // we only want to attack player characters + if ((*i)->getType() != OBJECT_CHARACTER) continue; + + Being *target = static_cast (*i); + + // dead characters are ignored + if (target->getAction() == DEAD) continue; + + // determine how much we hate the target + int targetPriority = 0; + std::map >::iterator angerIterator; + angerIterator = mAnger.find(target); + if (angerIterator != mAnger.end()) + { + targetPriority = angerIterator->second; + } + else if (mAgressive) + { + targetPriority = 1; + } + else + { + continue; + } + + // check all attack positions + for (std::list::iterator j = mAttackPositions.begin(); + j != mAttackPositions.end(); + j++) + { + Point attackPosition = (*i)->getPosition(); + attackPosition.x += (*j).x; + attackPosition.y += (*j).y; + + int posPriority = calculatePositionPriority(attackPosition, + targetPriority); + if (posPriority > bestTargetPriority) + { + bestAttackTarget = target; + bestTargetPriority = posPriority; + bestAttackPosition = attackPosition; + bestAttackDirection = (*j).direction; + } + } + } + + // check if an attack position has been found + if (bestAttackTarget) + { + // check if we are there + if (bestAttackPosition == getPosition()) + { + // we are there - let's get ready to beat the crap out of the target + setDirection(bestAttackDirection); + mAttackTime = mAttackPreDelay + mAttackAftDelay; + } + else + { + // we aren't there yet - let's move + setDestination(bestAttackPosition); + } + } + else { - if (mAction != DEAD) + // we have no target - let's wander around + mCountDown--; + if (mCountDown <= 0) { Point randomPos(rand() % 160 - 80 + getPosition().x, rand() % 160 - 80 + getPosition().y); @@ -49,20 +170,67 @@ void Monster::update() LOG_DEBUG("Setting new random destination " << randomPos.x << "," << randomPos.y << " for being " << getPublicID()); } - else - { - raiseUpdateFlags(UPDATEFLAG_REMOVE); - } + } +} + +int Monster::calculatePositionPriority(Point position, int targetPriority) +{ + Point thisPos = getPosition(); + + // check if we already are on this position + if (thisPos.x / 32 == position.x / 32 && + thisPos.y / 32 == position.y / 32) + { + return targetPriority *= mAgressionRange; + } + + std::list path; + path = mMap->getMap()->findPath(thisPos.x / 32, + thisPos.y / 32, + position.x / 32, + position.y / 32, + mAgressionRange); + + if (path.empty() || path.size() >= mAgressionRange) + { + return 0; } else { - mCountDown--; + return targetPriority * (mAgressionRange - path.size()); + } +} + +void Monster::died (Being *being) +{ + mAnger.erase(being); + mDeathListeners.remove((DeathListener *)being); +} + +int Monster::damage(Damage damage) +{ + int HPLoss = Being::damage(damage); + if ( HPLoss + && damage.source + && damage.source->getType() == OBJECT_CHARACTER + ) + { + if (mAnger.find(damage.source) == mAnger.end()) + { + damage.source->addDeathListener(this); + mAnger[damage.source] = HPLoss; + } + else + { + mAnger[damage.source] += HPLoss; + } } + return HPLoss; } void Monster::die() { - mCountDown = 50; //sets remove time to 5 seconds + mCountDown = 50; // sets remove time to 5 seconds Being::die(); } diff --git a/src/game-server/monster.hpp b/src/game-server/monster.hpp index c92a7698..616aa9d4 100644 --- a/src/game-server/monster.hpp +++ b/src/game-server/monster.hpp @@ -23,12 +23,35 @@ #ifndef _TMWSERV_MONSTER_H_ #define _TMWSERV_MONSTER_H_ +#include + #include "game-server/being.hpp" +#include "game-server/deathlistener.hpp" + +class MapComposite; +class MovingObject; + +/** + * Structure holding possible positions relative to the target from which + * the monster can attack + */ +struct AttackPosition +{ + AttackPosition(int posX, int posY, Direction dir): + x(posX), + y(posY), + direction(dir) + {}; + + int x; + int y; + Direction direction; +}; /** * The class for a fightable monster with its own AI */ -class Monster : public Being +class Monster : public Being, public DeathListener { public: /** @@ -36,6 +59,11 @@ class Monster : public Being */ Monster(); + /** + * Destructor. + */ + ~Monster(); + /** * Performs one step of controller logic. */ @@ -46,6 +74,25 @@ class Monster : public Being */ virtual void die(); + /** + * Calls the damage function in Being and updates the aggro list + */ + virtual int damage(Damage damage); + + /** + * Getting informed that a being that might be on the target list died + */ + virtual void died(Being *being); + + /** + * Getting informed that a being that might be on the target list is + * deleted + */ + virtual void deleted(Being *being) + { + died(being); + } + protected: /** * Gets the stats of the currently equipped weapon that are relevant @@ -59,8 +106,19 @@ class Monster : public Being void calculateDerivedAttributes(); private: - /** Count down till next random movement (temporary). */ - unsigned int mCountDown; + int calculatePositionPriority(Point position, int targetPriority); + + int mCountDown; /**< Count down till next random movement (temporary). */ + std::map mAnger; /**< Aggression towards other beings */ + int mAttackTime; /**< Delay until monster can attack */ + // TODO: the following vars should all be the same for all monsters of + // the same type. So they should be put into some central data structure + // to save memory. + int mAttackPreDelay; /**< time between decision to make an attack and performing the attack */ + int mAttackAftDelay; /**< time it takes to perform an attack */ + bool mAgressive; /**< Does the monster attack without being provoked? */ + unsigned mAgressionRange; /**< Distance the monster tracks enemies in */ + std::list mAttackPositions; /**< set positions relative to target from which the monster can attack */ }; #endif // _TMWSERV_MONSTER_H_ diff --git a/src/game-server/spawnarea.cpp b/src/game-server/spawnarea.cpp index 9d7687ff..780e3e65 100644 --- a/src/game-server/spawnarea.cpp +++ b/src/game-server/spawnarea.cpp @@ -23,6 +23,7 @@ #include "spawnarea.hpp" +#include "game-server/mapcomposite.hpp" #include "game-server/monster.hpp" #include "game-server/state.hpp" @@ -53,22 +54,40 @@ SpawnArea::update() if (mNextSpawn == 0) { - Being *being = new Monster(); - being->addDeathListener(this); + //find a free spawn location. Give up after 10 tries + int c = 10; + Point position; + do + { + position = Point(mZone.x + rand() % mZone.w, + mZone.y + rand() % mZone.h); + c--; + } while (! mMap->getMap()->getWalk(position.x / 32, position.y / 32) + && c); - // some bogus stats for testing - being->setSpeed(150); - being->setSize(8); - being->setAttribute(BASE_ATTR_VITALITY, 10); - being->fillHitpoints(); + if (c >= 0) + { + Being *being = new Monster(); + being->addDeathListener(this); - being->setMapId(1); - being->setPosition(Point(mZone.x + rand() % mZone.w, - mZone.y + rand() % mZone.h)); - DelayedEvent e = { EVENT_INSERT }; - gameState->enqueueEvent(being, e); + // some bogus stats for testing + being->setSpeed(150); + being->setSize(8); + being->setAttribute(BASE_ATTR_VITALITY, 10); + being->fillHitpoints(); - mNumBeings++; + being->setMapId(1); + being->setPosition(position); + DelayedEvent e = { EVENT_INSERT }; + gameState->enqueueEvent(being, e); + + mNumBeings++; + } + else { + //TODO: This log message should have more information when + // more flexibility is added to the spawn area + LOG_WARN("Unable to find a free spawn location for monster"); + } } } diff --git a/src/game-server/spawnarea.hpp b/src/game-server/spawnarea.hpp index 1debb1b3..2f8ff51a 100644 --- a/src/game-server/spawnarea.hpp +++ b/src/game-server/spawnarea.hpp @@ -45,6 +45,8 @@ class SpawnArea : public Thing, public DeathListener virtual void died(Being *being); + virtual void deleted(Being *being) {}; + protected: Rectangle mZone; int mMaxBeings; /**< Maximum population of this area. */ diff --git a/src/game-server/state.cpp b/src/game-server/state.cpp index 929d4304..c27f9209 100644 --- a/src/game-server/state.cpp +++ b/src/game-server/state.cpp @@ -69,7 +69,7 @@ void State::updateMap(MapComposite *map) (*i)->move(); } - // 4. remove dead beings + // 4. remove dead beings. for (MovingObjectIterator i(map->getWholeMapIterator()); i; ++i) { if ((*i)->getUpdateFlags() & UPDATEFLAG_REMOVE) @@ -79,6 +79,7 @@ void State::updateMap(MapComposite *map) } } + // 5. update the map itself. map->update(); } diff --git a/src/game-server/thing.hpp b/src/game-server/thing.hpp index 547885c4..1d95c23f 100644 --- a/src/game-server/thing.hpp +++ b/src/game-server/thing.hpp @@ -23,6 +23,8 @@ #ifndef _TMWSERV_THING_H_ #define _TMWSERV_THING_H_ +class MapComposite; + /** * Object type enumeration. */ @@ -50,7 +52,8 @@ class Thing * Constructor. */ Thing(int type) - : mType(type) + : mMap(NULL), + mType(type) {} /** @@ -100,11 +103,20 @@ class Thing { return mMapId; } /** - * Sets the map this thing is located on. + * Sets the map ID this thing is located on. */ void setMapId(int mapId) { mMapId = mapId; } + /** + * Sets the map this thing is located on. + */ + void setMap(MapComposite *map) + { mMap = map; } + + protected: + MapComposite *mMap; /**< Map the thing is on */ + private: unsigned short mMapId; /**< ID of the map this thing is on. */ char mType; /**< Type of this thing. */ -- cgit v1.2.3-60-g2f50