487 lines
9.8 KiB
C++

/**
* Tibia GIMUD Server - a free and open-source MMORPG server emulator
* Copyright (C) 2019 Sabrehaven and Mark Samman <mark.samman@gmail.com>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "otpch.h"
#include "npc.h"
#include "game.h"
#include "tools.h"
#include "position.h"
#include "player.h"
#include "spawn.h"
#include "script.h"
#include "behaviourdatabase.h"
extern Game g_game;
uint32_t Npc::npcAutoID = 0x80000000;
void Npcs::loadNpcs()
{
std::cout << ">> Loading npcs..." << std::endl;
std::vector<boost::filesystem::path> files;
getFilesInDirectory("data/npc/", ".npc", files);
for (auto file : files)
{
std::string npcName = file.filename().string();
int32_t end = npcName.find_first_of('/');
npcName = npcName.substr(end + 1, npcName.length() - end);
end = npcName.find_first_of('.');
npcName = npcName.substr(0, end);
Npc* npc = Npc::createNpc(npcName);
if (!npc) {
return;
}
if (npc->getClientVersion() <= g_game.getClientVersion()) {
g_game.placeCreature(npc, npc->getMasterPos(), false, true);
}
}
}
void Npcs::reload()
{
const std::map<uint32_t, Npc*>& npcs = g_game.getNpcs();
for (const auto& it : npcs) {
it.second->reload();
}
}
Npc* Npc::createNpc(const std::string& name)
{
std::unique_ptr<Npc> npc(new Npc(name));
npc->filename = "data/npc/" + name + ".npc";
if (!npc->load()) {
return nullptr;
}
return npc.release();
}
Npc::Npc(const std::string& name) :
Creature(),
filename("data/npc/" + name + ".npc"),
masterRadius(0),
staticMovementTime(0),
loaded(false),
behaviourDatabase(nullptr)
{
baseSpeed = 5;
reset();
}
Npc::~Npc()
{
reset();
}
void Npc::addList()
{
g_game.addNpc(this);
}
void Npc::removeList()
{
g_game.removeNpc(this);
}
bool Npc::load()
{
if (loaded) {
return true;
}
reset();
ScriptReader script;
if (!script.open(filename)) {
return false;
}
while (true) {
script.nextToken();
if (script.Token == ENDOFFILE) {
break;
}
if (script.Token != IDENTIFIER) {
script.error("identifier expected");
return false;
}
std::string ident = script.getIdentifier();
script.readSymbol('=');
if (ident == "name") {
name = script.readString();
} else if (ident == "outfit") {
script.readSymbol('(');
uint8_t* c;
currentOutfit.lookType = script.readNumber();
script.readSymbol(',');
if (currentOutfit.lookType > 0) {
c = script.readBytesequence();
currentOutfit.lookHead = c[0];
currentOutfit.lookBody = c[1];
currentOutfit.lookLegs = c[2];
currentOutfit.lookFeet = c[3];
currentOutfit.lookAddons = c[4];
} else {
currentOutfit.lookTypeEx = script.readNumber();
}
script.readSymbol(')');
} else if (ident == "home") {
script.readCoordinate(masterPos.x, masterPos.y, masterPos.z);
} else if (ident == "radius") {
masterRadius = script.readNumber();
} else if (ident == "clientversion") {
clientVersion = script.readNumber();
} else if (ident == "behaviour") {
if (behaviourDatabase) {
script.error("behaviour database already defined");
return false;
}
behaviourDatabase = new BehaviourDatabase(this);
if (!behaviourDatabase->loadDatabase(script)) {
return false;
}
}
}
script.close();
return true;
}
void Npc::reset()
{
loaded = false;
focusCreature = 0;
conversationEndTime = 0;
if (behaviourDatabase) {
delete behaviourDatabase;
behaviourDatabase = nullptr;
}
}
void Npc::reload()
{
loaded = false;
reset();
load();
if (baseSpeed > 0) {
addEventWalk();
}
}
bool Npc::canSee(const Position& pos) const
{
if (pos.z != getPosition().z) {
return false;
}
return Creature::canSee(getPosition(), pos, 3, 3);
}
std::string Npc::getDescription(int32_t) const
{
std::string descr;
descr.reserve(name.length() + 1);
descr.assign(name);
descr.push_back('.');
return descr;
}
void Npc::onCreatureAppear(Creature* creature, bool isLogin)
{
Creature::onCreatureAppear(creature, isLogin);
if (creature == this) {
if (baseSpeed > 0) {
addEventWalk();
}
} else if (Player* player = creature->getPlayer()) {
spectators.insert(player);
updateIdleStatus();
}
}
void Npc::onRemoveCreature(Creature* creature, bool isLogout)
{
Creature::onRemoveCreature(creature, isLogout);
if (!behaviourDatabase) {
return;
}
Player* player = creature->getPlayer();
if (player) {
if (player->getID() == focusCreature) {
behaviourDatabase->react(SITUATION_VANISH, player, "");
}
spectators.erase(player);
updateIdleStatus();
}
}
void Npc::onCreatureMove(Creature* creature, const Tile* newTile, const Position& newPos,
const Tile* oldTile, const Position& oldPos, bool teleport)
{
Creature::onCreatureMove(creature, newTile, newPos, oldTile, oldPos, teleport);
if (!behaviourDatabase) {
return;
}
Player* player = creature->getPlayer();
if (player && player->getID() == focusCreature) {
if (!Position::areInRange<3, 3, 0>(creature->getPosition(), getPosition())) {
behaviourDatabase->react(SITUATION_VANISH, player, "");
}
}
if (creature != this) {
if (player) {
if (player->canSee(position)) {
spectators.insert(player);
} else {
spectators.erase(player);
}
updateIdleStatus();
}
}
}
void Npc::onCreatureSay(Creature* creature, SpeakClasses type, const std::string& text)
{
if (creature->getID() == id || type != TALKTYPE_SAY || !behaviourDatabase) {
return;
}
Player* player = creature->getPlayer();
if (player) {
if (!Position::areInRange<3, 3>(creature->getPosition(), getPosition())) {
return;
}
lastTalkCreature = creature->getID();
if (focusCreature == 0) {
behaviourDatabase->react(SITUATION_ADDRESS, player, text);
} else if (focusCreature != player->getID()) {
behaviourDatabase->react(SITUATION_BUSY, player, text);
} else if (focusCreature == player->getID()) {
behaviourDatabase->react(SITUATION_NONE, player, text);
}
}
}
void Npc::onThink(uint32_t interval)
{
Creature::onThink(interval);
if (!isIdle && focusCreature == 0 && baseSpeed > 0 && getTimeSinceLastMove() >= 100 + getStepDuration()) {
addEventWalk();
}
if (!behaviourDatabase) {
return;
}
if (focusCreature) {
Player* player = g_game.getPlayerByID(focusCreature);
if (player) {
turnToCreature(player);
if (conversationEndTime != 0 && OTSYS_TIME() > conversationEndTime) {
if (player) {
behaviourDatabase->react(SITUATION_VANISH, player, "");
}
}
}
}
}
void Npc::doSay(const std::string& text)
{
if (lastTalkCreature == focusCreature) {
conversationEndTime = OTSYS_TIME() + 60000;
}
g_game.internalCreatureSay(this, TALKTYPE_SAY, text, false);
}
bool Npc::getNextStep(Direction& dir, uint32_t& flags)
{
if (Creature::getNextStep(dir, flags)) {
return true;
}
if (baseSpeed <= 0) {
return false;
}
if (focusCreature != 0) {
return false;
}
if (OTSYS_TIME() < staticMovementTime) {
return false;
}
if (getTimeSinceLastMove() < 100 + getStepDuration() + getStepSpeed()) {
return false;
}
return getRandomStep(dir);
}
void Npc::setIdle(bool idle)
{
if (isRemoved() || getHealth() <= 0) {
return;
}
isIdle = idle;
if (isIdle) {
onIdleStatus();
}
}
void Npc::updateIdleStatus()
{
bool status = spectators.empty();
if (status != isIdle) {
setIdle(status);
}
}
bool Npc::canWalkTo(const Position& fromPos, Direction dir) const
{
if (masterRadius == 0) {
return false;
}
Position toPos = getNextPosition(dir, fromPos);
if (!Spawns::isInZone(masterPos, masterRadius, toPos)) {
return false;
}
Tile* tile = g_game.map.getTile(toPos);
if (!tile || tile->queryAdd(0, *this, 1, 0) != RETURNVALUE_NOERROR) {
return false;
}
if (tile->hasFlag(TILESTATE_BLOCKPATH)) {
return false;
}
if (tile->hasHeight(1)) {
return false;
}
return true;
}
bool Npc::getRandomStep(Direction& dir) const
{
std::vector<Direction> dirList;
const Position& creaturePos = getPosition();
if (canWalkTo(creaturePos, DIRECTION_NORTH)) {
dirList.push_back(DIRECTION_NORTH);
}
if (canWalkTo(creaturePos, DIRECTION_SOUTH)) {
dirList.push_back(DIRECTION_SOUTH);
}
if (canWalkTo(creaturePos, DIRECTION_EAST)) {
dirList.push_back(DIRECTION_EAST);
}
if (canWalkTo(creaturePos, DIRECTION_WEST)) {
dirList.push_back(DIRECTION_WEST);
}
if (dirList.empty()) {
return false;
}
dir = dirList[uniform_random(0, dirList.size() - 1)];
return true;
}
void Npc::doMoveTo(const Position& target)
{
std::forward_list<Direction> listDir;
if (getPathTo(target, listDir, 1, 1, true, true)) {
startAutoWalk(listDir);
}
}
void Npc::turnToCreature(Creature* creature)
{
const Position& creaturePos = creature->getPosition();
const Position& myPos = getPosition();
const auto dx = Position::getOffsetX(myPos, creaturePos);
const auto dy = Position::getOffsetY(myPos, creaturePos);
float tan;
if (dx != 0) {
tan = static_cast<float>(dy) / dx;
} else {
tan = 10;
}
Direction dir;
if (std::abs(tan) < 1) {
if (dx > 0) {
dir = DIRECTION_WEST;
} else {
dir = DIRECTION_EAST;
}
} else {
if (dy > 0) {
dir = DIRECTION_NORTH;
} else {
dir = DIRECTION_SOUTH;
}
}
g_game.internalCreatureTurn(this, dir);
}
void Npc::setCreatureFocus(Creature* creature)
{
if (creature) {
focusCreature = creature->getID();
turnToCreature(creature);
} else {
focusCreature = 0;
}
}