aboutsummaryrefslogtreecommitdiff
path: root/src/crcombat.cc
diff options
context:
space:
mode:
authorfusion32 <marcopuzziello@gmail.com>2025-05-30 20:12:36 -0300
committerfusion32 <marcopuzziello@gmail.com>2025-05-30 20:12:36 -0300
commit08780dad536eb5d0544eb2ea70e5443dc237976f (patch)
tree673a57b538ca9016d5b386ff32c3bd67c2190c2f /src/crcombat.cc
parentdff5e62f6ebb1b06c234b9144458788893b6ca86 (diff)
downloadgame-08780dad536eb5d0544eb2ea70e5443dc237976f.tar.gz
game-08780dad536eb5d0544eb2ea70e5443dc237976f.zip
more work on `crcombat.cc`
Diffstat (limited to 'src/crcombat.cc')
-rw-r--r--src/crcombat.cc417
1 files changed, 413 insertions, 4 deletions
diff --git a/src/crcombat.cc b/src/crcombat.cc
index de6385a..256f056 100644
--- a/src/crcombat.cc
+++ b/src/crcombat.cc
@@ -1,6 +1,7 @@
#include "creature.hh"
#include "player.hh"
#include "config.hh"
+#include "magic.hh"
#include "stubs.hh"
@@ -10,8 +11,8 @@ TCombat::TCombat(void){
this->EarliestDefendTime = 0;
this->LastDefendTime = 0;
this->LatestAttackTime = 0;
- this->AttackMode = 2;
- this->ChaseMode = 0;
+ this->AttackMode = ATTACK_MODE_BALANCED;
+ this->ChaseMode = CHASE_MODE_NONE;
this->SecureMode = 1;
this->AttackDest = 0;
this->Following = false;
@@ -146,11 +147,184 @@ void TCombat::CheckCombatValues(void){
}
}
+int TCombat::GetDistance(void){
+ int Distance = 0;
+ if(this->Close != NONE || this->Fist){
+ Distance = 1;
+ }else if(this->Throw != NONE){
+ Distance = 2;
+ }else if(this->Missile != NONE || this->Wand != NONE){
+ Distance = 3;
+ }
+ return Distance;
+}
+
+void TCombat::SetAttackDest(uint32 TargetID, bool Follow){
+ if(this->AttackDest == TargetID && this->Following == Follow){
+ return;
+ }
+
+ TCreature *Master = this->Master;
+ if(TargetID == 0 || TargetID == Master->ID){
+ this->StopAttack(0);
+ return;
+ }
+
+ TCreature *Target = GetCreature(TargetID);
+ if(Target == NULL){
+ this->StopAttack(0);
+ return;
+ }
+
+ if(!Follow){
+ if(Master->Type == PLAYER && Target->Type == PLAYER){
+ if(this->SecureMode == 1 && WorldType == NORMAL
+ && !((TPlayer*)Master)->IsAttackJustified(TargetID)){
+ this->StopAttack(0);
+ throw SECUREMODE;
+ }
+ }
+
+ if(Master->Type == PLAYER && !CheckRight(Master->ID, ATTACK_EVERYWHERE)){
+ if(IsProtectionZone(Master->posx, Master->posy, Master->posz)
+ || IsProtectionZone(Target->posx, Target->posy, Target->posz)){
+ this->StopAttack(0);
+ throw PROTECTIONZONE;
+ }
+ }
+
+ if(Master->Type == PLAYER && CheckRight(Master->ID, NO_ATTACK)){
+ this->StopAttack(0);
+ throw ATTACKNOTALLOWED;
+ }
+
+ if(Master->Type == PLAYER && Target->Type == PLAYER){
+ if(((TPlayer*)Master)->GetRealProfession() == PROFESSION_NONE
+ && !CheckRight(Master->ID, ATTACK_EVERYWHERE)){
+ this->StopAttack(0);
+ throw ATTACKNOTALLOWED;
+ }
+ }
+
+ if(WorldType == NON_PVP){
+ if(!Master->IsPeaceful() || !Target->IsPeaceful()){
+ if(Target->Type == NPC){
+ this->StopAttack(0);
+ throw ATTACKNOTALLOWED;
+ }
+ }
+
+ if(Master->Type == PLAYER){
+ if(Target->Type == NPC || !CheckRight(Master->ID, ATTACK_EVERYWHERE)){
+ this->StopAttack(0);
+ throw ATTACKNOTALLOWED;
+ }
+ }
+ }
+ }
+
+ if(Master->Type == PLAYER && Target->Type != PLAYER){
+ if(Target->Outfit.OutfitID == 0 && Target->Outfit.ObjectType == 0){
+ this->StopAttack(0);
+ throw TARGETLOST;
+ }
+ }
+
+ int Distance = ObjectDistance(Master->CrObject, Target->CrObject);
+ if(Distance > 8){
+ this->StopAttack(0);
+ throw TARGETLOST;
+ }
+
+ this->AttackDest = TargetID;
+ this->Following = Follow;
+ if(!Follow){
+ Target->AttackStimulus(Master->ID);
+ Master->BlockLogout(60, Target->Type == PLAYER);
+ if(Master->Type == PLAYER && Target->Type == PLAYER){
+ ((TPlayer*)Master)->RecordAttack(TargetID);
+ }
+ this->LatestAttackTime = 0;
+ }
+}
+
+void TCombat::CanToDoAttack(void){
+ if(this->AttackDest == 0){
+ return;
+ }
+
+ // TODO(fusion): There is some `CanAttack` function inlined here.
+ TCreature *Master = this->Master;
+ if(Master == NULL){
+ error("TCombat::CanAttack: Kein Master gesetzt!\n");
+ throw ERROR;
+ }
+
+ TCreature *Target = GetCreature(this->AttackDest);
+ if(Target == NULL){
+ this->StopAttack(0);
+ throw TARGETLOST;
+ }
+
+ if(Master->Type == PLAYER && Target->Type != PLAYER){
+ if(Target->Outfit.OutfitID == 0 && Target->Outfit.ObjectType == 0){
+ this->StopAttack(0);
+ throw TARGETLOST;
+ }
+ }
+
+ if(!this->Following){
+ if(Master->Type == PLAYER && Target->Type == PLAYER){
+ if(this->SecureMode == 1 && WorldType == NORMAL
+ && !((TPlayer*)Master)->IsAttackJustified(Target->ID)){
+ this->StopAttack(0);
+ throw SECUREMODE;
+ }
+ }
+
+ if(WorldType == NON_PVP){
+ if(Master->IsPeaceful() && Target->IsPeaceful()){
+ if(Master->Type != PLAYER || !CheckRight(Master->ID, ATTACK_EVERYWHERE)){
+ this->StopAttack(0);
+ throw ATTACKNOTALLOWED;
+ }
+ }
+ }
+ }
+
+ int Distance = ObjectDistance(Master->CrObject, Target->CrObject);
+ if(Distance > 8){
+ this->StopAttack(0);
+ throw TARGETLOST;
+ }
+
+ int ChaseMode = this->ChaseMode;
+ if(this->Following){
+ ChaseMode = CHASE_MODE_CLOSE;
+ }
+
+ if(ChaseMode == CHASE_MODE_CLOSE){
+ if(Distance > 1){
+ Master->ToDoGo(Target->posx, Target->posy, Target->posz, false, 3);
+ }
+ }else if(ChaseMode == CHASE_MODE_RANGE){
+ if(Distance > 4){
+ Master->ToDoGo(Target->posx, Target->posy, Target->posz, false, Distance - 4);
+ }else if(Distance < 4){
+ int DestX, DestY, DestZ;
+ if(SearchFlightField(Master->ID, Target->ID, &DestX, &DestY, &DestZ)){
+ Master->ToDoGo(DestX, DestY, DestZ, true, -1);
+ }
+ }
+ }
+}
+
void TCombat::Attack(void){
if(this->AttackDest == 0 || this->Following){
return;
}
+ // TODO(fusion): There is some `CanAttack` function inlined here.
TCreature *Master = this->Master;
if(Master == NULL){
error("TCombat::CanAttack: Kein Master gesetzt!\n");
@@ -232,7 +406,7 @@ void TCombat::Attack(void){
}
this->CloseAttack(Target);
}else{
- if(Range < 1 || Range > 3){
+ if(Range < 2 || Range > 3){
throw ERROR;
}
@@ -243,7 +417,16 @@ void TCombat::Attack(void){
throw TARGETOUTOFRANGE;
}
- this->RangeAttack(Target);
+ // NOTE(fusion): Originally, this was a single function `RangeAttack`
+ // and I quickly realized that readability would be massively improved
+ // by splitting it into `DistanceAttack` and `WandAttack`.
+ if(this->Missile != NONE || this->Throw != NONE){
+ this->DistanceAttack(Target);
+ }else if(this->Wand != NONE){
+ this->WandAttack(Target);
+ }else{
+ throw ERROR;
+ }
}
this->DelayAttack(2000);
@@ -270,3 +453,229 @@ void TCombat::DelayAttack(int Milliseconds){
this->EarliestAttackTime = EarliestAttackTime;
}
}
+
+void TCombat::CloseAttack(TCreature *Target){
+ int Attack;
+ uint16 SkillNr;
+ this->GetAttackValue(&Attack, &SkillNr);
+ if(this->AttackMode == ATTACK_MODE_OFFENSIVE){
+ Attack += (Attack * 2) / 10;
+ }else if(this->AttackMode == ATTACK_MODE_DEFENSIVE){
+ Attack -= (Attack * 4) / 10;
+ }
+
+ TCreature *Master = this->Master;
+ Attack = Master->Skills[SkillNr]->ProbeValue(Attack, this->LearningPoints > 0);
+ if(this->LearningPoints > 0){
+ this->LearningPoints -= 1;
+ }
+
+ int Defense = Target->Combat.GetDefendDamage();
+ int Damage = Attack - Defense;
+ if(Damage < 0){
+ Damage = 0;
+ }
+
+ int DamageDone = Target->Damage(Master, Damage, 1); // DAMAGE_PHYSICAL ?
+ if(DamageDone > 0){
+ this->LearningPoints = 30;
+ }
+
+ int Poison = GetRacePoison(Master->Race);
+ if(Poison != 0){
+ if(DamageDone > 0 || (Attack > Defense && (rand() % 5) == 0)){
+ int PoisonDamage = random(Poison / 2, Poison);
+ if(PoisonDamage > 0){
+ Target->Damage(Master, PoisonDamage, DAMAGE_POISON_PERIODIC);
+ if(Target->Type == PLAYER){
+ SendMessage(Target->Connection, TALK_STATUS_MESSAGE, "You are poisoned.");
+ }
+ }
+ }
+ }
+
+ Object Close = this->Close;
+ if(Close != NONE){
+ ObjectType CloseType = Close.getObjectType();
+ if(CloseType.getFlag(WEAROUT)){
+ uint32 RemainingUses = Close.getAttribute(REMAININGUSES);
+ if(RemainingUses > 1){
+ Change(Close, REMAININGUSES, RemainingUses - 1);
+ }else{
+ ObjectType WearOutType = CloseType.getAttribute(WEAROUTTARGET);
+ Change(Close, WearOutType, 0);
+ this->CheckCombatValues();
+ }
+ }
+ }
+}
+
+void TCombat::WandAttack(TCreature *Target){
+ ASSERT(this->Wand != NONE);
+
+ TCreature *Master = this->Master;
+ int Distance = std::min<int>(
+ std::abs(Master->posx - Target->posx),
+ std::abs(Master->posy - Target->posy));
+
+ ObjectType WandType = this->Wand.getObjectType();
+ if(Distance > (int)WandType.getAttribute(WANDRANGE)){
+ throw TARGETOUTOFRANGE;
+ }
+
+ if(!ThrowPossible(Master->posx, Master->posy, Master->posz,
+ Target->posx, Target->posy, Target->posz, 0)){
+ throw TARGETHIDDEN;
+ }
+
+ int DamageType = WandType.getAttribute(WANDDAMAGETYPE);
+ int AnimType = WandType.getAttribute(WANDMISSILE);
+ int ManaConsumption = WandType.getAttribute(WANDMANACONSUMPTION);
+ int AttackStrength = WandType.getAttribute(WANDATTACKSTRENGTH);
+ int AttackVariation = (int)WandType.getAttribute(WANDATTACKVARIATION);
+
+ // NOTE(fusion): Oof...
+ try{
+ CheckMana(Master, ManaConsumption, 0, 0);
+ }catch(RESULT err){
+ if(err == NOTENOUGHMANA){
+ throw OUTOFAMMO;
+ }else{
+ throw err;
+ }
+ }
+
+ int Damage = AttackStrength + random(-AttackVariation, AttackVariation);
+ if(Target->Damage(Master, Damage, DamageType) > 0){
+ this->LearningPoints = 30;
+ }
+
+ ::Missile(Master->CrObject, Target->CrObject, AnimType);
+}
+
+void TCombat::DistanceAttack(TCreature *Target){
+ ASSERT(this->Missile != NONE || this->Throw != NONE);
+
+ int HitChance;
+ int DamageType;
+ int AnimType;
+ int Fragility;
+ int SpecialEffect;
+ int EffectStrength;
+
+ TCreature *Master = this->Master;
+ int DistanceX = std::abs(Master->posx - Target->posx);
+ int DistanceY = std::abs(Master->posy - Target->posy);
+ int Distance = std::max<int>(DistanceX, DistanceY);
+
+ if(this->Missile != NONE){
+ this->GetAmmo();
+ if(this->Ammo == NONE || !this->Ammo.exists()){
+ throw OUTOFAMMO;
+ }
+
+ ObjectType BowType = this->Missile.getObjectType();
+ ObjectType AmmoType = this->Ammo.getObjectType();
+ if(Distance > (int)BowType.getAttribute(BOWRANGE)){
+ throw TARGETOUTOFRANGE;
+ }
+
+ HitChance = 90;
+ DamageType = DAMAGE_PHYSICAL;
+ AnimType = AmmoType.getAttribute(AMMOMISSILE);
+ Fragility = 100;
+ SpecialEffect = AmmoType.getAttribute(AMMOSPECIALEFFECT);
+ EffectStrength = AmmoType.getAttribute(AMMOEFFECTSTRENGTH);
+ }else{
+ ASSERT(this->Throw != NONE);
+ ObjectType ThrowType = this->Throw.getObjectType();
+ if(Distance > (int)ThrowType.getAttribute(THROWRANGE)){
+ throw TARGETOUTOFRANGE;
+ }
+
+ HitChance = 75;
+ DamageType = DAMAGE_PHYSICAL;
+ AnimType = ThrowType.getAttribute(THROWMISSILE);
+ Fragility = ThrowType.getAttribute(THROWFRAGILITY);
+ SpecialEffect = ThrowType.getAttribute(THROWSPECIALEFFECT);
+ EffectStrength = ThrowType.getAttribute(THROWEFFECTSTRENGTH);
+ }
+
+ if(!ThrowPossible(Master->posx, Master->posy, Master->posz,
+ Target->posx, Target->posy, Target->posz, 0)){
+ throw TARGETHIDDEN;
+ }
+
+ int Difficulty = (Distance >= 2) ? Distance : 5;
+ bool Hit = Master->Skills[SKILL_DISTANCE]->Probe(
+ Difficulty * 15, HitChance, this->LearningPoints > 0);
+ if(this->LearningPoints > 0){
+ this->LearningPoints -= 1;
+ }
+
+ int DropX = Target->posx;
+ int DropY = Target->posy;
+ int DropZ = Target->posz;
+ if(Hit){
+ int Attack;
+ uint16 SkillNr;
+ this->GetAttackValue(&Attack, &SkillNr);
+ if(this->AttackMode == ATTACK_MODE_OFFENSIVE){
+ Attack += (Attack * 2) / 10;
+ }else if(this->AttackMode == ATTACK_MODE_DEFENSIVE){
+ Attack -= (Attack * 4) / 10;
+ }
+
+ Attack = Master->Skills[SkillNr]->ProbeValue(Attack, this->LearningPoints > 0);
+ if(this->LearningPoints > 0){
+ this->LearningPoints -= 1;
+ }
+
+ // TODO(fusion): This doesn't make any sense. The disassembly looks
+ // correct but I feel we're missing something here. Or maybe defense
+ // doesn't work with ranged attacks but worn them out?
+ if(this->Shield != NONE){
+ Target->Combat.GetDefendDamage();
+ }
+
+ if(Target->Damage(Master, Attack, DamageType) > 0){
+ this->LearningPoints = 30;
+ }
+ }else{
+ if(DistanceX > 1 || DistanceY > 1){
+ DropX += (rand() % 3) - 1;
+ DropY += (rand() % 3) - 1;
+ }
+
+ if(!CoordinateFlag(DropX, DropY, DropZ, BANK)
+ || CoordinateFlag(DropX, DropY, DropZ, UNLAY)
+ || !ThrowPossible(Master->posx, Master->posy, Master->posz, DropX, DropY, DropZ, 0)){
+ DropX = Target->posx;
+ DropY = Target->posy;
+ }
+ }
+
+ Object DropCon = GetMapContainer(DropX, DropY, DropZ);
+ ::Missile(Master->CrObject, DropCon, AnimType);
+
+ if(SpecialEffect == 1){ // POISON ARROW
+ if(Hit){
+ Target->Damage(Master, EffectStrength, DAMAGE_POISON_PERIODIC);
+ }
+ }else if(SpecialEffect == 2){ // BURST ARROW
+ int Damage = ComputeDamage(Master, 0, EffectStrength, EffectStrength);
+ TDamageImpact Impact(Master, DAMAGE_PHYSICAL, Damage, false);
+ CircleShapeSpell(Master, DropX, DropY, DropZ, -1,
+ ANIMATION_NONE, 2, &Impact, EFFECT_BURST_ARROW);
+ }
+
+ if(random(0, 99) < Fragility){
+ Delete(this->Ammo, 1);
+ }else{
+ Move(0, this->Ammo, DropCon, 1, false, NONE);
+ }
+
+ if(!Hit){
+ GraphicalEffect(DropX, DropY, DropZ, EFFECT_POFF);
+ }
+}