aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorfusion32 <marcopuzziello@gmail.com>2025-06-25 10:09:17 -0300
committerfusion32 <marcopuzziello@gmail.com>2025-06-25 10:09:17 -0300
commitea319bc2fbef91e3ea062be554b215756648d1d3 (patch)
treeaa26422a754346e9881e5e0b510f78fd4cfe068c /src
parent8bc2e5dc3a523af471065752a534d20d878c60b0 (diff)
downloadgame-ea319bc2fbef91e3ea062be554b215756648d1d3.tar.gz
game-ea319bc2fbef91e3ea062be554b215756648d1d3.zip
finish `crnonpl.cc`
Diffstat (limited to 'src')
-rw-r--r--src/connections.cc3
-rw-r--r--src/cr.hh46
-rw-r--r--src/crcombat.cc8
-rw-r--r--src/crmain.cc6
-rw-r--r--src/crnonpl.cc1078
-rw-r--r--src/magic.cc5
-rw-r--r--src/magic.hh7
-rw-r--r--src/operate.cc13
-rw-r--r--src/stubs.hh3
9 files changed, 1119 insertions, 50 deletions
diff --git a/src/connections.cc b/src/connections.cc
index fe26fea..7cf70c7 100644
--- a/src/connections.cc
+++ b/src/connections.cc
@@ -307,8 +307,7 @@ bool TConnection::IsVisible(int x, int y, int z){
int PlayerX, PlayerY, PlayerZ;
this->GetPosition(&PlayerX, &PlayerY, &PlayerZ);
- // NOTE(fusion): The player can see only floors above ground when above
- // ground, or two floors up and down when underground.
+ // TODO(fusion): Have a standalone version of `TCreature::CanSeeFloor`?
if(PlayerZ <= 7){
if(z > 7){
return false;
diff --git a/src/cr.hh b/src/cr.hh
index de40fcc..e095903 100644
--- a/src/cr.hh
+++ b/src/cr.hh
@@ -26,6 +26,10 @@ struct TOutfit{
return this->OutfitID == Other.OutfitID
&& this->PackedData == Other.PackedData;
}
+
+ static constexpr TOutfit Invisible(void){
+ return TOutfit{};
+ }
};
struct TSkillData {
@@ -555,6 +559,23 @@ struct TCreature: TSkillBase {
void NotifyDelete(void);
void NotifyChangeInventory(void);
+ void Kill(void){
+ this->Skills[SKILL_HITPOINTS]->Set(0);
+ this->Death();
+ }
+
+ bool IsInvisible(void){
+ return this->Outfit == TOutfit::Invisible();
+ }
+
+ bool CanSeeFloor(int FloorZ){
+ if(this->posz <= 7){
+ return FloorZ <= 7;
+ }else{
+ return std::abs(this->posz - FloorZ) <= 2;
+ }
+ }
+
// VIRTUAL FUNCTIONS
// =================
virtual ~TCreature(void); // VTABLE[ 0]
@@ -756,10 +777,23 @@ struct TMonsterhome {
struct TMonster: TNonplayer {
TMonster(int Race, int x, int y, int z, int Home, uint32 MasterID);
+ bool CanKickBoxes(void);
+ void KickBoxes(Object Obj);
+ bool KickCreature(TCreature *Creature);
+ void Convince(TCreature *NewMaster);
+ void SetTarget(TCreature *NewTarget);
+ bool IsPlayerControlled(void);
+ bool IsFleeing(void);
// VIRTUAL FUNCTIONS
// =================
~TMonster(void) override;
+ bool MovePossible(int x, int y, int z, bool Execute, bool Jump) override;
+ bool IsPeaceful(void) override;
+ uint32 GetMaster(void) override;
+ void DamageStimulus(uint32 AttackerID, int Damage, int DamageType) override;
+ void IdleStimulus(void) override;
+ void CreatureMoveStimulus(uint32 CreatureID, int Type) override;
// DATA
//TNonplayer super_TNonplayer; // IMPLICIT
@@ -945,9 +979,17 @@ void LoadMonsterhomes(void);
void ProcessMonsterhomes(void);
void NotifyMonsterhomeOfDeath(int Nr);
bool MonsterhomeInRange(int Nr, int x, int y, int z);
+
void ChangeNPCState(TCreature *Npc, int NewState, bool Stimulus);
-void InitNonplayer();
-void ExitNonplayer();
+
+TCreature *CreateMonster(int Race, int x, int y, int z,
+ int Home, uint32 MasterID, bool ShowEffect);
+void ConvinceMonster(TCreature *Master, TCreature *Slave);
+void ChallengeMonster(TCreature *Challenger, TCreature *Monster);
+
+void InitNPCs(void);
+void InitNonplayer(void);
+void ExitNonplayer(void);
// crplayer.cc
// =============================================================================
diff --git a/src/crcombat.cc b/src/crcombat.cc
index 0b45ea4..df8b312 100644
--- a/src/crcombat.cc
+++ b/src/crcombat.cc
@@ -420,7 +420,7 @@ void TCombat::SetAttackDest(uint32 TargetID, bool Follow){
}
if(Master->Type == PLAYER && Target->Type != PLAYER){
- if(Target->Outfit.OutfitID == 0 && Target->Outfit.ObjectType == 0){
+ if(Target->IsInvisible()){
this->StopAttack(0);
throw TARGETLOST;
}
@@ -463,7 +463,7 @@ void TCombat::CanToDoAttack(void){
}
if(Master->Type == PLAYER && Target->Type != PLAYER){
- if(Target->Outfit.OutfitID == 0 && Target->Outfit.ObjectType == 0){
+ if(Target->IsInvisible()){
this->StopAttack(0);
throw TARGETLOST;
}
@@ -559,7 +559,7 @@ void TCombat::Attack(void){
}
if(Master->Type == PLAYER && Target->Type != PLAYER){
- if(Target->Outfit.OutfitID == 0 && Target->Outfit.ObjectType == 0){
+ if(Target->IsInvisible()){
this->StopAttack(0);
throw TARGETLOST;
}
@@ -696,7 +696,7 @@ void TCombat::WandAttack(TCreature *Target){
ASSERT(this->Wand != NONE);
TCreature *Master = this->Master;
- int Distance = std::min<int>(
+ int Distance = std::max<int>(
std::abs(Master->posx - Target->posx),
std::abs(Master->posy - Target->posy));
diff --git a/src/crmain.cc b/src/crmain.cc
index 3d45827..b786676 100644
--- a/src/crmain.cc
+++ b/src/crmain.cc
@@ -589,11 +589,9 @@ int TCreature::Damage(TCreature *Attacker, int Damage, int DamageType){
this->DamageStimulus(Attacker->ID, Damage, DamageType);
- // NOTE(fusion): Remove non-player illusion. Might as well be an inlined
+ // NOTE(fusion): Remove non-player invisibility. Might as well be an inlined
// function.
- if(this->Type != PLAYER
- && this->Outfit.OutfitID == 0
- && this->Outfit.ObjectType == 0){
+ if(this->Type != PLAYER && this->IsInvisible()){
this->SetTimer(SKILL_ILLUSION, 0, 0, 0, -1);
this->Outfit = this->OrgOutfit;
AnnounceChangedCreature(this->ID, CREATURE_OUTFIT_CHANGED);
diff --git a/src/crnonpl.cc b/src/crnonpl.cc
index 433718a..3d43012 100644
--- a/src/crnonpl.cc
+++ b/src/crnonpl.cc
@@ -6,6 +6,8 @@
#include "stubs.hh"
+#include <dirent.h>
+
static vector<TNonplayer*> NonplayerList(0, 10000, 1000, NULL);
static int FirstFreeNonplayer;
@@ -1422,16 +1424,8 @@ void ProcessMonsterhomes(void){
break;
}
- // TODO(fusion): There is probably a helper function to check whether
- // a floor is visible from another.
- if(Player->posz <= 7){
- if(MH->z > 7){
- continue;
- }
- }else{
- if(std::abs(Player->posz - MH->z) > 2){
- continue;
- }
+ if(!Player->CanSeeFloor(MH->z)){
+ continue;
}
int DistanceX = std::abs(Player->posx - MH->x);
@@ -1772,18 +1766,17 @@ void TNPC::IdleStimulus(void){
}
}
- if(!FoundDest){
+ if(FoundDest){
+ try{
+ this->ToDoGo(DestX, DestY, DestZ, true, INT_MAX);
+ this->ToDoWait(2000);
+ this->ToDoStart();
+ }catch(RESULT r){
+ error("TNPC::IdleStimulus: Exception %d!\n", r);
+ }
+ }else{
this->ToDoWait(2000);
this->ToDoStart();
- return;
- }
-
- try{
- this->ToDoGo(DestX, DestY, DestZ, true, INT_MAX);
- this->ToDoWait(2000);
- this->ToDoStart();
- }catch(RESULT r){
- error("TNPC::IdleStimulus: Exception %d!\n", r);
}
}
}
@@ -2025,6 +2018,8 @@ TMonster::TMonster(int Race, int x, int y, int z, int Home, uint32 MasterID) :
return;
}
+ // TODO(fusion): This part is very similar to `ProcessMonsterRaids` when
+ // adding extra items to spawned monsters.
try{
if(this->Master == 0 && RaceData[Race].Items > 0){
Object Bag = Create(GetBodyContainer(this->ID, INVENTORY_BAG),
@@ -2045,7 +2040,6 @@ TMonster::TMonster(int Race, int x, int y, int z, int Home, uint32 MasterID) :
}
for(int j = 0; j < Repeat; j += 1){
- // TODO(fusion): Same as `ProcessMonsterRaids`.
Object Item = NONE;
try{
if(ItemType.getFlag(WEAPON)
@@ -2108,3 +2102,1045 @@ TMonster::~TMonster(void){
NotifyMonsterhomeOfDeath(this->Home);
}
}
+
+bool TMonster::MovePossible(int x, int y, int z, bool Execute, bool Jump){
+ if(this->posz != z){
+ error("TMonster::MovePossible: Prüfung über Ebenen hinweg ([%d,%d,%d] -> [%d,%d,%d]).\n",
+ this->posx, this->posy, this->posz, x, y, z);
+ return false;
+ }
+
+ if(this->State != ATTACKING && this->State != PANIC){
+ if(!MonsterhomeInRange(this->Home, x, y, z)){
+ return false;
+ }
+
+ // NOTE(fusion): Max distance.
+ int Distance = std::max<int>(
+ std::abs(x - this->posx),
+ std::abs(y - this->posy));
+ if(Distance > this->Radius){
+ return false;
+ }
+ }
+
+ if(this->Skills[SKILL_GO_STRENGTH]->Act < 0){
+ error("TMonster::MovePossible: Monster %s an [%d,%d,%d] darf sich nicht bewegen.\n",
+ this->Name, this->posx, this->posy, this->posz);
+ return false;
+ }
+
+ if(IsProtectionZone(x, y, z) || IsHouse(x, y, z)){
+ return false;
+ }
+
+ if(Execute && this->Master != 0 && this->State != ATTACKING && this->State != PANIC){
+ TCreature *Master = GetCreature(this->Master);
+ if(Master != NULL && Master->posz == this->posz){
+ // NOTE(fusion): Manhattan distance.
+ if((std::abs(Master->posx - this->posx) + std::abs(Master->posy - this->posy)) > 1
+ && (std::abs(Master->posx - x) + std::abs(Master->posy - y)) <= 1){
+ return false;
+ }
+ }
+ }
+
+ // NOTE(fusion): Check destination and retry while we keep kicking blocking
+ // objects away.
+ for(int Attempt = 0; Attempt < 100; Attempt += 1){
+ Object Obj = GetFirstObject(x, y, z);
+ if(!Obj.getObjectType().getFlag(BANK)){
+ return false;
+ }
+
+ while(Obj != NONE){
+ ObjectType ObjType = Obj.getObjectType();
+ if(ObjType.isCreatureContainer()){
+ if(this->State != ATTACKING && this->State != PANIC){
+ return false;
+ }
+
+ if(this->Target == 0){
+ return false;
+ }
+
+ if(!RaceData[this->Race].KickCreatures){
+ return false;
+ }
+
+ TCreature *Creature = GetCreature(Obj);
+ if(Creature == NULL){
+ error("TMonster::MovePossible: Kann Hindernis-Kreatur nicht identifizieren.\n");
+ return false;
+ }
+
+ if(Creature->ID == this->Target || Creature->ID == this->Master){
+ return false;
+ }
+
+ if(RaceData[Creature->Race].Unpushable){
+ return false;
+ }
+
+ // TODO(fusion): Why?
+ if(!RaceData[this->Race].SeeInvisible && Creature->IsInvisible()){
+ return false;
+ }
+
+ if(Creature->Type == NPC){
+ return false;
+ }
+
+ if(Creature->Type == PLAYER){
+ if(this->Master != 0 || CheckRight(Creature->ID, IGNORED_BY_MONSTERS)){
+ return false;
+ }
+ }
+
+ if(Execute){
+ if(Creature->Type == PLAYER){
+ this->Target = 0;
+ throw EXHAUSTED;
+ }
+
+ if(!this->KickCreature(Creature)){
+ throw EXHAUSTED;
+ }
+
+ // NOTE(fusion): Break from the inner loop and retry.
+ break;
+ }
+ }else{
+ if(ObjType.getFlag(UNPASS)){
+ if(ObjType.getFlag(UNMOVE) || !this->CanKickBoxes()){
+ return false;
+ }
+
+ if(Execute){
+ this->KickBoxes(Obj);
+
+ // NOTE(fusion): Break from the inner loop and retry.
+ break;
+ }
+ }
+
+ if(ObjType.getFlag(AVOID)){
+ int AvoidDamageTypes = (int)ObjType.getAttribute(AVOIDDAMAGETYPES);
+ bool IgnoreHazard = (this->State == PANIC && AvoidDamageTypes != 0)
+ || (RaceData[this->Race].NoPoison && AvoidDamageTypes == DAMAGE_POISON)
+ || (RaceData[this->Race].NoBurning && AvoidDamageTypes == DAMAGE_FIRE)
+ || (RaceData[this->Race].NoEnergy && AvoidDamageTypes == DAMAGE_ENERGY);
+ if(!IgnoreHazard){
+ if(ObjType.getFlag(UNMOVE) || !this->CanKickBoxes()){
+ return false;
+ }
+
+ if(Execute){
+ this->KickBoxes(Obj);
+
+ // NOTE(fusion): Break from the inner loop and retry.
+ break;
+ }
+ }
+ }
+ }
+ Obj = Obj.getNextObject();
+ }
+
+ if(Obj == NONE){
+ return true;
+ }
+ }
+
+ error("TMonster::MovePossible: Endlosschleife vermutet für %s an [%d,%d,%d].\n",
+ this->Name, x, y, z);
+ return false;
+}
+
+bool TMonster::IsPeaceful(void){
+ return this->Master != 0
+ && IsCreaturePlayer(this->Master);
+}
+
+uint32 TMonster::GetMaster(void){
+ return this->Master;
+}
+
+void TMonster::DamageStimulus(uint32 AttackerID, int Damage, int DamageType){
+ if(AttackerID != 0 && Damage != 0){
+ if(this->State == SLEEPING){
+ if(this->Target == 0){
+ this->State = PANIC;
+ }else{
+ this->State = UNDERATTACK;
+ }
+ this->ToDoYield();
+ }else{
+ if(this->Target == 0){
+ this->State = PANIC;
+ }else if(this->State == IDLE){
+ this->State = UNDERATTACK;
+ }
+ }
+ }
+}
+
+void TMonster::IdleStimulus(void){
+ if(this->LockToDo || this->LoggingOut){
+ return;
+ }
+
+ if(this->LifeEndRound != 0 && this->LifeEndRound <= RoundNr){
+ print(3, "Lebenszeit für %s abgelaufen.\n", this->Name);
+ this->StartLogout(true, true);
+ this->State = SLEEPING;
+ return;
+ }
+
+ if(this->Master != 0){
+ TCreature *Master = GetCreature(this->Master);
+ bool MasterIsPlayer = IsCreaturePlayer(this->Master);
+ bool ShouldDespawn = false;
+ if(Master == NULL){
+ if(MasterIsPlayer){
+ print(3, "Kreatur %s verliert ihren Spieler-Master.\n", this->Name);
+ }else{
+ print(3, "Kreatur %s verliert ihr Mutter-Monster.\n", this->Name);
+ }
+ ShouldDespawn = true;
+ }else if(MasterIsPlayer && Master->SummonedCreatures == 0){
+ print(3, "Spieler-Master hat sich eben wieder neu eingeloggt.\n");
+ ShouldDespawn = true;
+ }else if(!MasterIsPlayer && Master->posz != this->posz){
+ print(3, "Mutter-Monster ist zu weit entfernt.\n");
+ ShouldDespawn = true;
+ }else if(std::abs(Master->posz - this->posz) > 1
+ || std::abs(Master->posx - this->posx) > 30
+ || std::abs(Master->posy - this->posy) > 30){
+ if(MasterIsPlayer){
+ print(3, "Spieler-Master ist zu weit entfernt.\n");
+ }else{
+ print(3, "Mutter-Monster ist zu weit entfernt.\n");
+ }
+ ShouldDespawn = true;
+ }
+
+ if(ShouldDespawn){
+ if(MasterIsPlayer){
+ this->StartLogout(true, true);
+ }else{
+ this->Kill();
+ }
+ this->State = SLEEPING;
+ return;
+ }
+
+ if(Master->Combat.Following){
+ this->Target = 0;
+ }else{
+ this->Target = Master->Combat.AttackDest;
+ }
+
+ if(this->Target == 0 || this->Target == this->ID){
+ this->Target = this->Master;
+ }
+
+ }else{
+ if(!MonsterhomeInRange(this->Home, this->posx, this->posy, this->posz)){
+ TMonsterhome *MH = Monsterhome.at(this->Home);
+ print(3, "%s an [%d,%d,%d] zu weit von [%d,%d,%d] entfernt.\n",
+ this->Name, this->posx, this->posy, this->posz, MH->x, MH->y, MH->z);
+ this->StartLogout(true, true);
+ this->State = SLEEPING;
+ return;
+ }
+ }
+
+ if(this->Target != 0){
+ TCreature *Target = GetCreature(this->Target);
+ bool LoseTarget = (Target == NULL)
+ // RANGE CHECK
+ || Target->posz != this->posz
+ || std::abs(Target->posx - this->posx) > 10
+ || std::abs(Target->posy - this->posy) > 10
+ // FIELD CHECK
+ || IsProtectionZone(Target->posx, Target->posy, Target->posz)
+ || IsHouse(Target->posx, Target->posy, Target->posz)
+ // INVISIBILITY CHECK
+ || (Target->IsInvisible() && !RaceData[this->Race].SeeInvisible)
+ // LOSETARGET CHECK
+ || (this->Master == 0 && random(0, 99) < RaceData[this->Race].LoseTarget);
+ if(LoseTarget){
+ this->Target = 0;
+ }
+ }
+
+ if(this->State != PANIC && this->State != UNDERATTACK){
+ this->State = IDLE;
+ }
+
+ // NOTE(fusion): TALKING.
+ if(RaceData[this->Race].Talks > 0 && (rand() % 50) == 0){
+ int TalkNr = rand() % (RaceData[this->Race].Talks + 1);
+ const char *Text = GetDynamicString(*RaceData[this->Race].Talk.at(TalkNr));
+ if(Text != 0 && Text[0] != 0){
+ // TODO(fusion): We were only checking for a `#` but it could be
+ // problematic for any two character nul terminated string, since
+ // adding 3 would make it point out of bounds. Poor string management
+ // is a recurring theme.
+ int Mode = TALK_ANIMAL_LOW;
+ if(Text[0] == '#' && (Text[1] == 'y' || Text[1] == 'Y') && Text[2] == ' '){
+ Mode = TALK_ANIMAL_LOUD;
+ Text += 3;
+ }
+
+ try{
+ Talk(this->ID, Mode, NULL, Text, false);
+ }catch(RESULT r){
+ error("TMonster::IdleStimulus: Exception %d bei Talk.\n", r);
+ }
+ }else{
+ // TODO(fusion): The original wouldn't check if `Text` is actually
+ // valid which could also be a problem.
+ error("TMonster::IdleStimulus: Race %d talk entry %d is invalid.",
+ this->Race, TalkNr);
+ }
+ }
+
+ // NOTE(fusion): TARGETING.
+ if(this->Target == 0){
+ bool ShouldSleep = true;
+ if(this->Master == 0){
+ // IMPORTANT(fusion): We don't iterate over the last strategy on purpose.
+ int Strategy = 0;
+ int StrategyRoll = random(0, 99);
+ while(Strategy < (NARRAY(TRaceData::Strategy) - 1)){
+ if(StrategyRoll < RaceData[this->Race].Strategy[Strategy]){
+ break;
+ }
+ StrategyRoll -= RaceData[this->Race].Strategy[Strategy];
+ Strategy += 1;
+ }
+
+ int BestStrategyParam = INT_MIN;
+ int BestTieBreaker = 0;
+ TFindCreatures Search(12, 12, this->ID, FIND_PLAYERS | FIND_MONSTERS);
+ while(true){
+ uint32 TargetID = Search.getNext();
+ if(TargetID == 0){
+ break;
+ }
+
+ TCreature *Target = GetCreature(TargetID);
+ if(Target == NULL){
+ error("TMonster::IdleStimulus: Kreatur existiert nicht.\n");
+ continue;
+ }
+
+ if(Target->Type == MONSTER && !((TMonster*)Target)->IsPlayerControlled()){
+ continue;
+ }
+
+ if(Target->CanSeeFloor(this->posz)){
+ ShouldSleep = false;
+ }
+
+ // TODO(fusion): This is quite similar to the conditions for losing
+ // the target. Perhaps there is some common inlined function.
+ int DistanceX = std::abs(Target->posx - this->posx);
+ int DistanceY = std::abs(Target->posy - this->posy);
+ if((Target->posz != this->posz || DistanceX > 10 || DistanceY > 10)
+ ||(Target->Type == PLAYER && CheckRight(Target->ID, IGNORED_BY_MONSTERS))
+ || (Target->IsInvisible() && !RaceData[this->Race].SeeInvisible)
+ || IsProtectionZone(Target->posx, Target->posy, Target->posz)
+ || IsHouse(Target->posx, Target->posy, Target->posz)
+ || std::abs(Target->posx - this->posx) > 10
+ || std::abs(Target->posy - this->posy) > 10){
+ continue;
+ }
+
+ int StrategyParam = 0;
+ if(Strategy == 0){ // STRATEGY_NEAREST
+ StrategyParam = -(DistanceX + DistanceY);
+ }else if(Strategy == 1){ // STRATEGY_LOWEST_HEALTH
+ StrategyParam = -Target->Skills[SKILL_HITPOINTS]->Get();
+ }else if(Strategy == 2){ // STRATEGY_MOST_DAMAGE
+ StrategyParam = this->Combat.GetDamageByCreature(TargetID);
+ }else if(Strategy == 3){ // STRATEGY_RANDOM
+ // no-op
+ }else{
+ error("TMonster::IdleStimulus: Unbekannte Strategie %d.\n", Strategy);
+ }
+
+ // NOTE(fusion): We're looking for the creature that maximizes
+ // the strategy parameter.
+ int TieBreaker = random(0, 99);
+ if(StrategyParam >= BestStrategyParam
+ ||(StrategyParam == BestStrategyParam && TieBreaker > BestTieBreaker)){
+ this->Target = TargetID;
+ BestTieBreaker = TieBreaker;
+ BestStrategyParam = StrategyParam;
+ }
+ }
+ }
+
+ if(ShouldSleep && this->Target == 0
+ && this->State != UNDERATTACK
+ && this->State != PANIC){
+ if(this->Master == 0){
+ this->State = SLEEPING;
+ }else{
+ this->ToDoWait(1000);
+ this->ToDoStart();
+ }
+ return;
+ }
+
+ if(this->State == PANIC && this->Target == 0){
+ this->State = IDLE;
+ }
+
+ if(this->State == UNDERATTACK){
+ this->State = IDLE;
+ }
+ }
+
+ // NOTE(fusion): CASTING.
+ // TODO(fusion): It is highly unlikely but we could cast all spells at once.
+ // I'm not sure this is correct but I can see it happening.
+ if(RaceData[this->Race].Spells > 0){
+ TCreature *Target = GetCreature(this->Target);
+ for(int SpellNr = 1;
+ SpellNr <= RaceData[this->Race].Spells;
+ SpellNr += 1){
+ TSpellData *SpellData = RaceData[this->Race].Spell.at(SpellNr);
+ if((rand() % SpellData->Delay) != 0){
+ continue;
+ }
+
+ if(this->IsFleeing() && random(1, 3) == 1){
+ continue;
+ }
+
+ TImpact *Impact = NULL;
+ switch(SpellData->Impact){
+ case IMPACT_DAMAGE:{
+ int DamageType = SpellData->ImpactParam1;
+ int Damage = SpellData->ImpactParam2;
+ int Variation = SpellData->ImpactParam3;
+ Damage = ComputeDamage(this, 0, Damage, Variation);
+ Impact = new TDamageImpact(this, DamageType, Damage, true);
+ break;
+ }
+
+ case IMPACT_FIELD:{
+ int FieldType = SpellData->ImpactParam1;
+ Impact = new TFieldImpact(this, FieldType);
+ break;
+ }
+
+ case IMPACT_HEALING:{
+ int Amount = SpellData->ImpactParam1;
+ int Variation = SpellData->ImpactParam2;
+ Amount = ComputeDamage(this, 0, Amount, Variation);
+ Impact = new THealingImpact(this, Amount);
+ break;
+ }
+
+ case IMPACT_SPEED:{
+ int Percent = SpellData->ImpactParam1;
+ int Variation = SpellData->ImpactParam2;
+ int Duration = SpellData->ImpactParam3;
+ Percent = ComputeDamage(this, 0, Percent, Variation);
+ Impact = new TSpeedImpact(this, Percent, Duration);
+ break;
+ }
+
+ case IMPACT_DRUNKEN:{
+ int Strength = SpellData->ImpactParam1;
+ int Variation = SpellData->ImpactParam2;
+ int Duration = SpellData->ImpactParam3;
+ Strength = ComputeDamage(this, 0, Strength, Variation);
+ Impact = new TDrunkenImpact(this, Strength, Duration);
+ break;
+ }
+
+ case IMPACT_STRENGTH:{
+ int Skills = SpellData->ImpactParam1;
+ int Percent = SpellData->ImpactParam2;
+ int Variation = SpellData->ImpactParam3;
+ int Duration = SpellData->ImpactParam4;
+ Percent = ComputeDamage(this, 0, Percent, Variation);
+ Impact = new TStrengthImpact(this, Skills, Percent, Duration);
+ break;
+ }
+
+ case IMPACT_OUTFIT:{
+ TOutfit Outfit = {};
+ Outfit.OutfitID = SpellData->ImpactParam1;
+ Outfit.PackedData = (uint32)SpellData->ImpactParam2;
+ int Duration = SpellData->ImpactParam3;
+ Impact = new TOutfitImpact(this, Outfit, Duration);
+ break;
+ }
+
+ case IMPACT_SUMMON:{
+ int SummonRace = SpellData->ImpactParam1;
+ int MaxSummons = SpellData->ImpactParam2;
+ Impact = new TSummonImpact(this, SummonRace, MaxSummons);
+ break;
+ }
+ }
+
+ if(Impact != NULL){
+ switch(SpellData->Shape){
+ case SHAPE_ACTOR:{
+ int Effect = SpellData->ShapeParam1;
+ ActorShapeSpell(this, Impact, Effect);
+ break;
+ }
+
+ case SHAPE_VICTIM:{
+ if(Target != NULL){
+ this->Rotate(Target);
+
+ int Range = SpellData->ShapeParam1;
+ int Animation = SpellData->ShapeParam2;
+ int Effect = SpellData->ShapeParam3;
+ VictimShapeSpell(this, Target, Range,
+ Animation, Impact, Effect);
+ }
+ break;
+ }
+
+ case SHAPE_ORIGIN:{
+ int Radius = SpellData->ShapeParam1;
+ int Effect = SpellData->ShapeParam2;
+ OriginShapeSpell(this, Radius, Impact, Effect);
+ break;
+ }
+
+ case SHAPE_DESTINATION:{
+ if(Target != NULL){
+ this->Rotate(Target);
+
+ int Range = SpellData->ShapeParam1;
+ int Animation = SpellData->ShapeParam2;
+ int Radius = SpellData->ShapeParam3;
+ int Effect = SpellData->ShapeParam4;
+ DestinationShapeSpell(this, Target, Range,
+ Animation, Radius, Impact, Effect);
+ }
+ break;
+ }
+
+ case SHAPE_ANGLE:{
+ if(Target != NULL){
+ this->Rotate(Target);
+ }
+
+ int Angle = SpellData->ShapeParam1;
+ int Range = SpellData->ShapeParam2;
+ int Effect = SpellData->ShapeParam3;
+ AngleShapeSpell(this, Angle, Range, Impact, Effect);
+ break;
+ }
+ }
+
+ delete Impact;
+ }
+ }
+ }
+
+ // NOTE(fusion): WALKING. What was already bad got even worse.
+ TCreature *Target = GetCreature(this->Target);
+ if(this->Target != 0 && Target == NULL){
+ error("TMonster::IdleStimulus: Kreatur existiert nicht.\n");
+ this->Target = 0;
+ }
+
+ try{
+ if(Target != NULL){
+ if(this->IsFleeing()){
+ int DestX, DestY, DestZ;
+ if(SearchFlightField(this->ID, this->Target, &DestX, &DestY, &DestZ)){
+ this->ToDoGo(DestX, DestY, DestZ, true, INT_MAX);
+ this->ToDoStart();
+ return;
+ }
+ }else if(this->Target == this->Master){
+ // NOTE(fusion): Manhattan distance.
+ int DistanceX = std::abs(Target->posx - this->posx);
+ int DistanceY = std::abs(Target->posy - this->posy);
+ int Distance = DistanceX + DistanceY;
+ if(Distance > 1){
+ if(Distance == 2){
+ this->ToDoWait(1000);
+ }else{
+ if(Distance == 3){
+ this->ToDoWait(1000);
+ }
+ this->ToDoGo(Target->posx, Target->posy, Target->posz, false, 3);
+ }
+ this->ToDoStart();
+ return;
+ }
+ }else{
+ if(this->Skills[SKILL_FIST]->Get() > 0 && this->State != PANIC){
+ this->State = ATTACKING;
+ }
+
+ if(this->State == ATTACKING || this->State == PANIC){
+ this->Combat.SetAttackDest(this->Target, false);
+ this->Combat.SetChaseMode(CHASE_MODE_NONE);
+ }
+
+ // TODO(fusion): We're doing something similar to `TCombat::CanToDoAttack`
+ // with added random steps around the attacking position. This function is
+ // too convoluted and we should definitely split it into smaller ones. I
+ // don't have a problem with large functions but this it too much.
+
+ // NOTE(fusion): Max distance.
+ int Distance = std::max<int>(
+ std::abs(Target->posx - this->posx),
+ std::abs(Target->posy - this->posy));
+ if(!RaceData[this->Race].DistanceFighting
+ || !ThrowPossible(this->posx, this->posy, this->posz,
+ Target->posx, Target->posy, Target->posz, 0)){
+ this->Combat.SetChaseMode(CHASE_MODE_CLOSE);
+ if(Distance > 1){
+ // TODO(fusion): Not sure what is going on here. I think
+ // it's because `ToDoAttack` with `CHASE_MODE_CLOSE` will
+ // already take care of walking to the target?
+ if(this->State != ATTACKING && this->State != PANIC){
+ this->ToDoGo(Target->posx, Target->posy, Target->posz, false, 3);
+ }
+ }else{
+ int DestX = this->posx;
+ int DestY = this->posy;
+ int DestZ = this->posz;
+ switch(rand() % 5){
+ case 0: DestX -= 1; break;
+ case 1: DestX += 1; break;
+ case 2: DestY -= 1; break;
+ case 3: DestY += 1; break;
+ case 4: break; // no step
+ }
+
+ int DestDistance = std::max<int>(
+ std::abs(Target->posx - DestX),
+ std::abs(Target->posy - DestY));
+ if(DestDistance == 1 && this->MovePossible(DestX, DestY, DestZ, true, false)){
+ this->ToDoGo(DestX, DestY, DestZ, true, INT_MAX);
+ }
+
+ if(this->State == PANIC){
+ this->State = ATTACKING;
+ }
+ }
+ }else{
+ if(Distance < 4){
+ int DestX, DestY, DestZ;
+ if(SearchFlightField(this->ID, this->Target, &DestX, &DestY, &DestZ)){
+ this->ToDoGo(DestX, DestY, DestZ, true, INT_MAX);
+ }else{
+ this->ToDoWait(1000);
+ }
+ }else if(Distance > 4){
+ this->ToDoGo(Target->posx, Target->posy, Target->posz, false, Distance - 4);
+ }else{
+ int DestX = this->posx;
+ int DestY = this->posy;
+ int DestZ = this->posz;
+ switch(rand() % 5){
+ case 0: DestX -= 1; break;
+ case 1: DestX += 1; break;
+ case 2: DestY -= 1; break;
+ case 3: DestY += 1; break;
+ case 4: break; // no step
+ }
+
+ int DestDistance = std::max<int>(
+ std::abs(Target->posx - DestX),
+ std::abs(Target->posy - DestY));
+ if(DestDistance == 4 && this->MovePossible(DestX, DestY, DestZ, true, false)){
+ this->ToDoGo(DestX, DestY, DestZ, true, INT_MAX);
+ }
+
+ this->ToDoWait(1000);
+ }
+ }
+
+ if(this->State == ATTACKING || this->State == PANIC){
+ this->Rotate(Target);
+ if(this->Master != this->Target){
+ this->ToDoAttack();
+ }else{
+ error("TMonster::IdleStimulus: %s greift Master %u an (St=%d, T: %u).\n",
+ this->Name, this->Master, this->State, this->Target);
+ }
+ }else{
+ this->ToDoWait(1000);
+ }
+
+ this->ToDoStart();
+ return;
+ }
+ }
+ }catch(RESULT r){
+ this->Target = 0;
+ this->ToDoClear();
+ if(r == EXHAUSTED){
+ this->ToDoWait(1000);
+ this->ToDoStart();
+ return;
+ }
+ }
+
+ // TODO(fusion): This part is similar to `TNPC::IdleStimulus`. Perhaps there
+ // is a common function in `TNonplayer` that does this?
+ bool FoundDest = false;
+ int DestX, DestY, DestZ;
+ for(int i = 0; i < 10; i += 1){
+ DestX = this->posx;
+ DestY = this->posy;
+ DestZ = this->posz;
+ switch(rand() % 4){
+ case 0: DestX -= 1; break;
+ case 1: DestX += 1; break;
+ case 2: DestY -= 1; break;
+ case 3: DestY += 1; break;
+ }
+
+ if(this->MovePossible(DestX, DestY, DestZ, true, false)){
+ FoundDest = true;
+ break;
+ }
+ }
+
+ if(FoundDest){
+ try{
+ this->ToDoGo(DestX, DestY, DestZ, true, INT_MAX);
+ this->ToDoWait(1000);
+ this->ToDoStart();
+ }catch(RESULT r){
+ error("TMonster::IdleStimulus: Exception %d.\n", r);
+ }
+ }else{
+ this->ToDoWait(1000);
+ this->ToDoStart();
+ }
+}
+
+void TMonster::CreatureMoveStimulus(uint32 CreatureID, int Type){
+ if(CreatureID == this->ID){
+ if(this->State == SLEEPING && Type != OBJECT_DELETED){
+ this->State = IDLE;
+ this->ToDoYield();
+ }
+ return;
+ }
+
+ if(this->State == SLEEPING && Type != OBJECT_DELETED){
+ TCreature *Creature = GetCreature(CreatureID);
+ if(Creature == NULL){
+ error("TMonster::CreatureMoveStimulus: Kreatur %u existiert nicht.\n", CreatureID);
+ return;
+ }
+
+ if(Creature->Type == NPC){
+ return;
+ }
+
+ if(Creature->Type == MONSTER && !((TMonster*)Creature)->IsPlayerControlled()){
+ return;
+ }
+
+ this->State = IDLE;
+ this->ToDoYield();
+ }
+
+ TCreature::CreatureMoveStimulus(CreatureID, Type);
+}
+
+bool TMonster::CanKickBoxes(void){
+ bool Result = RaceData[this->Race].KickBoxes;
+ if(!Result && this->Master != 0){
+ TCreature *Master = GetCreature(this->Master);
+ Result = (Master != NULL && Master->Type == MONSTER
+ && ((TMonster*)Master)->CanKickBoxes());
+ }
+ return Result;
+}
+
+void TMonster::KickBoxes(Object Obj){
+ if(!Obj.exists()){
+ error("TMonster::KickBoxes: Übergebenes Objekt existiert nicht.\n");
+ return;
+ }
+
+ int ObjX, ObjY, ObjZ;
+ GetObjectCoordinates(Obj, &ObjX, &ObjY, &ObjZ);
+
+ try{
+ bool ObjMoved = false;
+ int OffsetX[4] = { 0, 0, -1, 1};
+ int OffsetY[4] = {-1, 1, 0, 0};
+ for(int i = 0; i < 4; i += 1){
+ int DestX = ObjX + OffsetX[i];
+ int DestY = ObjY + OffsetY[i];
+ int DestZ = ObjZ;
+ if(DestX == this->posx && DestY == this->posy){
+ continue;
+ }
+
+ if(CoordinateFlag(DestX, DestY, DestZ, BANK)
+ && !CoordinateFlag(DestX, DestY, DestZ, UNPASS)){
+ Object Dest = GetMapContainer(DestX, DestY, DestZ);
+ ::Move(this->ID, Obj, Dest, -1, false, NONE);
+ ObjMoved = true;
+ break;
+ }
+ }
+
+ if(!ObjMoved){
+ GraphicalEffect(Obj, EFFECT_BLOCK_HIT);
+ Delete(Obj, -1);
+ }
+ }catch(RESULT r){
+ error("TMonster::KickBoxes: Exception %d, Objekt %d.\n",
+ r, Obj.getObjectType().TypeID);
+ error("# eigene Position: [%d,%d,%d] - Objektposition: [%d,%d,%d]\n",
+ this->posx, this->posy, this->posz, ObjX, ObjY, ObjZ);
+ }
+}
+
+bool TMonster::KickCreature(TCreature *Creature){
+ if(Creature == NULL){
+ error("TMonster::KickCreature: Übergebene Kreatur existiert nicht.\n");
+ return false;
+ }
+
+ if(Creature->Type != MONSTER){
+ error("TMonster::KickCreature: Zu verschiebende Kreatur ist kein Monster.\n");
+ return false;
+ }
+
+ print(3, "%s verschiebt %s.\n", this->Name, Creature->Name);
+
+ // TODO(fusion): Declare these here so they can be used within the catch
+ // block to print out what's on the last position we tried to move the
+ // creature to.
+ int DestX = Creature->posx;
+ int DestY = Creature->posy;
+ int DestZ = Creature->posz;
+ bool CreatureMoved = false;
+ try{
+ int OffsetX[4] = { 0, 0, -1, 1};
+ int OffsetY[4] = {-1, 1, 0, 0};
+ for(int i = 0; i < 4; i += 1){
+ DestX = Creature->posx + OffsetX[i];
+ DestY = Creature->posy + OffsetY[i];
+ if(DestX == this->posx && DestY == this->posy){
+ continue;
+ }
+
+ if(Creature->MovePossible(DestX, DestY, DestZ, true, false)
+ && !CoordinateFlag(DestX, DestY, DestZ, AVOID)){
+ Object Dest = GetMapContainer(DestX, DestY, DestZ);
+ ::Move(this->ID, Creature->CrObject, Dest, -1, false, NONE);
+ CreatureMoved = true;
+ break;
+ }
+ }
+
+ if(!CreatureMoved){
+ print(3, "Kein Platz zum Verschieben => Töten.\n");
+ GraphicalEffect(Creature->CrObject, EFFECT_BLOCK_HIT);
+ Creature->Combat.AddDamageToCombatList(this->ID,
+ Creature->Skills[SKILL_HITPOINTS]->Get());
+ Creature->Kill();
+ }
+ }catch(RESULT r){
+ error("TMonster::KickCreature: Exception %d, Kreatur %s.\n",
+ r, Creature->Name);
+ error("# eigene Position: [%d,%d,%d] - Hindernisposition: [%d,%d,%d]\n",
+ this->posx, this->posy, this->posz,
+ Creature->posx, Creature->posy, Creature->posz);
+
+ error("# Objekte auf Zielfeld [%d,%d,%d]:\n", DestX, DestY, DestZ);
+ Object Obj = GetFirstObject(DestX, DestY, DestZ);
+ while(Obj != NONE){
+ error("# %d\n", Obj.getObjectType().TypeID);
+ Obj = Obj.getNextObject();
+ }
+ }
+
+ return CreatureMoved;
+}
+
+void TMonster::Convince(TCreature *NewMaster){
+ if(NewMaster == NULL){
+ error("TMonster::Convince: NewMaster ist NULL.");
+ return;
+ }
+
+ if(this->Home != 0){
+ NotifyMonsterhomeOfDeath(this->Home);
+ }
+
+ this->Home = 0;
+ if(this->Master != 0){
+ TCreature *OldMaster = GetCreature(this->Master);
+ if(OldMaster != NULL){
+ OldMaster->SummonedCreatures -= 1;
+ }
+ }
+
+ this->Master = NewMaster->ID;
+ NewMaster->SummonedCreatures += 1;
+
+ this->ToDoClear();
+ this->ToDoWait(100);
+ this->ToDoStart();
+}
+
+void TMonster::SetTarget(TCreature *NewTarget){
+ if(NewTarget == NULL){
+ error("TMonster::SetTarget: NewTarget ist NULL.\n");
+ return;
+ }
+
+ if(this->Master == 0){
+ this->Target = NewTarget->ID;
+ this->Rotate(NewTarget);
+ this->ToDoYield();
+ }
+}
+
+bool TMonster::IsPlayerControlled(void){
+ bool Result = false;
+ if(this->Master != 0){
+ TCreature *Master = GetCreature(this->Master);
+ Result = (Master && Master->Type == PLAYER);
+ }
+ return Result;
+}
+
+bool TMonster::IsFleeing(void){
+ bool Result = false;
+ if(this->Master == 0){
+ int HitPoints = this->Skills[SKILL_HITPOINTS]->Get();
+ int FleeThreshold = RaceData[this->Race].FleeThreshold;
+ Result = HitPoints <= FleeThreshold;
+ }
+ return Result;
+}
+
+TCreature *CreateMonster(int Race, int x, int y, int z, int Home, uint32 MasterID, bool ShowEffect){
+ if(!IsRaceValid(Race)){
+ error("CreateMonster: Ungültige Rassennummer %d.\n", Race);
+ return NULL;
+ }
+
+ if(RaceData[Race].Name[0] == 0){
+ error("CreateMonster: Daten für Rasse %d nicht definiert.\n", Race);
+ return NULL;
+ }
+
+ SearchFreeField(&x, &y, &z, 2, 0, false);
+ TMonster *Monster = new TMonster(Race, x, y, z, Home, MasterID);
+ if(ShowEffect){
+ GraphicalEffect(x, y, z, EFFECT_ENERGY);
+ }
+ return Monster;
+}
+
+void ConvinceMonster(TCreature *Master, TCreature *Slave){
+ if(Master == NULL){
+ error("ConvinceMonster: Master existiert nicht.\n");
+ return;
+ }
+
+ if(Slave == NULL){
+ error("ConvinceMonster: Slave existiert nicht.\n");
+ return;
+ }
+
+ if(Slave->Type != MONSTER){
+ error("ConvinceMonster: Slave ist kein Monster.\n");
+ return;
+ }
+
+ ((TMonster*)Slave)->Convince(Master);
+}
+
+void ChallengeMonster(TCreature *Challenger, TCreature *Monster){
+ if(Challenger == NULL){
+ error("ChallengeMonster: Herausforderer existiert nicht.\n");
+ return;
+ }
+
+ if(Monster == NULL){
+ error("ChallengeMonster: Monster existiert nicht.\n");
+ return;
+ }
+
+ if(Monster->Type != MONSTER){
+ error("ChallengeMonster: Monster ist kein Monster.\n");
+ return;
+ }
+
+ ((TMonster*)Monster)->Target = Challenger->ID;
+ Monster->Rotate(Challenger);
+ Monster->ToDoYield();
+}
+
+// Initialization
+// =============================================================================
+void InitNPCs(void){
+ DIR *NpcDir = opendir(NPCPATH);
+ if(NpcDir == NULL){
+ error("InitNPCs: Unterverzeichnis %s nicht gefunden\n", NPCPATH);
+ throw "Cannot init NPCs";
+ }
+
+ char FileName[4096];
+ while(dirent *DirEntry = readdir(NpcDir)){
+ if(DirEntry->d_type != DT_REG){
+ continue;
+ }
+
+ const char *FileExt = findLast(DirEntry->d_name, '.');
+ if(FileExt == NULL || strcmp(FileExt, ".npc") != 0){
+ continue;
+ }
+
+ snprintf(FileName, sizeof(FileName), "%s/%s", NPCPATH, DirEntry->d_name);
+ new TNPC(FileName);
+ }
+
+ closedir(NpcDir);
+}
+
+void InitNonplayer(void){
+ print(1, "Initializing Nonplayers ...\n");
+ FirstFreeNonplayer = 0;
+ InitNPCs();
+ LoadMonsterhomes();
+}
+
+void ExitNonplayer(void){
+ while(FirstFreeNonplayer > 0){
+ // NOTE(fusion): Deleting a non player or any other creature will
+ // automatically remove it from its related creature lists.
+ TNonplayer *Nonplayer = *NonplayerList.at(0);
+ delete Nonplayer;
+ }
+
+ // TODO(fusion): For any reason `BehaviourNodeTable` was originally a
+ // pointer and it was deleted here. I doesn't really make a difference
+ // because we'd usually exit after calling this function but if we wanted
+ // to cleanup all memory, we'd need to add some `freeAll` method to the
+ // `store` container.
+}
diff --git a/src/magic.cc b/src/magic.cc
index 4ed1826..19a9dc8 100644
--- a/src/magic.cc
+++ b/src/magic.cc
@@ -968,8 +968,7 @@ void KillAllMonsters(TCreature *Actor, int Effect, int Radius){
error("KillAllMonsters: Ungültige Kreatur.\n");
}else if(Actor != Victim && Victim->Type == MONSTER){
print(3, "Töte %s...\n", Victim->Name);
- Victim->Skills[SKILL_HITPOINTS]->Set(0);
- Victim->Death();
+ Victim->Kill();
}
}
Obj = Obj.getNextObject();
@@ -2395,7 +2394,7 @@ void CancelInvisibility(TCreature *Actor, Object Target, int ManaPoints, int Sou
TCreature *Victim = GetCreature(Obj);
if(Victim == NULL){
error("CancelInvisibility: Ungültige Kreatur.\n");
- }else if(Victim->Outfit.OutfitID == 0 && Victim->Outfit.ObjectType == 0
+ }else if(Victim->IsInvisible()
&& (WorldType != NON_PVP || !Victim->IsPeaceful())){
Victim->SetTimer(SKILL_ILLUSION, 0, 0, 0, -1);
if(Victim->Skills[SKILL_ILLUSION]->Get() == 0){
diff --git a/src/magic.hh b/src/magic.hh
index adc648f..ff14c48 100644
--- a/src/magic.hh
+++ b/src/magic.hh
@@ -19,6 +19,13 @@ struct TImpact{
virtual void handleCreature(TCreature *Victim); // VTABLE[1]
virtual bool isAggressive(void); // VTABLE[2]
+ // NOTE(fusion): I don't think the original version had a destructor declared
+ // here but the compiler complains when calling delete (which seems to only be
+ // used in `TMonster::IdleStimulus`).
+ virtual ~TImpact(void){
+ // no-op
+ }
+
// DATA
// =========================================================================
//void *VTABLE; // IMPLICIT
diff --git a/src/operate.cc b/src/operate.cc
index 6fb7f20..e742806 100644
--- a/src/operate.cc
+++ b/src/operate.cc
@@ -2787,17 +2787,8 @@ bool SectorRefreshable(int SectorX, int SectorY, int SectorZ){
continue;
}
- // TODO(fusion): Maybe an inlined function to check whether a player
- // can see some floor. All floors above ground, when above ground. Or
- // two floors up and down, when underground.
- if(Player->posz <= 7){
- if(SectorZ <= 7){
- return false;
- }
- }else{
- if(std::abs(Player->posz - SectorZ) <= 2){
- return false;
- }
+ if(Player->CanSeeFloor(SectorZ)){
+ return false;
}
}
diff --git a/src/stubs.hh b/src/stubs.hh
index d8b9839..4b9e3cc 100644
--- a/src/stubs.hh
+++ b/src/stubs.hh
@@ -24,9 +24,6 @@ extern void ChangeNameDoor(Object Door, TPlayer *Player, const char *Text);
extern void CharacterDeathOrder(TCreature *Creature, int OldLevel,
uint32 Offender, const char *Remark, bool Unjustified);
extern void CleanHouseField(int x, int y, int z);
-extern void ConvinceMonster(TCreature *Master, TCreature *Slave);
-extern void ChallengeMonster(TCreature *Challenger, TCreature *Monster);
-extern TCreature *CreateMonster(int Race, int x, int y, int z, int Home, uint32 Master, bool ShowEffect);
extern void DecrementIsOnlineOrder(uint32 CharacterID);
extern void GetExitPosition(uint16 HouseID, int *x, int *y, int *z);
extern int GetOrderBufferSpace(void);