void psNPCLoader::WriteStats() { csRef<iDocumentNode> statsNode = npcRoot->CreateNodeBefore(CS_NODE_ELEMENT); statsNode->SetValue("stats"); MathEnvironment baseSkillVal; npc->GetSkillBaseValues(&baseSkillVal); statsNode->SetAttributeAsInt("agi", baseSkillVal.Lookup("AGI")->GetRoundValue()); statsNode->SetAttributeAsInt("cha", baseSkillVal.Lookup("CHA")->GetRoundValue()); statsNode->SetAttributeAsInt("end", baseSkillVal.Lookup("END")->GetRoundValue()); statsNode->SetAttributeAsInt("int", baseSkillVal.Lookup("INT")->GetRoundValue()); statsNode->SetAttributeAsInt("str", baseSkillVal.Lookup("STR")->GetRoundValue()); statsNode->SetAttributeAsInt("wil", baseSkillVal.Lookup("WIL")->GetRoundValue()); }
int EntityManager::CalculateFamiliarAffinity( psCharacter * chardata, size_t type, size_t lifecycle, size_t attacktool, size_t attacktype ) { static MathScript *msAffinity; int affinityValue = 0; if (!msAffinity) { msAffinity = psserver->GetMathScriptEngine()->FindScript("CalculateFamiliarAffinity"); CS_ASSERT(msAffinity != NULL); } // Determine Familiar Type using Affinity Values if ( msAffinity ) { MathEnvironment env; env.Define("Actor", chardata); env.Define("Type", type); env.Define("Lifecycle", lifecycle); env.Define("AttackTool", attacktool); env.Define("AttackType", attacktype); msAffinity->Evaluate(&env); MathVar *affinity = env.Lookup("Affinity"); affinityValue = affinity->GetRoundValue(); } return affinityValue; }
TEST(MathScriptTest, StringTest2) { Foo foo; MathScript *script = MathScript::Create("StringTest2", "\ HasWeapon = 0;\ SkillName = if(HasWeapon, 'Sword', \"Lah'ar\");\ Rank = Quux:GetSkillRank(SkillName);\ "); ASSERT_NE(script, NULL); MathEnvironment env; env.Define("Quux", &foo); ASSERT_NE(env.Lookup("Quux"), NULL); script->Evaluate(&env); MathVar *rank = env.Lookup("Rank"); EXPECT_EQ(77, rank->GetValue()); };
void CombatManager::DebugOutput(psCombatGameEvent *event, const MathEnvironment & env) { MathVar *missed = env.Lookup("Missed"); // Missed = Attack missed the enemy MathVar *dodged = env.Lookup("Dodged"); // Dodged = Attack dodged by enemy MathVar *blocked = env.Lookup("Blocked"); // Blocked = Attack blocked by enemy MathVar *damage = env.Lookup("FinalDamage"); // Actual damage done, if any psItem* item = event->GetAttackerData()->Inventory().GetEffectiveWeaponInSlot(event->GetWeaponSlot() ); csString debug; debug.Append( "-----Debug Combat Summary--------\n"); debug.AppendFmt( "%s attacks %s with slot %d , weapon %s, quality %1.2f, basedmg %1.2f/%1.2f/%1.2f\n", event->attacker->GetName(),event->target->GetName(), event->GetWeaponSlot(),item->GetName(),item->GetItemQuality(), item->GetDamage(PSITEMSTATS_DAMAGETYPE_SLASH),item->GetDamage(PSITEMSTATS_DAMAGETYPE_BLUNT),item->GetDamage(PSITEMSTATS_DAMAGETYPE_PIERCE)); debug.AppendFmt( "Missed: %1.6f Dodged: %1.6f Blocked: %1.6f", missed->GetValue(), dodged->GetValue(), blocked->GetValue()); debug.AppendFmt( "Damage: %1.1f\n", damage->GetValue()); Debug2(LOG_COMBAT, event->attacker->GetClientID(), "%s", debug.GetData()); }
TEST(MathScriptTest, BasicExpression) { MathExpression *exp = MathExpression::Create("X < 7"); ASSERT_NE(exp, NULL); MathEnvironment env; env.Define("X", 5); ASSERT_NE(env.Lookup("X"), NULL); EXPECT_NE(exp->Evaluate(&env), 0.0); }
TEST(MathScriptTest, PropertyAndFunction) { Foo foo; MathExpression *exp = MathExpression::Create("Quux:TheAnswer = Quux:Multiply(6,7)"); MathEnvironment env; ASSERT_NE(exp, NULL); env.Define("Quux", &foo); ASSERT_NE(env.Lookup("Quux"), NULL); EXPECT_NE(0.0, exp->Evaluate(&env)); }
TEST(MathScriptTest, NestedCalls) { Foo foo; MathExpression *exp = MathExpression::Create("Quux:Multiply(Quux:Multiply(2,3),7)"); MathEnvironment env; ASSERT_NE(exp, NULL); env.Define("Quux", &foo); ASSERT_NE(env.Lookup("Quux"), NULL); EXPECT_EQ(42, exp->Evaluate(&env)); };
TEST(MathScriptTest, MultipleOr) { Foo foo; MathExpression *exp = MathExpression::Create("Quux:Multiply(0,8) | Quux:Multiply(2,3) | Quux:Multiply(17,0)"); ASSERT_NE(exp, NULL); MathEnvironment env; env.Define("Quux", &foo); ASSERT_NE(env.Lookup("Quux"), NULL); EXPECT_NE(exp->Evaluate(&env), 0.0); }
TEST(MathScriptTest, StringTest) { Foo foo; MathExpression *exp = MathExpression::Create("Quux:GetSkillRank('Lah\\'ar')"); MathEnvironment env; ASSERT_NE(exp, NULL); env.Define("Quux", &foo); ASSERT_NE(env.Lookup("Quux"), NULL); EXPECT_EQ(77, exp->Evaluate(&env)); };
TEST(MathScriptTest, BasicScript) { // Basic conversion from Fahrenheit to Celsius and Kelvin. MathScript *script = MathScript::Create("TemperatureConv", "C = (F - 32) * 5/9; K = C + 273.15"); ASSERT_NE(script, NULL); MathEnvironment env; env.Define("F", 212.0); script->Evaluate(&env); MathVar *F = env.Lookup("F"); MathVar *C = env.Lookup("C"); MathVar *K = env.Lookup("K"); EXPECT_NE(F, NULL); EXPECT_NE(C, NULL); EXPECT_NE(K, NULL); EXPECT_EQ(212.0, F->GetValue()); EXPECT_EQ(100.0, C->GetValue()); EXPECT_EQ(373.15, K->GetValue()); EXPECT_STREQ("373.15", K->ToString()); }
TEST(MathScriptTest, MethodNoArgs) { Foo foo; MathExpression *exp = MathExpression::Create("Quux:GetSeven()"); MathEnvironment env; ASSERT_NE(exp, NULL); env.Define("Quux", &foo); ASSERT_NE(env.Lookup("Quux"), NULL); EXPECT_EQ(7, exp->Evaluate(&env)); }
TEST(MathScriptTest, BasicProperty) { Foo foo; MathExpression *exp = MathExpression::Create("Quux:TheAnswer"); MathEnvironment env; ASSERT_NE(exp, NULL); env.Define("Quux", &foo); ASSERT_NE(env.Lookup("Quux"), NULL); EXPECT_EQ(42, exp->Evaluate(&env)); }
TEST(MathScriptTest, StringAssignment) { Foo foo; MathScript *script = MathScript::Create("StringAssignment", "Skill = 'Sword'; Rank = Quux:GetSkillRank(Skill);"); MathEnvironment env; ASSERT_NE(script, NULL); env.Define("Quux", &foo); script->Evaluate(&env); MathVar *rank = env.Lookup("Rank"); EXPECT_EQ(33, rank->GetValue()); };
TEST(MathScriptTest, StringAssignment2) { MathScript *script = MathScript::Create("StringAssignment2", "Skill = 'Sword';"); ASSERT_NE(script, NULL); MathEnvironment env; script->Evaluate(&env); MathVar *rank = env.Lookup("Skill"); ASSERT_NE(rank, NULL); EXPECT_STREQ("Sword", rank->ToString()); };
float LootRandomizer::CalcModifierCostCap(psCharacter* chr) { float result = 1000.0; if(modifierCostCalc) { // Use the mob's attributes to calculate modifier cost cap MathEnvironment env; env.Define("Actor", chr); env.Define("MaxHP", chr->GetMaxHP().Current()); env.Define("MaxMana", chr->GetMaxMana().Current()); (void) modifierCostCalc->Evaluate(&env); MathVar* modcap = env.Lookup("ModCap"); result = modcap->GetValue(); } Debug2(LOG_LOOT,0,"DEBUG: Calculated cost cap %f\n", result); return result; }
void randomgentest(int limit) { csString scriptstr; if (limit<0) { scriptstr = "Roll = rnd();"; limit=1; } else { scriptstr.Format("Roll = rnd(%d);",limit); } MathScript *script = MathScript::Create("randomgen test", scriptstr.GetData()); MathEnvironment env; ASSERT_NE(script, NULL) << scriptstr.GetData() << " did not create script"; bool above1=false, abovehalflimit=false; for (int i=0; i<100; i++) //try 100 times, since this is random { script->Evaluate(&env); MathVar *roll = env.Lookup("Roll"); EXPECT_GE(roll->GetValue(), 0); EXPECT_LE(roll->GetValue(), limit); if (roll->GetValue()>1) { above1 = true; } if (2*roll->GetValue()>limit) { abovehalflimit = true; } } if (limit>1) { EXPECT_TRUE(above1) << scriptstr << "never exceeds 1"; } if (limit>0) { EXPECT_TRUE(abovehalflimit) << scriptstr << "never exceeds half of limit"; } delete script; }
void CombatManager::HandleCombatEvent(psCombatGameEvent *event) { psCharacter *attacker_data,*target_data; int attack_result; bool skipThisRound = false; if (!event->GetAttacker() || !event->GetTarget()) // disconnected and deleted { #ifdef COMBAT_DEBUG psserver->SendSystemError(event->AttackerCID, "Combat stopped as one participant logged of."); #endif return; } gemActor *gemAttacker = dynamic_cast<gemActor*> ((gemObject *) event->attacker); gemActor *gemTarget = dynamic_cast<gemActor*> ((gemObject *) event->target); attacker_data=event->GetAttackerData(); target_data=event->GetTargetData(); // If the attacker is no longer in attack mode abort. if (gemAttacker->GetMode() != PSCHARACTER_MODE_COMBAT) { #ifdef COMBAT_DEBUG psserver->SendSystemError(event->AttackerCID, "Combat stopped as you left combat mode."); #endif return; } // If target is dead, abort. if (!gemTarget->IsAlive() ) { #ifdef COMBAT_DEBUG psserver->SendSystemResult(event->AttackerCID, "Combat stopped as one participant logged of."); #endif return; } // If the slot is no longer attackable, abort if (!attacker_data->Inventory().CanItemAttack(event->GetWeaponSlot())) { #ifdef COMBAT_DEBUG psserver->SendSystemError(event->AttackerCID, "Combat stopped as you have no longer an attackable item equipped."); #endif return; } if (attacker_data->Inventory().GetEquipmentObject(event->GetWeaponSlot()).eventId != event->id) { #ifdef COMBAT_DEBUG psserver->SendSystemError(event->AttackerCID, "Ignored combat event as newer is in."); #endif return; } psItem* weapon = attacker_data->Inventory().GetEffectiveWeaponInSlot(event->GetWeaponSlot()); // weapon became unwieldable csString response; if(weapon!=NULL && !weapon->CheckRequirements(attacker_data,response)) { Debug2(LOG_COMBAT, gemAttacker->GetClientID(),"%s has lost use of weapon", gemAttacker->GetName() ); psserver->SendSystemError(event->AttackerCID, "You can't use your %s any more.", weapon->GetName() ); return; } // If the weapon in the slot has been changed, skip a turn (latency for this slot may also have changed) if (event->WeaponID != weapon->GetUID()) { Debug2(LOG_COMBAT, gemAttacker->GetClientID(),"%s has changed weapons mid battle", gemAttacker->GetName() ); #ifdef COMBAT_DEBUG psserver->SendSystemError(event->AttackerCID, "Weapon changed. Skipping"); #endif skipThisRound = true; } Client * attacker_client = psserver->GetNetManager()->GetClient(event->AttackerCID); if (attacker_client) { // Input the stamina data MathEnvironment env; env.Define("Actor", event->GetAttacker()); env.Define("Weapon", weapon); staminacombat->Evaluate(&env); MathVar *PhyDrain = env.Lookup("PhyDrain"); MathVar *MntDrain = env.Lookup("MntDrain"); if ( (attacker_client->GetCharacterData()->GetStamina(true) < PhyDrain->GetValue()) || (attacker_client->GetCharacterData()->GetStamina(false) < MntDrain->GetValue()) ) { StopAttack(attacker_data->GetActor()); psserver->SendSystemError(event->AttackerCID, "You are too tired to attack."); return; } // If the target has become impervious, abort and give up attacking if (!attacker_client->IsAllowedToAttack(gemTarget)) { StopAttack(attacker_data->GetActor()); return; } // If the target has changed, abort (assume another combat event has started since we are still in attack mode) if (gemTarget != attacker_client->GetTargetObject()) { #ifdef COMBAT_DEBUG psserver->SendSystemError(event->AttackerCID, "Target changed."); #endif return; } } else { // Check if the npc's target has changed (if it has, then assume another combat event has started.) gemNPC* npcAttacker = dynamic_cast<gemNPC*>(gemAttacker); if (npcAttacker && npcAttacker->GetTarget() != gemTarget) { #ifdef COMBAT_DEBUG psserver->SendSystemError(event->AttackerCID, "NPC's target changed."); #endif return; } } if (gemAttacker->IsSpellCasting()) { psserver->SendSystemInfo(event->AttackerCID, "You can't attack while casting spells."); skipThisRound = true; } if (!skipThisRound) { if (weapon->GetIsRangeWeapon() && weapon->GetUsesAmmo()) { INVENTORY_SLOT_NUMBER otherHand = event->GetWeaponSlot() == PSCHARACTER_SLOT_RIGHTHAND ? PSCHARACTER_SLOT_LEFTHAND: PSCHARACTER_SLOT_RIGHTHAND; attack_result = ATTACK_NOTCALCULATED; psItem* otherItem = attacker_data->Inventory().GetInventoryItem(otherHand); if (otherItem == NULL) { attack_result = ATTACK_OUTOFAMMO; } else if (otherItem->GetIsContainer()) // Is it a quiver? { // Pick the first ammo we can shoot from the container // And set it as the active ammo bool bFound = false; for (size_t i=1; i<attacker_data->Inventory().GetInventoryIndexCount() && !bFound; i++) { psItem* currItem = attacker_data->Inventory().GetInventoryIndexItem(i); if (currItem && currItem->GetContainerID() == otherItem->GetUID() && weapon->GetAmmoTypeID().In(currItem->GetBaseStats()->GetUID())) { otherItem = currItem; bFound = true; } } if (!bFound) attack_result = ATTACK_OUTOFAMMO; } else if (!weapon->GetAmmoTypeID().In(otherItem->GetBaseStats()->GetUID())) { attack_result = ATTACK_OUTOFAMMO; } if (attack_result != ATTACK_OUTOFAMMO) { psItem* usedAmmo = attacker_data->Inventory().RemoveItemID(otherItem->GetUID(), 1); if (usedAmmo) { attack_result=CalculateAttack(event, usedAmmo); usedAmmo->Destroy(); psserver->GetCharManager()->UpdateItemViews(attacker_client->GetClientNum()); } else attack_result=CalculateAttack(event); } } else { attack_result=CalculateAttack(event); } event->AttackResult=attack_result; ApplyCombatEvent(event, attack_result); } // Queue next event to continue combat if this is an auto attack slot if (attacker_data->Inventory().IsItemAutoAttack(event->GetWeaponSlot())) { // CPrintf(CON_DEBUG, "Queueing Slot %d for %s's next combat event.\n",event->GetWeaponSlot(), event->attacker->GetName() ); QueueNextEvent(event); } else { #ifdef COMBAT_DEBUG psserver->SendSystemError(event->AttackerCID, "Item %s is not a auto attack item.",attacker_data->Inventory().GetEffectiveWeaponInSlot(event->GetWeaponSlot())->GetName()); #endif } // else // CPrintf(CON_DEBUG, "Slot %d for %s not an auto-attack slot.\n",event->GetWeaponSlot(), event->attacker->GetName() ); }
void CombatManager::ApplyCombatEvent(psCombatGameEvent *event, int attack_result) { psCharacter *attacker_data = event->GetAttackerData(); psCharacter *target_data=event->GetTargetData(); MathVar *weaponDecay = NULL; MathVar *blockDecay = NULL; MathVar *armorDecay = NULL; MathEnvironment env; psItem *weapon = attacker_data->Inventory().GetEffectiveWeaponInSlot(event->GetWeaponSlot()); psItem *blockingWeapon = target_data->Inventory().GetEffectiveWeaponInSlot(event->GetWeaponSlot(),true); psItem *struckArmor = target_data->Inventory().GetEffectiveArmorInSlot(event->AttackLocation); // there may only be a decay if you actually hit your target by some means if(attack_result == ATTACK_DAMAGE || attack_result == ATTACK_BLOCKED) { // we are guaranteed some armor is present - real one, race one or base one CS_ASSERT(struckArmor); float ArmorVsWeapon = weapon->GetArmorVSWeaponResistance(struckArmor->GetBaseStats()); // clamp value between 0 and 1 ArmorVsWeapon = ArmorVsWeapon > 1.0F ? 1.0F : ArmorVsWeapon < 0.0F ? 0.0F : ArmorVsWeapon; env.Define("Weapon", weapon); // weapon that was used to attack env.Define("BlockingWeapon", blockingWeapon); // weapon that blocked the attack env.Define("Armor", struckArmor); // armor hit env.Define("ArmorVsWeapon", ArmorVsWeapon); // armor vs weapon effectiveness env.Define("Damage", event->FinalDamage); // actual damage dealt env.Define("Blocked", (attack_result == ATTACK_BLOCKED)); // identifies whether this attack was blocked calc_decay->Evaluate(&env); weaponDecay = env.Lookup("WeaponDecay"); blockDecay = env.Lookup("BlockingDecay"); armorDecay = env.Lookup("ArmorDecay"); } gemActor *gemAttacker = dynamic_cast<gemActor*> ((gemObject *) event->attacker); gemActor *gemTarget = dynamic_cast<gemActor*> ((gemObject *) event->target); switch (attack_result) { case ATTACK_DAMAGE: { bool isNearlyDead = false; if (target_data->GetMaxHP().Current() > 0.0 && target_data->GetHP()/target_data->GetMaxHP().Current() > 0.2) { if ((target_data->GetHP() - event->FinalDamage) / target_data->GetMaxHP().Current() <= 0.2) isNearlyDead = true; } psCombatEventMessage ev(event->AttackerCID, isNearlyDead ? psCombatEventMessage::COMBAT_DAMAGE_NEARLY_DEAD : psCombatEventMessage::COMBAT_DAMAGE, gemAttacker->GetEID(), gemTarget->GetEID(), event->AttackLocation, event->FinalDamage, weapon->GetAttackAnimID(gemAttacker->GetCharacterData()), gemTarget->FindAnimIndex("hit")); ev.Multicast(gemTarget->GetMulticastClients(),0,MAX_COMBAT_EVENT_RANGE); // Apply final damage if (target_data!=NULL) { gemTarget->DoDamage(gemAttacker,event->FinalDamage); if (gemAttacker) { gemAttacker->InvokeAttackScripts(gemTarget, weapon); } if (gemTarget) { gemTarget->InvokeDefenseScripts(gemAttacker, weapon); if(isNearlyDead) { gemTarget->InvokeNearlyDeadScripts(gemAttacker, weapon); } } } // If the target wasn't in combat, it is now... // Note that other modes shouldn't be interrupted automatically if (gemTarget->GetMode() == PSCHARACTER_MODE_PEACE || gemTarget->GetMode() == PSCHARACTER_MODE_WORK) { if (gemTarget->GetClient()) // Set reciprocal target gemTarget->GetClient()->SetTargetObject(gemAttacker,true); // The default stance is 'Fully Defensive'. Stance initialStance = GetStance(cacheManager, "FullyDefensive"); AttackSomeone(gemTarget,gemAttacker,initialStance); } if (weapon) { weapon->AddDecay(weaponDecay->GetValue()); } if (struckArmor) { struckArmor->AddDecay(armorDecay->GetValue()); } NotifyTarget(gemAttacker,gemTarget); break; } case ATTACK_DODGED: { psCombatEventMessage ev(event->AttackerCID, psCombatEventMessage::COMBAT_DODGE, gemAttacker->GetEID(), gemTarget->GetEID(), event->AttackLocation, 0, // no dmg on a dodge weapon->GetAttackAnimID(gemAttacker->GetCharacterData()), (unsigned int)-1); // no defense anims yet ev.Multicast(gemTarget->GetMulticastClients(),0,MAX_COMBAT_EVENT_RANGE); NotifyTarget(gemAttacker,gemTarget); break; } case ATTACK_BLOCKED: { psCombatEventMessage ev(event->AttackerCID, psCombatEventMessage::COMBAT_BLOCK, gemAttacker->GetEID(), gemTarget->GetEID(), event->AttackLocation, 0, // no dmg on a block weapon->GetAttackAnimID( gemAttacker->GetCharacterData() ), (unsigned int)-1); // no defense anims yet ev.Multicast(gemTarget->GetMulticastClients(),0,MAX_COMBAT_EVENT_RANGE); if (weapon) { weapon->AddDecay(weaponDecay->GetValue()); } if (blockingWeapon) { blockingWeapon->AddDecay(blockDecay->GetValue()); } NotifyTarget(gemAttacker,gemTarget); break; } case ATTACK_MISSED: { psCombatEventMessage ev(event->AttackerCID, psCombatEventMessage::COMBAT_MISS, gemAttacker->GetEID(), gemTarget->GetEID(), event->AttackLocation, 0, // no dmg on a miss weapon->GetAttackAnimID( gemAttacker->GetCharacterData() ), (unsigned int)-1); // no defense anims yet ev.Multicast(gemTarget->GetMulticastClients(),0,MAX_COMBAT_EVENT_RANGE); NotifyTarget(gemAttacker,gemTarget); break; } case ATTACK_OUTOFRANGE: { if (event->AttackerCID) { psserver->SendSystemError(event->AttackerCID,"You are too far away to attack!"); // Auto-stop attack is commented out below, when out of range to prevent npc kiting by jumping in and out of range //if (event->attacker && event->attacker.IsValid()) // StopAttack(dynamic_cast<gemActor*>((gemObject *) event->attacker)); // if you run away, you exit attack mode } break; } case ATTACK_BADANGLE: { if (event->AttackerCID) // if human player { psserver->SendSystemError(event->AttackerCID,"You must face the enemy to attack!"); // Auto-stop attack is commented out below, when out of range to prevent npc kiting by jumping in and out of range //if (event->attacker && event->attacker.IsValid()) // StopAttack(dynamic_cast<gemActor*>((gemObject *) event->attacker)); // if you run away, you exit attack mode } break; } case ATTACK_OUTOFAMMO: { psserver->SendSystemError(event->AttackerCID, "You are out of ammo!"); if (event->attacker && event->attacker.IsValid()) StopAttack(dynamic_cast<gemActor*>((gemObject *) event->attacker)); // if you run out of ammo, you exit attack mode break; } } }
void psAttack::Affect(psCombatAttackGameEvent* event) { gemActor* attacker = event->GetAttacker(); gemActor* target = event->GetTarget(); if(!attacker || !target) { Debug2(LOG_COMBAT,event->GetAttackerID(),"Attacker ID: %d. Combat stopped as one participant logged off.",event->GetAttackerID()); if(attacker) { attacker->EndAttack(); psserver->GetCombatManager()->StopAttack(attacker); } return; } attacker->EndAttack(); // If the attacker is no longer in attack mode, abort. if(attacker->GetMode() != PSCHARACTER_MODE_COMBAT) { Debug2(LOG_COMBAT,event->GetAttackerID(),"Combat stopped as attacker %d left combat mode.",event->GetAttackerID()); return; } // If target is dead, abort. if(!target->IsAlive()) { Debug2(LOG_COMBAT,event->GetAttackerID(),"Combat stopped as target %d is dead.",event->GetTargetID()); psserver->GetCombatManager()->StopAttack(attacker); return; } // If the slot is no longer attackable, abort psCharacter* attacker_data = attacker->GetCharacterData(); INVENTORY_SLOT_NUMBER slot = event->GetWeaponSlot(); if(!attacker_data->Inventory().CanItemAttack(slot)) { Debug2(LOG_COMBAT,event->GetAttackerID(),"Combat stopped as attacker no longer has an attacking item equipped. Attacker ID: %d",event->GetAttackerID()); psserver->GetCombatManager()->StopAttack(attacker); return; } psItem* weapon = attacker_data->Inventory().GetEffectiveWeaponInSlot(slot); // weapon became unwieldable csString response; if(weapon && !weapon->CheckRequirements(attacker_data, response)) { Debug2(LOG_COMBAT, event->GetAttackerID(), "%s has lost use of weapon", attacker->GetName()); psserver->GetCombatManager()->StopAttack(attacker); psserver->SendSystemError(event->GetAttackerID(), "You can't use your %s any more.", weapon->GetName()); return; } Client* attacker_client = attacker->GetClient(); if(attacker_client) { // Input the stamina data MathEnvironment env; env.Define("Actor", event->GetAttacker()); env.Define("Weapon", weapon); (void) psserver->GetCacheManager()->GetStaminaCombat()->Evaluate(&env); MathVar* PhyDrain = env.Lookup("PhyDrain"); MathVar* MntDrain = env.Lookup("MntDrain"); // stop the attack if the attacker has no stamina left if((attacker_data->GetStamina(true) < PhyDrain->GetValue()) || (attacker_data->GetStamina(false) < MntDrain->GetValue())) { psserver->GetCombatManager()->StopAttack(attacker); psserver->SendSystemError(event->GetAttackerID(), "You are too tired to attack."); return; } // If the target has become impervious, abort and give up attacking csString msg; if(!attacker->IsAllowedToAttack(target, msg)) { psserver->GetCombatManager()->StopAttack(attacker); return; } //I will be seeing how this affects things and make changes accordingly // If the target has changed, abort (assume another combat event has started since we are still in attack mode) if(target != attacker_client->GetTargetObject()) { Debug2(LOG_COMBAT,event->GetAttackerID(),"Skipping attack, Target changed for attacker ID: %d.",event->GetAttackerID()); return; } } else { // Check if the npc's target has changed. // If it has, then assume another combat event has started. gemNPC* npcAttacker = attacker->GetNPCPtr(); if(npcAttacker && npcAttacker->GetTarget() != target) { Debug2(LOG_COMBAT,event->GetAttackerID(),"Skipping attack, Target changed for attacker ID: %d.",event->GetAttackerID()); return; } } // If the weapon in the slot has been changed, // skip a turn (latency for this slot may also have changed). if(event->GetWeapon() != weapon) { Debug2(LOG_COMBAT, attacker->GetClientID(), "Skipping attack because %s has changed weapons mid battle", attacker->GetName()); } else if(attacker->IsSpellCasting()) { psserver->SendSystemInfo(event->GetAttackerID(), "You can't attack while casting spells."); } else { int attack_result; if(weapon->GetIsRangeWeapon() && weapon->GetUsesAmmo()) { INVENTORY_SLOT_NUMBER otherHand = event->GetWeaponSlot() == PSCHARACTER_SLOT_RIGHTHAND ? PSCHARACTER_SLOT_LEFTHAND: PSCHARACTER_SLOT_RIGHTHAND; psItem* otherItem = attacker_data->Inventory().GetInventoryItem(otherHand); uint32_t item_id = 0; if(weapon->GetAmmoType() == PSITEMSTATS_AMMOTYPE_ROCKS) { // Rocks are their own ammo. attack_result = ATTACK_NOTCALCULATED; item_id = weapon->GetUID(); } else if(otherItem == NULL) { attack_result = ATTACK_OUTOFAMMO; } else if(otherItem->GetIsContainer()) // Is it a quiver? { // Pick the first ammo we can shoot from the container // And set it as the active ammo attack_result = ATTACK_OUTOFAMMO; for(size_t i=1; i<attacker_data->Inventory().GetInventoryIndexCount(); i++) { psItem* currItem = attacker_data->Inventory().GetInventoryIndexItem(i); if(currItem && currItem->GetContainerID() == item_id && weapon->UsesAmmoType(currItem->GetBaseStats()->GetUID())) { otherItem = currItem; attack_result = ATTACK_NOTCALCULATED; item_id = otherItem->GetUID(); break; } } } else if(!weapon->UsesAmmoType(otherItem->GetBaseStats()->GetUID())) { attack_result = ATTACK_OUTOFAMMO; } else { attack_result = ATTACK_NOTCALCULATED; item_id = otherItem->GetUID(); } if(attack_result != ATTACK_OUTOFAMMO) { psItem* usedAmmo = attacker_data->Inventory().RemoveItemID(item_id, 1); if(usedAmmo) { attack_result = CalculateAttack(event, usedAmmo); usedAmmo->Destroy(); psserver->GetCharManager()->UpdateItemViews(attacker_client->GetClientNum()); } else attack_result = CalculateAttack(event); } } else { attack_result = CalculateAttack(event); } int affectedCount = 1; AffectTarget(target, event, attack_result); // Handle AOE (Area of Effect) if attack caused some damage. float radius = aoeRadius ? aoeRadius->Evaluate(&event->env) : 0.0; if(radius >= 0.01f && attack_result == ATTACK_DAMAGE) { csVector3 attackerPos; csVector3 targetPos; iSector* attackerSector, *targetSector; InstanceID targetInstance = target->GetInstance(); attacker->GetPosition(attackerPos, attackerSector); target->GetPosition(targetPos, targetSector); // directional vector for a line from attacker to original target csVector3 attackerToTarget; attackerToTarget = targetPos - attackerPos; float angle = aoeAngle ? aoeAngle->Evaluate(&event->env) : 0.0; if(angle <= SMALL_EPSILON || angle > 360) angle = 360; angle = (angle/2)*(PI/180); // convert degrees to radians MathEnvironment& env(event->env); env.Define("AOE_Radius", radius); env.Define("AOE_Angle", angle); csArray<gemObject*> nearby = psserver->entitymanager->GetGEM()->FindNearbyEntities(targetSector, targetPos, targetInstance, radius); for(size_t i = 0; i < nearby.GetSize(); i++) { gemActor* nearby_target = nearby[i]->GetActorPtr(); if(nearby_target == attacker || nearby_target == target) continue; csString msg; if(!attacker->IsAllowedToAttack(nearby_target, msg)) continue; if(angle < PI) { csVector3 attackerToAffected = nearby_target->GetPosition(); // obtain a directional line for the vector from attacker to affacted target // note that this line does not originate at the original target because the // cone that shall include the hittable area shall originate at the attacker attackerToAffected -= attackerPos; // Angle between the line original target->attacker and original target->affected target float cosATAngle = (attackerToAffected * attackerToTarget) / (attackerToAffected.Norm() * attackerToTarget.Norm()); // cap the value to meaningful ones to account for rounding issues if(cosATAngle > 1.0f || CS::IsNaN(cosATAngle)) cosATAngle = 1.0f; if(cosATAngle < -1.0f) cosATAngle = -1.0f; // use the absolute value of the angle here to account // for both sides equally - see above if(fabs(acosf(cosATAngle)) >= angle) continue; } // XXX recompute attack_result? AffectTarget(nearby_target, event, attack_result); affectedCount++; } psserver->SendSystemInfo(attacker->GetClientID(), "%s affected %d %s.", name.GetData(), affectedCount, (affectedCount == 1) ? "target" : "targets"); } // Get the next special attack to execute. psAttack* attack = attacker_data->GetAttackQueue()->First(); if(!attack || !attack->CanAttack(attacker_client)) { // Use the default attack type. attack = psserver->GetCacheManager()->GetAttackByID(1); } // Schedule the next attack. attack->Attack(attacker, target, slot); } }
/** * This is the meat and potatoes of the combat engine here. */ int CombatManager::CalculateAttack(psCombatGameEvent *event, psItem* subWeapon) { INVENTORY_SLOT_NUMBER otherHand = event->GetWeaponSlot() == PSCHARACTER_SLOT_LEFTHAND ? PSCHARACTER_SLOT_RIGHTHAND : PSCHARACTER_SLOT_LEFTHAND; event->AttackLocation = (INVENTORY_SLOT_NUMBER) targetLocations[randomgen->Get((int) targetLocations.GetSize())]; gemObject *attacker = event->GetAttacker(); gemObject *target = event->GetTarget(); // calculate difference between target and attacker location - to be used for angle validation csVector3 diff(0); // initialize to some big value that shows an error { csVector3 attackPos, targetPos; iSector *attackSector, *targetSector; attacker->GetPosition(attackPos, attackSector); target->GetPosition(targetPos, targetSector); if((attacker->GetInstance() != target->GetInstance() && attacker->GetInstance() != INSTANCE_ALL && target->GetInstance() != INSTANCE_ALL) || !(entityManager->GetWorld()->WarpSpace(targetSector, attackSector, targetPos))) { return ATTACK_OUTOFRANGE; } diff = targetPos - attackPos; } MathEnvironment env; env.Define("Attacker", attacker); env.Define("Target", target); env.Define("AttackWeapon", event->GetAttackerData()->Inventory().GetEffectiveWeaponInSlot(event->GetWeaponSlot())); env.Define("AttackWeaponSecondary", subWeapon); env.Define("TargetWeapon", event->GetTargetData()->Inventory().GetEffectiveWeaponInSlot(event->GetWeaponSlot(), true)); env.Define("TargetWeaponSecondary", event->GetTargetData()->Inventory().GetEffectiveWeaponInSlot(otherHand,true)); env.Define("AttackLocationItem", event->GetTargetData()->Inventory().GetEffectiveArmorInSlot(event->AttackLocation)); env.Define("DiffX", diff.x ? diff.x : 0.00001F); // force minimal value env.Define("DiffY", diff.y ? diff.y : 0.00001F); // force minimal value env.Define("DiffZ", diff.z ? diff.z : 0.00001F); // force minimal value calc_damage->Evaluate(&env); if (DoLogDebug2(LOG_COMBAT, event->GetAttackerData()->GetPID().Unbox())) { CPrintf(CON_DEBUG, "Variables for Calculate Damage:\n"); env.DumpAllVars(); } MathVar *badrange = env.Lookup("BadRange"); // BadRange = Target is too far away MathVar *badangle = env.Lookup("BadAngle"); // BadAngle = Attacker doesn't aim at enemy MathVar *missed = env.Lookup("Missed"); // Missed = Attack missed the enemy MathVar *dodged = env.Lookup("Dodged"); // Dodged = Attack dodged by enemy MathVar *blocked = env.Lookup("Blocked"); // Blocked = Attack blocked by enemy MathVar *damage = env.Lookup("FinalDamage"); // Actual damage done, if any if (badrange && badrange->GetValue() < 0.0) return ATTACK_OUTOFRANGE; else if (badangle && badangle->GetValue() < 0.0) return ATTACK_BADANGLE; else if (missed && missed->GetValue() < 0.0) return ATTACK_MISSED; else if (dodged && dodged->GetValue() < 0.0) return ATTACK_DODGED; else if (blocked && blocked->GetValue() < 0.0) return ATTACK_BLOCKED; event->FinalDamage = damage->GetValue(); DebugOutput(event, env); return ATTACK_DAMAGE; }