1590 lines
40 KiB
C++
1590 lines
40 KiB
C++
#include "cr.hh"
|
|
#include "config.hh"
|
|
#include "info.hh"
|
|
#include "operate.hh"
|
|
|
|
#include "stubs.hh"
|
|
|
|
// TShortway
|
|
// =============================================================================
|
|
struct TShortwayPoint {
|
|
TShortwayPoint(void){
|
|
this->x = 0;
|
|
this->y = 0;
|
|
this->Waypoints = -1;
|
|
this->Waylength = -1;
|
|
this->Heuristic = -1;
|
|
this->Predecessor = NULL;
|
|
this->NextToExpand = NULL;
|
|
}
|
|
|
|
int x;
|
|
int y;
|
|
int Waypoints;
|
|
int Waylength;
|
|
int Heuristic;
|
|
TShortwayPoint *Predecessor;
|
|
TShortwayPoint *NextToExpand;
|
|
};
|
|
|
|
struct TShortway{
|
|
TShortway(TCreature *Creature, int VisibleX, int VisibleY);
|
|
~TShortway(void);
|
|
void FillMap(void);
|
|
void ClearMap(void);
|
|
void Expand(TShortwayPoint *Node);
|
|
bool Calculate(int DestX, int DestY, bool MustReach, int MaxSteps);
|
|
|
|
// DATA
|
|
// =================
|
|
matrix<TShortwayPoint> *Map;
|
|
TShortwayPoint *FirstToExpand;
|
|
TCreature *Creature;
|
|
int VisibleX;
|
|
int VisibleY;
|
|
int StartX;
|
|
int StartY;
|
|
int StartZ;
|
|
int MinWaypoints;
|
|
};
|
|
|
|
TShortway::TShortway(TCreature *Creature, int VisibleX, int VisibleY){
|
|
if(Creature == NULL){
|
|
error("TShortway::TShortway: Übergebene Kreatur ist NULL.\n");
|
|
return;
|
|
}
|
|
|
|
if(VisibleX < 1 || VisibleX > 100 || VisibleY < 1 || VisibleY > 100){
|
|
error("TShortway::TShortway: Ungültiger Sichtbarkeitsbereich %d*%d.\n", VisibleX, VisibleY);
|
|
return;
|
|
}
|
|
|
|
this->Creature = Creature;
|
|
this->VisibleX = VisibleX;
|
|
this->VisibleY = VisibleY;
|
|
this->StartX = Creature->posx;
|
|
this->StartY = Creature->posy;
|
|
this->StartZ = Creature->posz;
|
|
this->Map = new matrix<TShortwayPoint>(
|
|
-(VisibleX + 1), +(VisibleX + 1),
|
|
-(VisibleY + 1), +(VisibleY + 1));
|
|
this->FillMap();
|
|
}
|
|
|
|
TShortway::~TShortway(void){
|
|
if(this->Map != NULL){
|
|
delete this->Map;
|
|
}
|
|
}
|
|
|
|
void TShortway::FillMap(void){
|
|
this->MinWaypoints = 1000;
|
|
|
|
for(int X = -this->VisibleX; X <= this->VisibleX; X += 1)
|
|
for(int Y = -this->VisibleY; Y <= this->VisibleY; Y += 1){
|
|
int FieldX = this->StartX + X;
|
|
int FieldY = this->StartY + Y;
|
|
int FieldZ = this->StartZ;
|
|
|
|
int Waypoints = -1;
|
|
Object Obj = GetFirstObject(FieldX, FieldY, FieldZ);
|
|
if(Obj.exists()){
|
|
ObjectType ObjType = Obj.getObjectType();
|
|
if(ObjType.getFlag(BANK) && !ObjType.getFlag(UNPASS)){
|
|
Waypoints = (int)ObjType.getAttribute(WAYPOINTS);
|
|
if(Waypoints == 0){
|
|
error("TShortway::FillMap: Ungültiger Wegpunkte-Wert %d für Bank %d.\n",
|
|
Waypoints, ObjType.TypeID);
|
|
Waypoints = -1;
|
|
}
|
|
|
|
if(!this->Creature->MovePossible(FieldX, FieldY, FieldZ, false, false)){
|
|
Waypoints = -1;
|
|
}
|
|
|
|
if(Waypoints > 0 && Waypoints < this->MinWaypoints){
|
|
this->MinWaypoints = Waypoints;
|
|
}
|
|
}
|
|
}
|
|
|
|
TShortwayPoint *Node = this->Map->at(X, Y);
|
|
Node->x = X;
|
|
Node->y = Y;
|
|
Node->Waypoints = Waypoints;
|
|
}
|
|
}
|
|
|
|
void TShortway::ClearMap(void){
|
|
for(int X = -this->VisibleX; X <= this->VisibleX; X += 1)
|
|
for(int Y = -this->VisibleY; Y <= this->VisibleY; Y += 1){
|
|
TShortwayPoint *Node = this->Map->at(X, Y);
|
|
Node->Waylength = INT_MAX;
|
|
Node->Heuristic = INT_MAX;
|
|
Node->Predecessor = NULL;
|
|
Node->NextToExpand = NULL;
|
|
}
|
|
}
|
|
|
|
void TShortway::Expand(TShortwayPoint *Node){
|
|
if(Node == NULL){
|
|
error("TShortway::Expand: Übergebener Knoten ist NULL.\n");
|
|
return;
|
|
}
|
|
|
|
this->FirstToExpand = Node->NextToExpand;
|
|
|
|
int MinNeighborWaylength = Node->Waylength + Node->Waypoints;
|
|
if(MinNeighborWaylength >= this->Map->at(0, 0)->Waylength){
|
|
return;
|
|
}
|
|
|
|
for(int OffsetX = -1; OffsetX <= 1; OffsetX += 1)
|
|
for(int OffsetY = -1; OffsetY <= 1; OffsetY += 1){
|
|
if(OffsetX == 0 && OffsetY == 0){
|
|
continue;
|
|
}
|
|
|
|
TShortwayPoint *Neighbor = this->Map->at(Node->x + OffsetX, Node->y + OffsetY);
|
|
|
|
// NOTE(fusion): The minimum neighbor waylength already contains the cost
|
|
// of a single step. Diagonal steps are three times more expensive so we
|
|
// add waypoints two more times.
|
|
int NeighborWaylength = MinNeighborWaylength;
|
|
if(OffsetX != 0 && OffsetY != 0){
|
|
NeighborWaylength += Node->Waylength * 2;
|
|
}
|
|
|
|
if(NeighborWaylength < Neighbor->Waylength){
|
|
Neighbor->Waylength = NeighborWaylength;
|
|
Neighbor->Predecessor = Node;
|
|
if((Neighbor->x != 0 || Neighbor->y != 0) && Neighbor->Waypoints != -1){
|
|
// NOTE(fusion): Remove from expand list if it was already expanded upon.
|
|
if(Neighbor->Heuristic != INT_MAX){
|
|
TShortwayPoint *Prev = NULL;
|
|
TShortwayPoint *Cur = this->FirstToExpand;
|
|
while(Cur != NULL && Cur != Neighbor){
|
|
Prev = Cur;
|
|
Cur = Cur->NextToExpand;
|
|
}
|
|
|
|
if(Cur != Neighbor){
|
|
error("TShortway::Expand: Knoten steht nicht in der ExpandList.\n");
|
|
}else if(Prev == NULL){
|
|
this->FirstToExpand = Neighbor->NextToExpand;
|
|
}else{
|
|
Prev->NextToExpand = Neighbor->NextToExpand;
|
|
}
|
|
}
|
|
|
|
// NOTE(fusion): Compute heuristic using the manhattan distance.
|
|
int Distance = std::abs(Neighbor->x) + std::abs(Neighbor->y);
|
|
Neighbor->Heuristic = Neighbor->Waylength
|
|
+ Neighbor->Waypoints * 1
|
|
+ this->MinWaypoints * (Distance - 1);
|
|
|
|
// NOTE(fusion): Insert into expand list.
|
|
{
|
|
TShortwayPoint *Prev = NULL;
|
|
TShortwayPoint *Cur = this->FirstToExpand;
|
|
while(Cur != NULL && Cur->Heuristic < Neighbor->Heuristic){
|
|
Prev = Cur;
|
|
Cur = Cur->NextToExpand;
|
|
}
|
|
|
|
if(Prev == NULL){
|
|
this->FirstToExpand = Neighbor;
|
|
}else{
|
|
Prev->NextToExpand = Neighbor;
|
|
}
|
|
Neighbor->NextToExpand = Cur;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool TShortway::Calculate(int DestX, int DestY, bool MustReach, int MaxSteps){
|
|
if(this->Map == NULL){
|
|
error("TShortway::Calculate: Karte existiert nicht.\n");
|
|
return false;
|
|
}
|
|
|
|
// NOTE(fusion): Transform dest to relative coordinates.
|
|
DestX -= this->StartX;
|
|
DestY -= this->StartY;
|
|
|
|
// NOTE(fusion): Check if already at destination.
|
|
if(DestX == 0 && DestY == 0){
|
|
return true;
|
|
}
|
|
|
|
// NOTE(fusion): Check if out of range.
|
|
if(std::abs(DestX) > this->VisibleX || std::abs(DestY) > this->VisibleY){
|
|
return false;
|
|
}
|
|
|
|
// NOTE(fusion): Find shortest path from the destination to the origin.
|
|
this->ClearMap();
|
|
this->FirstToExpand = this->Map->at(DestX, DestY);
|
|
this->FirstToExpand->Waylength = 0;
|
|
while(this->FirstToExpand != NULL){
|
|
this->Expand(this->FirstToExpand);
|
|
}
|
|
|
|
// NOTE(fusion): Check if the origin was reached from the destination.
|
|
TShortwayPoint *Node = this->Map->at(0, 0);
|
|
if(Node->Waylength == INT_MAX){
|
|
return false;
|
|
}
|
|
|
|
// NOTE(fusion): Walk back from the origin to reconstruct the path.
|
|
Node = Node->Predecessor;
|
|
while(Node != NULL && MaxSteps > 0){
|
|
int MaxDistance = std::max<int>(
|
|
std::abs(Node->x - DestX),
|
|
std::abs(Node->y - DestY));
|
|
if(!MustReach && MaxDistance <= 1){
|
|
break;
|
|
}
|
|
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDGo;
|
|
TD.Go.x = this->StartX + Node->x;
|
|
TD.Go.y = this->StartY + Node->y;
|
|
TD.Go.z = this->StartZ;
|
|
Creature->ToDoAdd(TD);
|
|
|
|
Node = Node->Predecessor;
|
|
MaxSteps -= 1;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// TCreature
|
|
// =============================================================================
|
|
bool TCreature::SetOnMap(void){
|
|
int LoginX = this->posx;
|
|
int LoginY = this->posy;
|
|
int LoginZ = this->posz;
|
|
bool Player = (this->Type == PLAYER);
|
|
if(!SearchLoginField(&LoginX, &LoginY, &LoginZ, 1, Player)){
|
|
bool Found = false;
|
|
if(IsHouse(LoginX, LoginY, LoginZ)){
|
|
uint16 HouseID = GetHouseID(LoginX, LoginY, LoginZ);
|
|
GetExitPosition(HouseID, &LoginX, &LoginY, &LoginZ);
|
|
Found = SearchLoginField(&LoginX, &LoginY, &LoginZ, 1, Player);
|
|
}
|
|
|
|
if(!Found){
|
|
LoginX = this->startx;
|
|
LoginY = this->starty;
|
|
LoginZ = this->startz;
|
|
}
|
|
}
|
|
|
|
Object Con = GetMapContainer(LoginX, LoginY, LoginZ);
|
|
if(Con == NONE){
|
|
error("TCreature::SetOnMap: Kartencontainer für Punkt [%d,%d,%d] existiert nicht.\n",
|
|
LoginX, LoginY, LoginZ);
|
|
return false;
|
|
}
|
|
|
|
this->posx = LoginX;
|
|
this->posy = LoginY;
|
|
this->posz = LoginZ;
|
|
|
|
// NOTE(fusion): `Create` automatically sets `this->CrObject` and creates
|
|
// its body container slots.
|
|
bool Result = true;
|
|
try{
|
|
Create(Con, TYPEID_CREATURE_CONTAINER, this->ID);
|
|
}catch(RESULT r){
|
|
error("TCreature::SetOnMap: Kann Kreatur nicht setzen ([%d,%d,%d] - Exception %d).\n",
|
|
LoginX, LoginY, LoginZ, r);
|
|
if(this->Type == PLAYER){
|
|
SendResult(this->Connection, r);
|
|
}
|
|
Result = false;
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
bool TCreature::DelOnMap(void){
|
|
Object Obj = this->CrObject;
|
|
if(Obj == NONE){
|
|
return true;
|
|
}
|
|
|
|
// TODO(fusion): I feel `Delete` should also manage `this->CrObject` automatically.
|
|
bool Result = true;
|
|
this->CrObject = NONE;
|
|
try{
|
|
Delete(Obj, -1);
|
|
}catch(RESULT r){
|
|
error("TCreature::DelOnMap: Error Deleting CreatureObject: %d\n", r);
|
|
Result = false;
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
void TCreature::Go(int DestX, int DestY, int DestZ){
|
|
// NOTE(fusion): This is the execution function for `ToDoGo` which computes
|
|
// the path step by step. If the destination is outside the range of a single
|
|
// step, then it is an error.
|
|
int OrigX = this->posx;
|
|
int OrigY = this->posy;
|
|
int OrigZ = this->posz;
|
|
int Distance = std::max<int>(std::abs(OrigX - DestX), std::abs(OrigY - DestY));
|
|
if(Distance > 1 || OrigZ != DestZ){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
// TODO(fusion): See note in `TPlayer::CheckState`.
|
|
int DrunkLevel = this->Skills[SKILL_DRUNKEN]->TimerValue();
|
|
if(DrunkLevel > 0 && this->Skills[SKILL_DRUNKEN]->Get() == 0){
|
|
int StaggerChance = std::max<int>(7 - DrunkLevel, 1);
|
|
if(rand() % StaggerChance == 0){
|
|
DestX = OrigX;
|
|
DestY = OrigY;
|
|
switch(rand() % 4){
|
|
case DIRECTION_NORTH: DestY -= 1; break;
|
|
case DIRECTION_EAST: DestX += 1; break;
|
|
case DIRECTION_SOUTH: DestY += 1; break;
|
|
case DIRECTION_WEST: DestX -= 1; break;
|
|
}
|
|
|
|
if(this->ToDoClear() && this->Type == PLAYER){
|
|
SendSnapback(this->Connection);
|
|
}
|
|
|
|
int TalkMode = (this->Type == MONSTER) ? TALK_ANIMAL_LOW : TALK_SAY;
|
|
this->ToDoTalk(TalkMode, NULL, "Hicks!", false);
|
|
this->ToDoStart();
|
|
}
|
|
}
|
|
|
|
if(!this->MovePossible(DestX, DestY, DestZ, true, false)){
|
|
bool DiagonalMove = (OrigX != DestX && OrigY != DestY);
|
|
if(this->Type == PLAYER && !DiagonalMove){
|
|
// TODO(fusion): These are quite similar to `MagicClimbing`. Perhaps
|
|
// there is an inlined function here that checks whether climbing is
|
|
// possible.
|
|
if(DestZ > 0 && GetHeight(OrigX, OrigY, OrigZ) >= 24
|
|
&& !CoordinateFlag(OrigX, OrigY, OrigZ - 1, BANK)
|
|
&& !CoordinateFlag(OrigX, OrigY, OrigZ - 1, UNPASS)
|
|
&& this->MovePossible(DestX, DestY, DestZ - 1, true, true)){
|
|
DestZ -= 1;
|
|
}else if(DestZ < 15 && GetHeight(DestX, DestY, DestZ + 1) >= 24
|
|
&& !CoordinateFlag(DestX, DestY, DestZ, BANK)
|
|
&& !CoordinateFlag(DestX, DestY, DestZ, UNPASS)
|
|
&& this->MovePossible(DestX, DestY, DestZ + 1, true, true)){
|
|
DestZ += 1;
|
|
}
|
|
}
|
|
|
|
if(this->posz == DestZ){
|
|
throw MOVENOTPOSSIBLE;
|
|
}
|
|
}
|
|
|
|
Object Dest = GetMapContainer(DestX, DestY, DestZ);
|
|
::Move(this->ID, this->CrObject, Dest, -1, false, NONE);
|
|
}
|
|
|
|
void TCreature::Rotate(int Direction){
|
|
this->Direction = Direction;
|
|
AnnounceChangedObject(this->CrObject, OBJECT_CHANGED);
|
|
}
|
|
|
|
void TCreature::Rotate(TCreature *Target){
|
|
if(Target == NULL){
|
|
error("TCreature::Rotate: Target ist NULL.\n");
|
|
return;
|
|
}
|
|
|
|
int Direction = this->Direction;
|
|
int OffsetX = Target->posx - this->posx;
|
|
int OffsetY = Target->posy - this->posy;
|
|
int DistanceX = std::abs(OffsetX);
|
|
int DistanceY = std::abs(OffsetY);
|
|
if(DistanceY > DistanceX){
|
|
Direction = (OffsetY < 0) ? DIRECTION_NORTH : DIRECTION_SOUTH;
|
|
}else{
|
|
Direction = (OffsetX < 0) ? DIRECTION_WEST : DIRECTION_EAST;
|
|
}
|
|
|
|
this->Rotate(Direction);
|
|
}
|
|
|
|
void TCreature::Move(Object Obj, int DestX, int DestY, int DestZ, uint8 Count){
|
|
// NOTE(fusion): What a disaster.
|
|
|
|
if(!Obj.exists()){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
if(Obj == this->CrObject){
|
|
this->Go(DestX, DestY, DestZ);
|
|
return;
|
|
}
|
|
|
|
ObjectType ObjType = Obj.getObjectType();
|
|
if(ObjType.isCreatureContainer()){
|
|
this->Combat.DelayAttack(2000);
|
|
}
|
|
|
|
int MoveCount = (int)Count;
|
|
if(ObjType.getFlag(CUMULATIVE)){
|
|
int ObjAmount = (int)Obj.getAttribute(AMOUNT);
|
|
if(MoveCount > ObjAmount){
|
|
MoveCount = ObjAmount;
|
|
}
|
|
}
|
|
|
|
if(DestX == 0xFFFF){ // SPECIAL_COORDINATE ?
|
|
// NOTE(fusion): Any inventory slot.
|
|
if(DestY == 0){
|
|
// NOTE(fusion): `CheckInventoryDestination` will throw if it's not
|
|
// possible to place the object on the chosen container. We want to
|
|
// find a slot that doesn't make it throw while giving priority to
|
|
// non hand or ammo slots.
|
|
for(int Position = INVENTORY_FIRST;
|
|
Position <= INVENTORY_LAST;
|
|
Position += 1){
|
|
try{
|
|
Object Con = GetBodyContainer(this->ID, Position);
|
|
CheckInventoryDestination(Obj, Con, false);
|
|
}catch(RESULT r){
|
|
continue;
|
|
}
|
|
|
|
DestY = Position;
|
|
if(Position != INVENTORY_RIGHTHAND
|
|
&& Position != INVENTORY_LEFTHAND
|
|
&& Position != INVENTORY_AMMO){
|
|
break;
|
|
}
|
|
}
|
|
|
|
// NOTE(fusion): No appropriate inventory slot was found so we now
|
|
// fallback to inventory containers. For whatever reason we don't
|
|
// give priority to the bag slot.
|
|
if(DestY == 0){
|
|
for(int Position = INVENTORY_FIRST;
|
|
Position <= INVENTORY_LAST;
|
|
Position += 1){
|
|
Object Con = GetBodyObject(this->ID, Position);
|
|
if(Con != NONE && Con.getObjectType().getFlag(CONTAINER)){
|
|
try{
|
|
CheckContainerDestination(Obj, Con);
|
|
}catch(RESULT r){
|
|
continue;
|
|
}
|
|
|
|
DestY = Position;
|
|
}
|
|
}
|
|
}
|
|
|
|
if(DestY == 0){
|
|
throw NOROOM;
|
|
}
|
|
}
|
|
|
|
Object DestCon = NONE;
|
|
Object DestObj = NONE;
|
|
if(DestY >= INVENTORY_FIRST && DestY <= INVENTORY_LAST){
|
|
DestCon = GetBodyContainer(this->ID, DestY);
|
|
DestObj = GetBodyObject(this->ID, DestY);
|
|
}else if(DestY >= CONTAINER_FIRST && DestY <= CONTAINER_LAST){
|
|
DestCon = GetBodyContainer(this->ID, DestY);
|
|
if(DestZ < 254){
|
|
// TODO(fusion): The last argument to `GetObject` is the object
|
|
// type we expect to find and it seems that the type id of a map
|
|
// container (which is 0) can be used as a wildcard for any object
|
|
// it finds.
|
|
DestObj = GetObject(this->ID, DestX, DestY, DestZ, DestZ, 0);
|
|
}else if(DestZ == 254){
|
|
if(DestCon == NONE){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
DestCon = DestCon.getContainer();
|
|
}
|
|
}else{
|
|
error("TCreature::Move: Ungültiger Containercode %d.\n", DestY);
|
|
throw ERROR;
|
|
}
|
|
|
|
if(DestObj != NONE){
|
|
ObjectType DestObjType = DestObj.getObjectType();
|
|
if(DestObjType.getFlag(CONTAINER)){
|
|
DestCon = DestObj;
|
|
DestObj = NONE;
|
|
}else if(DestObjType.getFlag(CUMULATIVE) && DestObjType == ObjType){
|
|
int DestAmount = (int)DestObj.getAttribute(AMOUNT);
|
|
int MergeCount = MoveCount;
|
|
if((DestAmount + MergeCount) > 100){
|
|
MergeCount = 100 - DestAmount;
|
|
}
|
|
|
|
if(MergeCount > 0){
|
|
try{
|
|
::Merge(this->ID, Obj, DestObj, MergeCount, NONE);
|
|
MoveCount -= MergeCount;
|
|
if(MoveCount <= 0){
|
|
return;
|
|
}
|
|
}catch(RESULT r){
|
|
if(r == TOOHEAVY){
|
|
throw;
|
|
}
|
|
}
|
|
}
|
|
|
|
DestObj = NONE;
|
|
}
|
|
}
|
|
|
|
if(DestCon == NONE){
|
|
throw OUTOFRANGE;
|
|
}
|
|
|
|
try{
|
|
// TODO(fusion): Find out what is this last parameter to `Move`.
|
|
::Move(this->ID, Obj, DestCon, MoveCount, false, DestObj);
|
|
}catch(RESULT r){
|
|
// NOTE(fusion): Attempt to exchange inventory items.
|
|
if(DestY >= INVENTORY_FIRST && DestY <= INVENTORY_LAST
|
|
&& DestObj != NONE
|
|
&& (r == NOROOM
|
|
|| r == HANDSNOTFREE
|
|
|| r == HANDBLOCKED
|
|
|| r == ONEWEAPONONLY)){
|
|
Object ObjCon = Obj.getContainer();
|
|
::Move(this->ID, DestObj, ObjCon, -1, false, NONE);
|
|
::Move(this->ID, Obj, DestCon, MoveCount, false, NONE);
|
|
}else{
|
|
throw;
|
|
}
|
|
}
|
|
}else{
|
|
Object Dest = GetMapContainer(DestX, DestY, DestZ);
|
|
if(!Dest.exists()){
|
|
throw ERROR;
|
|
}
|
|
|
|
if(ObjType.getFlag(HANG)
|
|
&& (CoordinateFlag(DestX, DestY, DestZ, HOOKSOUTH)
|
|
|| CoordinateFlag(DestY, DestY, DestZ, HOOKEAST))
|
|
&& !ObjectInRange(this->ID, Dest, 1)){
|
|
if(this->ToDoClear() && this->Type == PLAYER){
|
|
SendSnapback(this->Connection);
|
|
}
|
|
|
|
// NOTE(fusion): Pick up object if it's not in our inventory.
|
|
if(GetObjectCreatureID(Obj) != this->ID){
|
|
this->ToDoMove(Obj, 0xFFFF, 0, 0, 1);
|
|
}
|
|
|
|
// NOTE(fusion): Walk to destination and try again.
|
|
this->ToDoGo(DestX, DestY, DestZ, false, INT_MAX);
|
|
this->ToDoMove(Obj, DestX, DestY, DestZ, 1);
|
|
this->ToDoStart();
|
|
}else{
|
|
::Move(this->ID, Obj, Dest, MoveCount, false, NONE);
|
|
}
|
|
}
|
|
}
|
|
|
|
void TCreature::Trade(Object Obj, uint32 PartnerID){
|
|
if(this->Type != PLAYER){
|
|
error("TCreature::Trade: Nur Spieler können handeln.\n");
|
|
throw ERROR;
|
|
}
|
|
|
|
if(!Obj.exists() || !ObjectAccessible(this->ID, Obj, 1)){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
if(this->ID == PartnerID){
|
|
throw CROSSREFERENCE;
|
|
}
|
|
|
|
TPlayer *Partner = GetPlayer(PartnerID);
|
|
if(Partner == NULL || Partner->Type != PLAYER){
|
|
throw PLAYERNOTONLINE;
|
|
}
|
|
|
|
if(((TPlayer*)this)->TradeObject != NONE){
|
|
throw ALREADYTRADING;
|
|
}
|
|
|
|
if(CountObjects(Obj) > 100){
|
|
throw TOOMANYOBJECTS;
|
|
}
|
|
|
|
if(ObjectDistance(this->CrObject, Partner->CrObject) > 2){
|
|
throw OUTOFRANGE;
|
|
}
|
|
|
|
if(!ThrowPossible(this->posx, this->posy, this->posz,
|
|
Partner->posx, Partner->posy, Partner->posz, 0)){
|
|
throw CANNOTTHROW;
|
|
}
|
|
|
|
if(Partner->TradeObject != NONE && Partner->TradePartner != this->ID){
|
|
throw PARTNERTRADING;
|
|
}
|
|
|
|
// NOTE(fusion): Check if one object is contained by the other.
|
|
if(Partner->TradeObject != NONE){
|
|
Object Help = Partner->TradeObject;
|
|
while(Help != NONE && !Help.getObjectType().isMapContainer()){
|
|
if(Help == Obj){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
Help = Help.getContainer();
|
|
}
|
|
|
|
Help = Obj;
|
|
while(Help != NONE && !Help.getObjectType().isMapContainer()){
|
|
if(Help == Partner->TradeObject){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
Help = Help.getContainer();
|
|
}
|
|
}
|
|
|
|
((TPlayer*)this)->TradeObject = Obj;
|
|
((TPlayer*)this)->TradePartner = PartnerID;
|
|
((TPlayer*)this)->TradeAccepted = false;
|
|
|
|
if(Partner->TradeObject != NONE){
|
|
SendTradeOffer(Partner->Connection, this->Name, false, Obj);
|
|
SendTradeOffer(this->Connection, this->Name, true, Obj);
|
|
SendTradeOffer(this->Connection, Partner->Name, false, Partner->TradeObject);
|
|
}else{
|
|
SendMessage(Partner->Connection, TALK_INFO_MESSAGE,
|
|
"%s wants to trade with you.", this->Name);
|
|
SendTradeOffer(this->Connection, this->Name, true, Obj);
|
|
}
|
|
}
|
|
|
|
void TCreature::Use(Object Obj1, Object Obj2, uint8 Dummy){
|
|
if(!Obj1.exists()){
|
|
throw DESTROYED;
|
|
}
|
|
|
|
if(Obj2 != NONE){
|
|
if(!Obj2.exists()){
|
|
throw DESTROYED;
|
|
}
|
|
|
|
bool DistUse = Obj1.getObjectType().getFlag(DISTUSE);
|
|
if(!DistUse && !ObjectInRange(this->ID, Obj2, 1)){
|
|
int ObjX2, ObjY2, ObjZ2;
|
|
GetObjectCoordinates(Obj2, &ObjX2, &ObjY2, &ObjZ2);
|
|
|
|
if(this->ToDoClear() && this->Type == PLAYER){
|
|
SendSnapback(this->Connection);
|
|
}
|
|
|
|
// NOTE(fusion): Pick up object 1 if it's not in our inventory.
|
|
if(GetObjectCreatureID(Obj1) != this->ID){
|
|
this->ToDoMove(Obj1, 0xFFFF, 0, 0, 1);
|
|
}
|
|
|
|
// NOTE(fusion): Walk to object 2 and try again.
|
|
this->ToDoGo(ObjX2, ObjY2, ObjZ2, false, INT_MAX);
|
|
// TODO(fusion): Missing `Dummy` here? Probably not because this
|
|
// could only be triggered if object 2 exists and Dummy is only
|
|
// used when opening containers.
|
|
this->ToDoUse(2, Obj1, Obj2);
|
|
this->ToDoStart();
|
|
return;
|
|
}
|
|
|
|
if(DistUse && !ObjectInRange(this->ID, Obj2, 7)){
|
|
throw OUTOFRANGE;
|
|
}
|
|
|
|
this->EarliestMultiuseTime = ServerMilliseconds + 1000;
|
|
}
|
|
|
|
::Use(this->ID, Obj1, Obj2, Dummy);
|
|
}
|
|
|
|
void TCreature::Turn(Object Obj){
|
|
if(!Obj.exists()){
|
|
throw DESTROYED;
|
|
}
|
|
|
|
::Turn(this->ID, Obj);
|
|
}
|
|
|
|
void TCreature::Attack(void){
|
|
this->Combat.Attack();
|
|
}
|
|
|
|
void TCreature::Execute(void){
|
|
while(true){
|
|
if(!this->LockToDo || this->IsDead || this->NextWakeup > ServerMilliseconds){
|
|
break;
|
|
}
|
|
|
|
if(this->NrToDo <= this->ActToDo){
|
|
this->ToDoClear();
|
|
this->IdleStimulus();
|
|
break;
|
|
}
|
|
|
|
uint32 Delay = this->CalculateDelay();
|
|
if(Delay > 0){
|
|
if(this->Stop){
|
|
this->ToDoClear();
|
|
if(this->Type == PLAYER){
|
|
SendSnapback(this->Connection);
|
|
}
|
|
}else{
|
|
this->NextWakeup = ServerMilliseconds + Delay;
|
|
ToDoQueue.insert(this->NextWakeup, this->ID);
|
|
}
|
|
break;
|
|
}
|
|
|
|
TToDoEntry TD = *this->ToDoList.at(this->ActToDo);
|
|
this->ActToDo += 1;
|
|
try{
|
|
switch(TD.Code){
|
|
case TDGo:{
|
|
this->Go(TD.Go.x, TD.Go.y, TD.Go.z);
|
|
break;
|
|
}
|
|
|
|
case TDRotate:{
|
|
this->Rotate(TD.Rotate.Direction);
|
|
break;
|
|
}
|
|
|
|
case TDMove:{
|
|
this->Move(TD.Move.Obj, TD.Move.x, TD.Move.y, TD.Move.z, (uint8)TD.Move.Count);
|
|
break;
|
|
}
|
|
|
|
case TDTrade:{
|
|
this->Trade(TD.Trade.Obj, TD.Trade.Partner);
|
|
break;
|
|
}
|
|
|
|
case TDUse:{
|
|
this->Use(TD.Use.Obj1, TD.Use.Obj2, TD.Use.Dummy);
|
|
break;
|
|
}
|
|
|
|
case TDTurn:{
|
|
this->Turn(TD.Turn.Obj);
|
|
break;
|
|
}
|
|
|
|
case TDAttack:{
|
|
this->Attack();
|
|
break;
|
|
}
|
|
|
|
case TDTalk:{
|
|
const char *Text = GetDynamicString(TD.Talk.Text);
|
|
if(Text != NULL){
|
|
const char *Addressee = GetDynamicString(TD.Talk.Addressee);
|
|
Talk(this->ID, TD.Talk.Mode, Addressee, Text, TD.Talk.CheckSpamming);
|
|
}else{
|
|
error("TCreature::Execute: Text ist NULL bei %s.\n", this->Name);
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TDChangeState:{
|
|
if(this->Type == NPC){
|
|
ChangeNPCState(this, TD.ChangeState.NewState, true);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:{
|
|
break;
|
|
}
|
|
}
|
|
}catch(RESULT r){
|
|
bool SnapbackNecessary = (this->ToDoClear() || this->Stop);
|
|
if(r == EXHAUSTED){
|
|
this->ToDoWait(1000);
|
|
this->ToDoStart();
|
|
}else{
|
|
this->ToDoYield();
|
|
}
|
|
|
|
if(this->Type == PLAYER){
|
|
SendResult(this->Connection, r);
|
|
if(SnapbackNecessary
|
|
&& r != MOVENOTPOSSIBLE
|
|
&& r != NOTINVITED
|
|
&& r != ENTERPROTECTIONZONE){
|
|
SendSnapback(this->Connection);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
if(this->Stop){
|
|
this->ToDoClear();
|
|
if(this->Type == PLAYER){
|
|
SendSnapback(this->Connection);
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
uint32 TCreature::CalculateDelay(void){
|
|
uint32 Delay = 0;
|
|
TToDoEntry *TD = this->ToDoList.at(this->ActToDo);
|
|
switch(TD->Code){
|
|
case TDWait:{
|
|
// TODO(fusion): I'm not sure about having `EarliestWalkTime` here.
|
|
uint32 WaitTime = TD->Wait.Time;
|
|
if(WaitTime < this->EarliestWalkTime){
|
|
WaitTime = this->EarliestWalkTime;
|
|
}
|
|
|
|
if(WaitTime > ServerMilliseconds){
|
|
Delay = WaitTime - ServerMilliseconds;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TDGo:{
|
|
if(this->EarliestWalkTime > ServerMilliseconds){
|
|
Delay = this->EarliestWalkTime - ServerMilliseconds;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TDUse:{
|
|
if(TD->Use.Obj2 != 0){
|
|
if(this->EarliestMultiuseTime > ServerMilliseconds){
|
|
Delay = this->EarliestMultiuseTime - ServerMilliseconds;
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TDAttack:{
|
|
uint32 EarliestAttackTime = this->Combat.EarliestAttackTime;
|
|
if(EarliestAttackTime < this->EarliestSpellTime){
|
|
EarliestAttackTime = this->EarliestSpellTime;
|
|
}
|
|
|
|
if(EarliestAttackTime > ServerMilliseconds){
|
|
Delay = EarliestAttackTime - ServerMilliseconds;
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:{
|
|
break;
|
|
}
|
|
}
|
|
return Delay;
|
|
}
|
|
|
|
bool TCreature::ToDoClear(void){
|
|
bool SnapbackNecessary = false;
|
|
for(int i = 0; i < this->NrToDo; i += 1){
|
|
TToDoEntry *TD = this->ToDoList.at(i);
|
|
switch(TD->Code){
|
|
case TDGo:{
|
|
if(this->ActToDo <= i){
|
|
SnapbackNecessary = true;
|
|
}
|
|
break;
|
|
}
|
|
|
|
case TDTalk:{
|
|
DeleteDynamicString(TD->Talk.Text);
|
|
DeleteDynamicString(TD->Talk.Addressee);
|
|
break;
|
|
}
|
|
|
|
case TDChangeState:{
|
|
if(this->ActToDo <= i && this->Type == NPC){
|
|
ChangeNPCState(this, TD->ChangeState.NewState, false);
|
|
}
|
|
break;
|
|
}
|
|
|
|
default:{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
this->LockToDo = false;
|
|
this->ActToDo = 0;
|
|
this->NrToDo = 0;
|
|
this->Stop = false;
|
|
return SnapbackNecessary;
|
|
}
|
|
|
|
void TCreature::ToDoAdd(TToDoEntry TD){
|
|
if(this->LockToDo){
|
|
if(this->ToDoClear() && this->Type == PLAYER){
|
|
SendSnapback(this->Connection);
|
|
}
|
|
}
|
|
|
|
*this->ToDoList.at(this->NrToDo) = TD;
|
|
this->NrToDo += 1;
|
|
}
|
|
|
|
void TCreature::ToDoStop(void){
|
|
if(this->LockToDo){
|
|
this->Stop = true;
|
|
}else if(this->Type == PLAYER){
|
|
SendSnapback(this->Connection);
|
|
}
|
|
}
|
|
|
|
void TCreature::ToDoStart(void){
|
|
if(this->NrToDo != 0){
|
|
this->LockToDo = true;
|
|
this->ActToDo = 0;
|
|
|
|
uint32 Delay = this->CalculateDelay();
|
|
if(Delay < 1){
|
|
Delay = 1;
|
|
}
|
|
|
|
uint32 NextWakeup = ServerMilliseconds + Delay;
|
|
ToDoQueue.insert(NextWakeup, this->ID);
|
|
this->NextWakeup = NextWakeup;
|
|
}
|
|
}
|
|
|
|
void TCreature::ToDoYield(void){
|
|
if(!this->LockToDo){
|
|
this->ToDoWait(0);
|
|
this->ToDoStart();
|
|
}
|
|
}
|
|
|
|
void TCreature::ToDoWait(int Delay){
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDWait;
|
|
TD.Wait.Time = ServerMilliseconds + Delay;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoGo(int DestX, int DestY, int DestZ, bool MustReach, int MaxSteps){
|
|
if(this->posz > DestZ){
|
|
throw UPSTAIRS;
|
|
}else if(this->posz < DestZ){
|
|
throw DOWNSTAIRS;
|
|
}
|
|
|
|
if(this->LockToDo){
|
|
TToDoEntry *Last = this->ToDoList.at(this->NrToDo - 1);
|
|
if(Last->Code == TDGo
|
|
&& Last->Go.x == DestX
|
|
&& Last->Go.y == DestY
|
|
&& Last->Go.z == DestZ){
|
|
// TODO(fusion): Why? Shouldn't we just return here?
|
|
throw NOERROR;
|
|
}
|
|
}
|
|
|
|
int DistanceX = std::abs(DestX - this->posx);
|
|
int DistanceY = std::abs(DestY - this->posy);
|
|
int MaxDistance = std::max<int>(DistanceX, DistanceY);
|
|
if(MaxDistance == 0 || (!MustReach && MaxDistance <= 1)){
|
|
return;
|
|
}
|
|
|
|
// NOTE(fusion): The number of steps between two points is the same as the
|
|
// their manhattan distance, if we exclude diagonal movement. We can skip
|
|
// the path finder if we know we're step away from the destination.
|
|
if(DistanceX + DistanceY == 1){
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDGo;
|
|
TD.Go.x = DestX;
|
|
TD.Go.y = DestY;
|
|
TD.Go.z = DestZ;
|
|
this->ToDoAdd(TD);
|
|
}else{
|
|
int VisibleX = (this->Type == PLAYER) ? 7 : 10;
|
|
int VisibleY = (this->Type == PLAYER) ? 7 : 10;
|
|
TShortway Shortway(this, VisibleX, VisibleY);
|
|
if(!Shortway.Calculate(DestX, DestY, MustReach, MaxSteps)){
|
|
this->ToDoClear();
|
|
if(this->Type == PLAYER){
|
|
SendSnapback(this->Connection);
|
|
}
|
|
throw NOWAY;
|
|
}
|
|
}
|
|
}
|
|
|
|
void TCreature::ToDoRotate(int Direction){
|
|
if(Direction != DIRECTION_NORTH
|
|
&& Direction != DIRECTION_EAST
|
|
&& Direction != DIRECTION_SOUTH
|
|
&& Direction != DIRECTION_WEST){
|
|
throw ERROR;
|
|
}
|
|
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDRotate;
|
|
TD.Rotate.Direction = Direction;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoMove(int ObjX, int ObjY, int ObjZ, ObjectType Type, uint8 RNum,
|
|
int DestX, int DestY, int DestZ, uint8 Count){
|
|
Object Obj = GetObject(this->ID, ObjX, ObjY, ObjZ, RNum, Type);
|
|
if(!Obj.exists()){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
// TODO(fusion): This could be an inlined function.
|
|
if(ObjX != 0xFFFF){ // SPECIAL_COORDINATE ?
|
|
if(this->posz > ObjZ){
|
|
throw UPSTAIRS;
|
|
}else if(this->posz < ObjZ){
|
|
throw DOWNSTAIRS;
|
|
}
|
|
|
|
if(!ObjectInRange(this->ID, Obj, 1)){
|
|
this->ToDoGo(ObjX, ObjY, ObjZ, false, INT_MAX);
|
|
}
|
|
}
|
|
|
|
int Delay = 100;
|
|
if(Obj.getObjectType().isCreatureContainer()){
|
|
Object DestBank = GetFirstObject(DestX, DestY, DestZ);
|
|
if(DestBank == NONE || !DestBank.getObjectType().getFlag(BANK)){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
TCreature *Creature = GetCreature(Obj);
|
|
if(Creature == NULL){
|
|
error("TCreature::ToDoMove: Kreatur existiert nicht.\n");
|
|
throw ERROR;
|
|
}
|
|
|
|
Delay = 1000;
|
|
if(this->EarliestWalkTime > ServerMilliseconds){
|
|
Delay += (int)(this->EarliestWalkTime - ServerMilliseconds);
|
|
}
|
|
}
|
|
|
|
this->ToDoWait(Delay);
|
|
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDMove;
|
|
TD.Move.Obj = Obj.ObjectID;
|
|
TD.Move.x = DestX;
|
|
TD.Move.y = DestY;
|
|
TD.Move.z = DestZ;
|
|
TD.Move.Count = Count;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoMove(Object Obj, int DestX, int DestY, int DestZ, uint8 Count){
|
|
if(!Obj.exists()){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
if(!ObjectInRange(this->ID, Obj, 1)){
|
|
this->ToDoGo(DestX, DestY, DestZ, false, INT_MAX);
|
|
}
|
|
|
|
int Delay = 100;
|
|
if(Obj.getObjectType().isCreatureContainer()){
|
|
// TODO(fusion): We don't add the delay until the earliest walk time
|
|
// like in the other variant above.
|
|
Delay = 1000;
|
|
}
|
|
|
|
this->ToDoWait(Delay);
|
|
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDMove;
|
|
TD.Move.Obj = Obj.ObjectID;
|
|
TD.Move.x = DestX;
|
|
TD.Move.y = DestY;
|
|
TD.Move.z = DestZ;
|
|
TD.Move.Count = Count;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoTrade(int ObjX, int ObjY, int ObjZ, ObjectType Type, uint8 RNum,
|
|
uint32 TradePartner){
|
|
Object Obj = GetObject(this->ID, ObjX, ObjY, ObjZ, RNum, Type);
|
|
if(!Obj.exists()){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
ObjectType ObjType = Obj.getObjectType();
|
|
if(ObjType.getFlag(UNMOVE)){
|
|
throw NOTMOVABLE;
|
|
}
|
|
|
|
if(!ObjType.getFlag(TAKE)){
|
|
throw NOTTAKABLE;
|
|
}
|
|
|
|
if(ObjType.isCreatureContainer()){
|
|
error("TCreature::ToDoTrade: Objekt ist eine Kreatur.\n");
|
|
throw ERROR;
|
|
}
|
|
|
|
if(TradePartner == 0){
|
|
error("TCreature::ToDoTrade: Handelspartner ist Null.\n");
|
|
throw ERROR;
|
|
}
|
|
|
|
TCreature *Creature = GetCreature(TradePartner);
|
|
if(Creature == NULL){
|
|
throw PLAYERNOTONLINE;
|
|
}
|
|
|
|
if(Creature->Type != PLAYER){
|
|
error("TCreature::ToDoTrade: Handelspartner von %s ist kein Spieler.\n", this->Name);
|
|
throw ERROR;
|
|
}
|
|
|
|
// TODO(fusion): This could be an inlined function.
|
|
if(ObjX != 0xFFFF){ // SPECIAL_COORDINATE ?
|
|
if(this->posz > ObjZ){
|
|
throw UPSTAIRS;
|
|
}else if(this->posz < ObjZ){
|
|
throw DOWNSTAIRS;
|
|
}
|
|
|
|
if(!ObjectInRange(this->ID, Obj, 1)){
|
|
this->ToDoGo(ObjX, ObjY, ObjZ, false, INT_MAX);
|
|
}
|
|
}
|
|
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDTrade;
|
|
TD.Trade.Obj = Obj.ObjectID;
|
|
TD.Trade.Partner = TradePartner;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoUse(uint8 Count, int ObjX1, int ObjY1, int ObjZ1, ObjectType Type1, uint8 RNum1,
|
|
uint8 Dummy, int ObjX2, int ObjY2, int ObjZ2, ObjectType Type2, uint8 RNum2){
|
|
Object Obj1 = GetObject(this->ID, ObjX1, ObjY1, ObjZ1, RNum1, Type1);
|
|
if(!Obj1.exists()){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
Object Obj2 = NONE;
|
|
if(Count >= 2){
|
|
Obj2 = GetObject(this->ID, ObjX2, ObjY2, ObjZ2, RNum2, Type2);
|
|
if(!Obj2.exists()){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
}
|
|
|
|
// TODO(fusion): This could be an inlined function.
|
|
if(ObjX1 != 0xFFFF){ // SPECIAL_COORDINATE ?
|
|
if(this->posz > ObjZ1){
|
|
throw UPSTAIRS;
|
|
}else if(this->posz < ObjZ1){
|
|
throw DOWNSTAIRS;
|
|
}
|
|
|
|
if(!ObjectInRange(this->ID, Obj1, 1)){
|
|
this->ToDoGo(ObjX1, ObjY1, ObjZ1, false, INT_MAX);
|
|
}
|
|
}
|
|
|
|
this->ToDoWait(100);
|
|
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDUse;
|
|
TD.Use.Obj1 = Obj1.ObjectID;
|
|
TD.Use.Obj2 = Obj2.ObjectID;
|
|
// TODO(fusion): This is the value sent by the client on single object use
|
|
// that contains the next free container index.
|
|
TD.Use.Dummy = Dummy;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoUse(uint8 Count, Object Obj1, Object Obj2){
|
|
if(!Obj1.exists()){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
if(Count <= 1){
|
|
Obj2 = NONE;
|
|
}else if(!Obj2.exists()){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
if(!ObjectInRange(this->ID, Obj1, 1)){
|
|
int ObjX1, ObjY1, ObjZ1;
|
|
GetObjectCoordinates(Obj1, &ObjX1, &ObjY1, &ObjZ1);
|
|
this->ToDoGo(ObjX1, ObjY1, ObjZ1, false, INT_MAX);
|
|
}
|
|
|
|
this->ToDoWait(100);
|
|
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDUse;
|
|
TD.Use.Obj1 = Obj1.ObjectID;
|
|
TD.Use.Obj2 = Obj2.ObjectID;
|
|
// TODO(fusion): Not sure why we set this value at all.
|
|
TD.Use.Dummy = Count;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoTurn(int ObjX, int ObjY, int ObjZ, ObjectType Type, uint8 RNum){
|
|
Object Obj = GetObject(this->ID, ObjX, ObjY, ObjZ, RNum, Type);
|
|
if(!Obj.exists()){
|
|
throw NOTACCESSIBLE;
|
|
}
|
|
|
|
// TODO(fusion): This could be an inlined function.
|
|
if(ObjX != 0xFFFF){ // SPECIAL_COORDINATE ?
|
|
if(this->posz > ObjZ){
|
|
throw UPSTAIRS;
|
|
}else if(this->posz < ObjZ){
|
|
throw DOWNSTAIRS;
|
|
}
|
|
|
|
if(!ObjectInRange(this->ID, Obj, 1)){
|
|
this->ToDoGo(ObjX, ObjY, ObjZ, false, INT_MAX);
|
|
}
|
|
}
|
|
|
|
this->ToDoWait(100);
|
|
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDTurn;
|
|
TD.Turn.Obj = Obj.ObjectID;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoAttack(void){
|
|
this->Combat.CanToDoAttack();
|
|
if(this->Combat.GetDistance() != 1){
|
|
this->ToDoWait(100);
|
|
}
|
|
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDAttack;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoTalk(int Mode, const char *Addressee, const char *Text, bool CheckSpamming){
|
|
TToDoEntry TD = {};
|
|
|
|
if(Text == NULL || Text[0] == 0){
|
|
error("TCreature::ToDoTalk: Text ist NULL bei %s.\n", this->Name);
|
|
|
|
// TODO(fusion): The original code would attempt to call `AddDynamicString`
|
|
// even after this check but it doesn't check whether `Text` is NULL and
|
|
// calls `strlen` immediately on it which could be a problem.
|
|
TD.Talk.Text = 0;
|
|
}else{
|
|
TD.Talk.Text = AddDynamicString(Text);
|
|
}
|
|
|
|
if(Addressee == NULL){
|
|
TD.Talk.Addressee = 0;
|
|
}else{
|
|
TD.Talk.Addressee = AddDynamicString(Addressee);
|
|
}
|
|
|
|
TD.Code = TDTalk;
|
|
TD.Talk.Mode = Mode;
|
|
TD.Talk.CheckSpamming = CheckSpamming;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::ToDoChangeState(int NewState){
|
|
TToDoEntry TD = {};
|
|
TD.Code = TDChangeState;
|
|
TD.ChangeState.NewState = NewState;
|
|
this->ToDoAdd(TD);
|
|
}
|
|
|
|
void TCreature::NotifyGo(void){
|
|
// IMPORTANT(fusion): This and the function that does move `this->CrObject`
|
|
// should be the only ones where the output of `GetObjectCoordinates` will
|
|
// differ from `this->posx`, `this->posy`, and `this->posz`.
|
|
|
|
int DestX, DestY, DestZ;
|
|
GetObjectCoordinates(this->CrObject, &DestX, &DestY, &DestZ);
|
|
MoveChainCreature(this, DestX, DestY);
|
|
|
|
int OrigX = this->posx;
|
|
int OrigY = this->posy;
|
|
int OrigZ = this->posz;
|
|
bool DiagonalMove = (DestX != OrigX && DestY != OrigY && DestZ == OrigZ);
|
|
|
|
// IMPORTANT(fusion): `SendFloors` and `SendRow` will use the current creature
|
|
// position to know where to pull fields from, so we need to keep them updated
|
|
// as we go.
|
|
if(this->Type == PLAYER && this->Connection != NULL){
|
|
int DistanceX = std::abs(DestX - OrigX);
|
|
int DistanceY = std::abs(DestY - OrigY);
|
|
int DistanceZ = std::abs(DestZ - OrigZ);
|
|
if(DistanceX <= 1 && DistanceY <= 1 && DistanceZ <= 1){
|
|
while(this->posz < DestZ){
|
|
this->posx -= 1;
|
|
this->posy -= 1;
|
|
this->posz += 1;
|
|
SendFloors(this->Connection, false);
|
|
}
|
|
|
|
while(this->posz > DestZ){
|
|
this->posx += 1;
|
|
this->posy += 1;
|
|
this->posz -= 1;
|
|
SendFloors(this->Connection, true);
|
|
}
|
|
|
|
while(this->posx < DestX){
|
|
this->posx += 1;
|
|
SendRow(this->Connection, DIRECTION_EAST);
|
|
}
|
|
|
|
while(this->posx > DestX){
|
|
this->posx -= 1;
|
|
SendRow(this->Connection, DIRECTION_WEST);
|
|
}
|
|
|
|
while(this->posy < DestY){
|
|
this->posy += 1;
|
|
SendRow(this->Connection, DIRECTION_SOUTH);
|
|
}
|
|
|
|
while(this->posy > DestY){
|
|
this->posy -= 1;
|
|
SendRow(this->Connection, DIRECTION_NORTH);
|
|
}
|
|
}else{
|
|
this->posx = DestX;
|
|
this->posy = DestY;
|
|
this->posz = DestZ;
|
|
SendFullScreen(this->Connection);
|
|
}
|
|
}else{
|
|
this->posx = DestX;
|
|
this->posy = DestY;
|
|
this->posz = DestZ;
|
|
}
|
|
|
|
if(this->Type == PLAYER){
|
|
// NOTE(fusion): Check open containers.
|
|
for(int ContainerNr = 0;
|
|
ContainerNr < NARRAY(TPlayer::OpenContainer);
|
|
ContainerNr += 1){
|
|
Object Con = ((TPlayer*)this)->GetOpenContainer(ContainerNr);
|
|
if(Con == NONE){
|
|
continue;
|
|
}
|
|
|
|
if(!Con.exists()){
|
|
error("TCreature::NotifyGo: OpenContainer existiert nicht. (%s, [%d,%d,%d]->[%d,%d,%d])\n",
|
|
this->Name, OrigX, OrigY, OrigZ, DestX, DestY, DestZ);
|
|
continue;
|
|
}
|
|
|
|
if(!ObjectAccessible(this->ID, Con, 1)){
|
|
((TPlayer*)this)->SetOpenContainer(ContainerNr, NONE);
|
|
SendCloseContainer(this->Connection, ContainerNr);
|
|
}
|
|
}
|
|
|
|
// NOTE(fusion): Check trade.
|
|
Object TradeObject = ((TPlayer*)this)->TradeObject;
|
|
if(TradeObject != NONE){
|
|
TPlayer *Partner = GetPlayer(((TPlayer*)this)->TradePartner);
|
|
if(!TradeObject.exists()){
|
|
error("TCreature::NotifyGo: Handelsobjekt existiert nicht mehr.\n");
|
|
error("# Händler %s an [%d,%d,%d]\n", this->Name, DestX, DestY, DestZ);
|
|
if(Partner != NULL){
|
|
error("# Partner %s an [%d,%d,%d]\n", Partner->Name,
|
|
Partner->posx, Partner->posy, Partner->posz);
|
|
}
|
|
}
|
|
|
|
if(Partner == NULL || !ObjectAccessible(this->ID, TradeObject, 1)
|
|
|| ObjectDistance(this->CrObject, Partner->CrObject) > 2
|
|
|| !ThrowPossible(this->posx, this->posy, this->posz,
|
|
Partner->posx, Partner->posy, Partner->posz, 0)){
|
|
SendCloseTrade(this->Connection);
|
|
SendMessage(this->Connection, TALK_FAILURE_MESSAGE, "Trade cancelled.");
|
|
((TPlayer*)this)->RejectTrade();
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO(fusion): This is probably an inlined function to get the first object
|
|
// with some specific flag on a field.
|
|
int Waypoints;
|
|
Object Bank = GetFirstObject(DestX, DestY, DestZ);
|
|
while(Bank != NONE){
|
|
ObjectType BankType = Bank.getObjectType();
|
|
if(BankType.getFlag(BANK)){
|
|
Waypoints = BankType.getAttribute(WAYPOINTS);
|
|
break;
|
|
}
|
|
Bank = Bank.getNextObject();
|
|
}
|
|
|
|
if(Bank != NONE){
|
|
// NOTE(fusion): Diagonal movement has three times the delay of a regular one.
|
|
if(DiagonalMove){
|
|
Waypoints *= 3;
|
|
}
|
|
|
|
// NOTE(fusion): Compute the step interval and quantize it to be a multiple
|
|
// of `Beat`, rounding up.
|
|
int Delay = (Waypoints * 1000) / this->GetSpeed();
|
|
int BeatCount = (Delay + Beat - 1) / Beat;
|
|
this->EarliestWalkTime = ServerMilliseconds + BeatCount * Beat;
|
|
}else{
|
|
error("TCreature::NotifyGo: Auf Feld [%d,%d,%d] befindet sich kein Bank.\n",
|
|
DestX, DestY, DestZ);
|
|
}
|
|
}
|
|
|
|
void TCreature::NotifyTurn(Object DestCon){
|
|
int DestX, DestY, DestZ;
|
|
GetObjectCoordinates(DestCon, &DestX, &DestY, &DestZ);
|
|
|
|
// NOTE(fusion): This is somewhat similar to `TCreature::Rotate`.
|
|
int OffsetX = DestX - this->posx;
|
|
int OffsetY = DestY - this->posy;
|
|
if(OffsetX > 0){
|
|
this->Direction = DIRECTION_EAST;
|
|
}else if(OffsetX < 0){
|
|
this->Direction = DIRECTION_WEST;
|
|
}else if(OffsetY < 0){
|
|
this->Direction = DIRECTION_NORTH;
|
|
}else if(OffsetY > 0){
|
|
this->Direction = DIRECTION_SOUTH;
|
|
}
|
|
}
|
|
|
|
void TCreature::NotifyCreate(void){
|
|
InsertChainCreature(this, 0, 0);
|
|
}
|
|
|
|
void TCreature::NotifyDelete(void){
|
|
if(this->Type == PLAYER){
|
|
if(this->Connection != NULL){
|
|
this->Connection->Logout(30, true);
|
|
this->LoggingOut = true;
|
|
}
|
|
|
|
((TPlayer*)this)->RejectTrade();
|
|
}
|
|
|
|
DeleteChainCreature(this);
|
|
}
|
|
|
|
void TCreature::NotifyChangeInventory(void){
|
|
if(this->CrObject == NONE){
|
|
return;
|
|
}
|
|
|
|
if(!this->CrObject.exists()){
|
|
error("TCreature::NotifyChangeInventory: Kreatur-Objekt existiert nicht.\n");
|
|
error("# Kreatur: %s, Position: %d/%d/%d.\n",
|
|
this->Name, this->posx, this->posy, this->posz);
|
|
return;
|
|
}
|
|
|
|
this->Combat.CheckCombatValues();
|
|
if(this->Type == PLAYER){
|
|
int NewDelta[NARRAY(this->Skills)];
|
|
for(int Position = INVENTORY_FIRST;
|
|
Position <= INVENTORY_LAST;
|
|
Position += 1){
|
|
Object Obj = GetBodyObject(this->ID, Position);
|
|
if(Obj == NONE){
|
|
continue;
|
|
}
|
|
|
|
ObjectType ObjType = Obj.getObjectType();
|
|
if(!ObjType.getFlag(SKILLBOOST)){
|
|
continue;
|
|
}
|
|
|
|
if(!ObjType.getFlag(CLOTHES)){
|
|
error("TCreature::NotifyChangeInventory: Objekt %d hat SKILLBOOST, aber nicht CLOTHES.\n",
|
|
ObjType.TypeID);
|
|
continue;
|
|
}
|
|
|
|
if((int)ObjType.getAttribute(BODYPOSITION) != Position){
|
|
continue;
|
|
}
|
|
|
|
int SkillNr = (int)ObjType.getAttribute(SKILLNUMBER);
|
|
if(SkillNr < 0 || SkillNr >= NARRAY(this->Skills)){
|
|
error("TCreature::NotifyChangeInventory: Objekt %d hat ungültige SKILLNUMBER %d.\n",
|
|
ObjType.TypeID, SkillNr);
|
|
continue;
|
|
}
|
|
|
|
// IMPORTANT(fusion): The value for the skill modification is stored
|
|
// as uint32 but is bitcast from a signed integer on load, meaning that
|
|
// that bitcasting it back should retrieve the original signed value.
|
|
// See `LoadObjects`.
|
|
NewDelta[SkillNr] += (int)ObjType.getAttribute(SKILLMODIFICATION);
|
|
}
|
|
|
|
bool SkillsChanged = false;
|
|
for(int SkillNr = 0;
|
|
SkillNr < NARRAY(this->Skills);
|
|
SkillNr += 1){
|
|
TSkill *Skill = this->Skills[SkillNr];
|
|
if(Skill->DAct != NewDelta[SkillNr]){
|
|
SkillsChanged = true;
|
|
Skill->DAct = NewDelta[SkillNr];
|
|
if(SkillNr == SKILL_GO_STRENGTH){
|
|
AnnounceChangedCreature(this->ID, CREATURE_SPEED_CHANGED);
|
|
}else if(SkillNr == SKILL_ILLUSION){
|
|
if(NewDelta[SKILL_ILLUSION] > 0){
|
|
if(this->Outfit.OutfitID != 0 || this->Outfit.ObjectType != 0){
|
|
if(Skill->TimerValue() != 0){
|
|
this->SetTimer(SKILL_ILLUSION, 0, 0, 0, -1);
|
|
}
|
|
this->Outfit.OutfitID = 0;
|
|
this->Outfit.ObjectType = 0;
|
|
}
|
|
}else if(Skill->TimerValue() == 0){
|
|
this->Outfit = this->OrgOutfit;
|
|
}
|
|
AnnounceChangedCreature(this->ID, CREATURE_OUTFIT_CHANGED);
|
|
}
|
|
}
|
|
}
|
|
|
|
if(SkillsChanged){
|
|
SendPlayerSkills(this->Connection);
|
|
((TPlayer*)this)->CheckState();
|
|
}
|
|
|
|
SendPlayerData(this->Connection);
|
|
}
|
|
}
|