aboutsummaryrefslogtreecommitdiff
path: root/src/crmain.cc
diff options
context:
space:
mode:
authorfusion32 <marcopuzziello@gmail.com>2025-06-05 12:00:34 -0300
committerfusion32 <marcopuzziello@gmail.com>2025-06-05 12:00:34 -0300
commitebfa80ab573b1a0ef9aec65a45fd0ae751196207 (patch)
treee86ce77ae1f497b216861d763d92de57ec9e0490 /src/crmain.cc
parent3c8bcba6b540f5cb84572994d9ec2426dd348a93 (diff)
downloadgame-ebfa80ab573b1a0ef9aec65a45fd0ae751196207.tar.gz
game-ebfa80ab573b1a0ef9aec65a45fd0ae751196207.zip
merge creature headers
Diffstat (limited to 'src/crmain.cc')
-rw-r--r--src/crmain.cc589
1 files changed, 589 insertions, 0 deletions
diff --git a/src/crmain.cc b/src/crmain.cc
new file mode 100644
index 0000000..31d93fb
--- /dev/null
+++ b/src/crmain.cc
@@ -0,0 +1,589 @@
+#include "cr.hh"
+#include "enums.hh"
+
+#include "stubs.hh"
+
+static uint32 NextCreatureID;
+static matrix<uint32> *FirstChainCreature;
+static TCreature *HashList[1000];
+
+static int FirstFreeCreature;
+static vector<TCreature*> CreatureList(0, 10000, 1000);
+
+static priority_queue<uint32, uint32> ToDoQueue(5000, 1000);
+//static priority_queue<uint32, TAttackWave*> AttackWaveQueue(100, 100);
+
+TRaceData RaceData[MAX_RACES];
+int KilledCreatures[MAX_RACES];
+int KilledPlayers[MAX_RACES];
+
+// TCreature
+// =============================================================================
+TCreature::TCreature(void) :
+ TSkillBase(),
+ Combat(),
+ ToDoList(0, 20, 10)
+{
+ this->Combat.Master = this;
+ this->ID = 0;
+ this->NextHashEntry = NULL;
+ this->NextChainCreature = 0;
+ this->Murderer[0] = 0;
+ this->startx = 0;
+ this->starty = 0;
+ this->startz = 0;
+ this->posx = 0;
+ this->posy = 0;
+ this->posz = 0;
+ this->Direction = 0;
+ this->Radius = INT_MAX;
+ this->IsDead = false;
+ this->LoseInventory = 2;
+ this->LoggingOut = false;
+ this->LogoutAllowed = false;
+ this->EarliestLogoutRound = 0;
+ this->EarliestProtectionZoneRound = 0;
+ this->EarliestYellRound = 0;
+ this->EarliestTradeChannelRound = 0;
+ this->EarliestSpellTime = 0;
+ this->EarliestMultiuseTime = 0;
+ this->EarliestWalkTime = 0;
+ this->LifeEndRound = 0;
+ this->FirstKnowingConnection = NULL;
+ this->SummonedCreatures = 0;
+ this->FireDamageOrigin = 0;
+ this->PoisonDamageOrigin = 0;
+ this->EnergyDamageOrigin = 0;
+ this->CrObject = NONE;
+ this->ActToDo = 0;
+ this->NrToDo = 0;
+ this->NextWakeup = 0;
+ this->Stop = false;
+ this->LockToDo = false;
+ this->Connection = NULL;
+
+ for(int i = 0; i < NARRAY(this->Skills); i += 1){
+ this->NewSkill((uint16)i, this);
+ }
+}
+
+void TCreature::Attack(void){
+ this->Combat.Attack();
+}
+
+int TCreature::Damage(TCreature *Attacker, int Damage, int DamageType){
+ if(this->IsDead || this->Type == NPC){
+ return 0;
+ }
+
+ if(this->Type == PLAYER){
+ if(this->Connection != NULL && Attacker != NULL){
+ SendMarkCreature(this->Connection, Attacker->ID, COLOR_BLACK);
+ }
+
+ if(Attacker != NULL && Attacker->Type == PLAYER
+ && DamageType != DAMAGE_POISON_PERIODIC
+ && DamageType != DAMAGE_FIRE_PERIODIC
+ && DamageType != DAMAGE_ENERGY_PERIODIC){
+ Damage = (Damage + 1) / 2;
+ }
+ }
+
+ // NOTE(fusion): `Responsible` is either the attacker or its master. It is
+ // always valid if `Attacker` is not NULL.
+ TCreature *Responsible = Attacker;
+ if(Attacker != NULL){
+ uint32 MasterID = Attacker->GetMaster();
+ if(MasterID != 0){
+ Responsible = GetCreature(MasterID);
+ }
+
+ Attacker->BlockLogout(60, this->Type == PLAYER);
+ if(Responsible != Attacker){
+ Responsible->BlockLogout(60, this->Type == PLAYER);
+ }
+
+ if(this->Type == PLAYER && Responsible->Type == PLAYER){
+ ((TPlayer*)Responsible)->RecordAttack(this->ID);
+ }
+ }
+
+ if(this->Type == PLAYER){
+ if(CheckRight(this->ID, INVULNERABLE)){
+ Damage = 0;
+ }
+
+ // NOTE(fusion): We're iterating over the victim's inventory to apply
+ // damage reductions, while the damage is greater than zero.
+ for(int Position = 1;
+ Position <= 10 && Damage > 0;
+ Position += 1){
+ Object Obj = GetBodyObject(this->ID, Position);
+ if(Obj == NONE){
+ continue;
+ }
+
+ ObjectType ObjType = Obj.getObjectType();
+ if(ObjType.getFlag(PROTECTION) && ObjType.getFlag(CLOTHES)
+ && (int)ObjType.getAttribute(BODYPOSITION) == Position
+ && (ObjType.getAttribute(PROTECTIONDAMAGETYPES) & DamageType) != 0){
+ int DamageReduction = ObjType.getAttribute(DAMAGEREDUCTION);
+ Damage = (Damage * (100 - DamageReduction)) / 100;
+ if(ObjType.getFlag(WEAROUT)){
+ // TODO(fusion): Ugh... The try..catch block might be used only
+ // when changing the object's type.
+ try{
+ uint32 RemainingUses = Obj.getAttribute(REMAININGUSES);
+ if(RemainingUses > 1){
+ Change(Obj, REMAININGUSES, RemainingUses - 1);
+ }else{
+ ObjectType WearOutType = ObjType.getAttribute(WEAROUTTARGET);
+ Change(Obj, WearOutType, 0);
+ }
+ }catch(RESULT r){
+ error("TCreature::Damage: Exception %d beim Abnutzen von Objekt %d.\n",
+ r, ObjType.TypeID);
+ }
+ }
+ }else if(ObjType.getFlag(PROTECTION) && !ObjType.getFlag(CLOTHES)){
+ error("TCreature::Damage: Objekt %d hat PROTECTION, aber nicht CLOTHES.\n",
+ ObjType.TypeID);
+ }
+ }
+ }
+
+ if(Damage <= 0){
+ GraphicalEffect(this->CrObject, EFFECT_POFF);
+ return 0;
+ }
+
+ if(DamageType == DAMAGE_POISON_PERIODIC){
+ if(RaceData[this->Race].NoPoison){
+ return 0;
+ }
+
+ if(Damage > this->Skills[SKILL_POISON]->TimerValue()){
+ this->PoisonDamageOrigin = Attacker->ID;
+ this->SetTimer(SKILL_POISON, Damage, 3, 3, -1);
+ }
+
+ this->DamageStimulus(Attacker->ID, Damage, DamageType);
+ return Damage;
+ }else if(DamageType == DAMAGE_FIRE_PERIODIC){
+ if(RaceData[this->Race].NoBurning){
+ return 0;
+ }
+
+ this->FireDamageOrigin = Attacker->ID;
+ this->SetTimer(SKILL_BURNING, Damage / 10, 8, 8, -1);
+ this->DamageStimulus(Attacker->ID, Damage, DamageType);
+ return Damage;
+ }else if(DamageType == DAMAGE_ENERGY_PERIODIC){
+ if(RaceData[this->Race].NoEnergy){
+ return 0;
+ }
+
+ // TODO(fusion): Shouldn't we use `Damage / 25` here?
+ this->EnergyDamageOrigin = Attacker->ID;
+ this->SetTimer(SKILL_ENERGY, Damage / 20, 10, 10, -1);
+ this->DamageStimulus(Attacker->ID, Damage, DamageType);
+ return Damage;
+ }
+
+ if((DamageType == DAMAGE_PHYSICAL && RaceData[this->Race].NoHit)
+ || (DamageType == DAMAGE_POISON && RaceData[this->Race].NoPoison)
+ || (DamageType == DAMAGE_FIRE && RaceData[this->Race].NoBurning)
+ || (DamageType == DAMAGE_ENERGY && RaceData[this->Race].NoEnergy)
+ || (DamageType == DAMAGE_LIFEDRAIN && RaceData[this->Race].NoLifeDrain)){
+ GraphicalEffect(this->CrObject, EFFECT_BLOCK_HIT);
+ return 0;
+ }
+
+ if(DamageType == DAMAGE_PHYSICAL){
+ Damage -= this->Combat.GetArmorStrength();
+ if(Damage <= 0){
+ GraphicalEffect(this->CrObject, EFFECT_BLOCK_HIT);
+ return 0;
+ }
+ }
+
+ this->DamageStimulus(Attacker->ID, Damage, DamageType);
+
+ // NOTE(fusion): Remove non-player illusion. Might as well be an inlined
+ // function.
+ if(this->Type != PLAYER
+ && this->Outfit.OutfitID == 0
+ && this->Outfit.ObjectType == 0){
+ this->SetTimer(SKILL_ILLUSION, 0, 0, 0, -1);
+ this->Outfit = this->OrgOutfit;
+ AnnounceChangedCreature(this->ID, 3); // CREATURE_OUTFIT_CHANGED ?
+ NotifyAllCreatures(this->CrObject, 2, NONE); // CREATURE_APPEAR ?
+ }
+
+ if(DamageType == DAMAGE_MANADRAIN){
+ int ManaPoints = this->Skills[SKILL_MANA]->Get();
+ if(Damage > ManaPoints){
+ Damage = ManaPoints;
+ }
+
+ if(Damage > 0){
+ this->Skills[SKILL_MANA]->Change(-Damage);
+ if(this->Type == PLAYER && this->Connection != NULL){
+ SendMessage(this->Connection, TALK_STATUS_MESSAGE,
+ "You lose %d mana.", Damage);
+ }
+ GraphicalEffect(this->CrObject, EFFECT_MAGIC_RED);
+ TextualEffect(this->CrObject, COLOR_BLUE, "%d", Damage);
+ }
+
+ return Damage;
+ }
+
+ if(this->Skills[SKILL_MANASHIELD]->TimerValue() > 0
+ || this->Skills[SKILL_MANASHIELD]->Get() > 0){
+ // NOTE(fusion): We only send these if the attack was fully absorbed,
+ // else it'd be overwritten by whatever effect and messages we send
+ // next, when the victim's hitpoints are actually touched.
+ int ManaPoints = this->Skills[SKILL_MANA]->Get();
+ if(Damage <= ManaPoints){
+ this->Skills[SKILL_MANA]->Change(-Damage);
+ GraphicalEffect(this->CrObject, EFFECT_MANA_HIT);
+ TextualEffect(this->CrObject, COLOR_BLUE, "%d", Damage);
+ if(this->Type == PLAYER && this->Connection != NULL){
+ if(Attacker != NULL){
+ SendMessage(this->Connection, TALK_STATUS_MESSAGE,
+ "You lose %d mana blocking an attack by %s.",
+ Damage, Attacker->Name);
+ }else{
+ SendMessage(this->Connection, TALK_STATUS_MESSAGE,
+ "You lose %d mana.", Damage);
+ }
+ SendPlayerData(this->Connection);
+ }
+ return Damage;
+ }
+
+ this->Skills[SKILL_MANA]->Set(0);
+ Damage -= ManaPoints;
+ }
+
+ int HitPoints = this->Skills[SKILL_HITPOINTS]->Get();
+ if(Damage > HitPoints){
+ Damage = HitPoints;
+ }
+
+ this->Skills[SKILL_HITPOINTS]->Change(-Damage);
+ if(Responsible != NULL){
+ ASSERT(Attacker != NULL);
+ if(Responsible == Attacker){
+ this->Combat.AddDamageToCombatList(Attacker->ID, Damage);
+ }else{
+ this->Combat.AddDamageToCombatList(Attacker->ID, Damage / 2);
+ this->Combat.AddDamageToCombatList(Responsible->ID, Damage / 2);
+ }
+ }
+
+ int HitEffect = EFFECT_NONE;
+ int TextColor = COLOR_BLACK;
+ int SplashLiquid = LIQUID_NONE;
+ if(DamageType == DAMAGE_PHYSICAL){
+ switch(RaceData[this->Race].Blood){
+ case BT_BLOOD:{
+ HitEffect = EFFECT_BLOOD_HIT;
+ TextColor = COLOR_RED;
+ SplashLiquid = LIQUID_BLOOD;
+ break;
+ }
+ case BT_SLIME:{
+ HitEffect = EFFECT_POISON_HIT;
+ TextColor = COLOR_LIGHTGREEN;
+ SplashLiquid = LIQUID_SLIME;
+ break;
+ }
+ case BT_BONES:{
+ HitEffect = EFFECT_BONE_HIT;
+ TextColor = COLOR_LIGHTGRAY;
+ break;
+ }
+ case BT_FIRE:{
+ HitEffect = EFFECT_FIRE_HIT;
+ TextColor = COLOR_ORANGE;
+ break;
+ }
+ case BT_ENERGY:{
+ HitEffect = EFFECT_ENERGY_HIT;
+ TextColor = COLOR_LIGHTBLUE;
+ break;
+ }
+ default:{
+ error("TCreature::Damage: Ungültiger Bluttyp %d für Rasse %d.\n",
+ RaceData[this->Race].Blood, this->Race);
+ break;
+ }
+ }
+ }else if(Damage == DAMAGE_POISON){
+ HitEffect = EFFECT_POISON;
+ TextColor = COLOR_LIGHTGREEN;
+ }else if(DamageType == DAMAGE_FIRE){
+ HitEffect = EFFECT_FIRE_HIT;
+ TextColor = COLOR_ORANGE;
+ }else if(DamageType == DAMAGE_ENERGY){
+ HitEffect = EFFECT_ENERGY_HIT;
+ TextColor = COLOR_LIGHTBLUE;
+ }else if(DamageType == DAMAGE_LIFEDRAIN){
+ HitEffect = EFFECT_MAGIC_RED;
+ TextColor = COLOR_RED;
+ }else{
+ // TODO(fusion): The original decompiled function would return here but
+ // I don't think it's a good idea because it would skip death handling.
+ error("TCreature::Damage: Ungültiger Schadenstyp %d.\n", DamageType);
+ //return Damage;
+ }
+
+ if(HitEffect != EFFECT_NONE){
+ GraphicalEffect(this->CrObject, HitEffect);
+ TextualEffect(this->CrObject, TextColor, "%d", Damage);
+ if(SplashLiquid != LIQUID_NONE){
+ CreatePool(GetMapContainer(this->CrObject),
+ GetSpecialObject(BLOOD_SPLASH),
+ SplashLiquid);
+ }
+ }
+
+ if(this->Type == PLAYER && this->Connection != NULL){
+ if(Attacker != NULL){
+ SendMessage(this->Connection, TALK_STATUS_MESSAGE,
+ "You lose %d hitpoint%s due to an attack by %s.",
+ Damage, (Damage == 1 ? "" : "s"), Attacker->Name);
+ }else{
+ SendMessage(this->Connection, TALK_STATUS_MESSAGE,
+ "You lose %d hitpoint%s.",
+ Damage, (Damage == 1 ? "" : "s"));
+ }
+ SendPlayerData(this->Connection);
+ }
+
+ if(Damage == HitPoints){
+ if(this->Type == PLAYER){
+ for(int Position = 1; Position <= 10; Position += 1){
+ Object Obj = GetBodyObject(this->ID, Position);
+ if(Obj == NONE){
+ continue;
+ }
+
+ ObjectType ObjType = Obj.getObjectType();
+ if(ObjType.getFlag(CLOTHES) && (int)ObjType.getAttribute(BODYPOSITION) == Position){
+ ObjectType AmuletOfLossType = GetNewObjectType(77, 12);
+ if(ObjType == AmuletOfLossType){
+ Log("game", "%s stirbt mit Amulett of Loss.\n", this->Name);
+ this->LoseInventory = 0;
+ Delete(Obj, -1);
+ // TODO(fusion): Shouldn't we break here? We could also
+ // just check if there is an amulet of loss in the necklace
+ // container instead of iterating over all of them.
+ }
+ }
+ }
+ }
+
+ int OldLevel = this->Skills[SKILL_LEVEL]->Get();
+ this->Death();
+ if(Attacker != NULL && this->Type == PLAYER){
+ Attacker->BlockLogout(900, true);
+ if(Responsible != Attacker){
+ Responsible->BlockLogout(900, true);
+ }
+ }
+
+ uint32 MurdererID = 0;
+ char Remark[30] = {};
+ if(Attacker == NULL){
+ AddKillStatistics(0, this->Race);
+ if(DamageType == DAMAGE_PHYSICAL){
+ strcpy(Remark, "a hit");
+ }else if(DamageType == DAMAGE_POISON){
+ strcpy(Remark, "poison");
+ }else if(DamageType == DAMAGE_FIRE){
+ strcpy(Remark, "fire");
+ }else if(DamageType == DAMAGE_ENERGY){
+ strcpy(Remark, "energy");
+ }else{
+ // NOTE(fusion): We probably don't expect any other damage type
+ // as the cause of death when there is no attacker.
+ error("TCreature::Damage: Ungültiger Schadenstyp %d als Todesursache.\n", DamageType);
+ }
+ }else{
+ AddKillStatistics(Attacker->Race, this->Race);
+ strcpy(this->Murderer, Attacker->Name);
+
+ if(Responsible->Type == PLAYER){
+ MurdererID = Responsible->ID;
+ }
+
+ if(Attacker->Type != PLAYER){
+ strcpy(Remark, Attacker->Name);
+ }
+ }
+
+ if(this->Type == PLAYER){
+ if(MurdererID != 0 && MurdererID != this->ID){
+ bool Justified = true;
+ if(IsCreaturePlayer(MurdererID)){
+ TPlayer *Murderer = GetPlayer(MurdererID);
+ if(Murderer != NULL){
+ Justified = Murderer->IsAttackJustified(this->ID);
+ Murderer->RecordMurder(this->ID);
+ }
+ }
+ CharacterDeathOrder(this, OldLevel, MurdererID, Remark, !Justified);
+ }
+
+ uint32 MostDangerousID = this->Combat.GetMostDangerousAttacker();
+ if(MostDangerousID != 0
+ && MostDangerousID != MurdererID
+ && MostDangerousID != this->ID
+ && IsCreaturePlayer(MostDangerousID)){
+ bool Justified = true;
+ TPlayer *MostDangerous = GetPlayer(MostDangerousID);
+ if(MostDangerous != NULL){
+ Justified = MostDangerous->IsAttackJustified(this->ID);
+ MostDangerous->RecordMurder(this->ID);
+ }
+
+ // TODO(fusion): The original function is confusing at this point
+ // but it seems correct that the remark is included only with the
+ // murderer.
+ CharacterDeathOrder(this, OldLevel, MostDangerousID, "", !Justified);
+ }
+ }
+ }
+
+ AnnounceChangedCreature(this->ID, 1); // CREATURE_HITPOINTS_CHANGED ?
+ return Damage;
+}
+
+// Creature
+// =============================================================================
+bool IsCreaturePlayer(uint32 CreatureID){
+ return CreatureID < 0x40000000;
+}
+
+//
+
+void DeleteChainCreature(TCreature *Creature){
+ if(Creature == NULL){
+ error("DeleteChainCreature: Übegebene Kreatur existiert nicht.\n");
+ return;
+ }
+
+ // NOTE(fusion): All creatures in each 16x16 region form a creature linked
+ // list, despite its current floor. Whether that is a good idea is a whole
+ // other matter.
+ int BlockX = Creature->posx / 16;
+ int BlockY = Creature->posy / 16;
+ uint32 *FirstID = FirstChainCreature->at(BlockX, BlockY);
+
+ if(*FirstID == Creature->ID){
+ *FirstID = Creature->NextChainCreature;
+ }else{
+ uint32 CurrentID = *FirstID;
+ while(true){
+ if(CurrentID == 0){
+ error("DeleteChainCreature: Kreatur nicht gefunden.\n");
+ return;
+ }
+
+ TCreature *Current = GetCreature(CurrentID);
+ if(Current == NULL){
+ error("DeleteChainCreature: Kreatur existiert nicht.\n");
+ return;
+ }
+
+ if(Current->NextChainCreature == Creature->ID){
+ Current->NextChainCreature = Creature->NextChainCreature;
+ break;
+ }
+
+ CurrentID = Current->NextChainCreature;
+ }
+ }
+}
+
+// Kill Statistics
+// =============================================================================
+void AddKillStatistics(int AttackerRace, int DefenderRace){
+ // NOTE(fusion): I think the race name can be "human" only for players,
+ // which means we're probably tracking how many creatures are killed by
+ // players with `KilledCreatures`, and how many players are killed by
+ // creatures with `KilledPlayers`.
+
+ if(strcmp(RaceData[AttackerRace].Name, "human") == 0){
+ KilledCreatures[DefenderRace] += 1;
+ }
+
+ // NOTE(fusion): This seems to track how many players are killed by
+ if(strcmp(RaceData[DefenderRace].Name, "human") == 0){
+ KilledPlayers[AttackerRace] += 1;
+ }
+}
+
+// Race
+// =============================================================================
+TRaceData::TRaceData(void) :
+ Skill(1, 5, 5),
+ Talk(1, 5, 5),
+ Item(1, 5, 5),
+ Spell(1, 5, 5)
+{
+ this->Name[0] = 0;
+ this->Article[0] = 0;
+ this->Outfit.OutfitID = 0;
+ this->Outfit.ObjectType = 0;
+ this->Blood = BT_BLOOD;
+ this->ExperiencePoints = 0;
+ this->FleeThreshold = 0;
+ this->Attack = 0;
+ this->Defend = 0;
+ this->Armor = 0;
+ this->Poison = 0;
+ this->SummonCost = 0;
+ this->LoseTarget = 0;
+ this->Strategy[0] = 100;
+ this->Strategy[1] = 0;
+ this->Strategy[2] = 0;
+ this->Strategy[3] = 0;
+ this->KickBoxes = false;
+ this->KickCreatures = false;
+ this->SeeInvisible = false;
+ this->Unpushable = false;
+ this->DistanceFighting = false;
+ this->NoSummon = false;
+ this->NoIllusion = false;
+ this->NoConvince = false;
+ this->NoBurning = false;
+ this->NoPoison = false;
+ this->NoEnergy = false;
+ this->NoHit = false;
+ this->NoLifeDrain = false;
+ this->NoParalyze = false;
+ this->Skills = 0;
+ this->Talks = 0;
+ this->Items = 0;
+ this->Spells = 0;
+}
+
+int GetRaceByName(const char *RaceName){
+ int Result = 0;
+ for(int Race = 1; Race < NARRAY(RaceData); Race += 1){
+ if(stricmp(RaceName, RaceData[Race].Name) == 0){
+ Result = Race;
+ break;
+ }
+ }
+ return Result;
+}
+
+// Raid
+// =============================================================================
+
+