diff --git a/areas/room.cpp b/areas/room.cpp index 17598c40..75b4a0a1 100644 --- a/areas/room.cpp +++ b/areas/room.cpp @@ -429,59 +429,85 @@ int Monster::doDeleteFromRoom(std::shared_ptr room, bool delPortal) { return(0); } -//******************************************************************** -// addPermCrt -//******************************************************************** -// This function checks a room to see if any permanent monsters need to -// be loaded. If so, the monsters are loaded to the room, and their -// permanent flag is set. - void UniqueRoom::addPermCrt() { std::map::iterator it, nt; CRLastTime* crtm; std::map checklist; - std::shared_ptr monster=nullptr; - long t = time(nullptr); - int j=0, m=0, n=0; + std::shared_ptr monster = nullptr; + long t = time(nullptr); + std::vector groupedTimers; - for(it = permMonsters.begin(); it != permMonsters.end() ; it++) { + for (it = permMonsters.begin(); it != permMonsters.end(); ++it) { crtm = &(*it).second; - if(checklist[(*it).first]) + if (checklist[(*it).first]) continue; - if(!crtm->cr.id) + if (!crtm->cr.id) continue; - if(crtm->ltime + crtm->interval > t) + if (crtm->ltime + crtm->interval > t) continue; - n = 1; - m = 0; + groupedTimers.clear(); + groupedTimers.push_back(crtm); + nt = it; - nt++; - for(; nt != permMonsters.end() ; nt++) { - if( crtm->cr == (*nt).second.cr && - ((*nt).second.ltime + (*nt).second.interval) < t - ) { - n++; + ++nt; + for (; nt != permMonsters.end(); ++nt) { + if (crtm->cr == (*nt).second.cr && + ((*nt).second.ltime + (*nt).second.interval) < t) + { checklist[(*nt).first] = true; + groupedTimers.push_back(&(*nt).second); } } - if(!loadMonster(crtm->cr, monster)) + // Count how many of this monster are already spawned + if (!loadMonster(crtm->cr, monster)) continue; - for(const auto& mons : monsters) { - if( mons->flagIsSet(M_PERMANENT_MONSTER) && mons->getName() == monster->getName() ) - m++; + int existingCount = 0; + for (const auto& mons : monsters) { + if (mons->flagIsSet(M_PERMANENT_MONSTER) && mons->getName() == monster->getName()) { + existingCount++; + } } - monster.reset(); - for(j=0; jcr, monster)) + if (!loadMonster(crtm->cr, monster)) continue; + int spawnChance = monster->getPermSpawnChance(); + if (spawnChance > 0) { + int roll = Random::get(1, 1000); + + if (roll > spawnChance) { + // Failed spawn chance + broadcast(isCt, "^y### PermCrt '%s' in room %s (%s) FAILED perm spawn chance (%d on %d/1000) and did not spawn.^x", + monster->getCName(), info.displayStr().c_str(), getCName(), roll, spawnChance); + groupedTimers[i]->ltime = t; // Reset only this monster's timer + monster.reset(); + continue; + } + + // Passed spawn chance + broadcast(isCt, "^y### PermCrt '%s' in room %s (%s) PASSED perm spawn chance (%d on %d/1000) and has spawned.^x", + monster->getCName(), info.displayStr().c_str(), getCName(), roll, spawnChance); + } else { + // No spawn chance set, guaranteed spawn + broadcast(isCt, "^y### PermCrt '%s' ^yhas respawned in room %s (%s).^x", + monster->getCName(), info.displayStr().c_str(), getCName()); + } + + // Spawn the monster monster->initMonster(); monster->setFlag(M_PERMANENT_MONSTER); monster->daily[DL_BROAD].cur = 20; @@ -490,86 +516,111 @@ void UniqueRoom::addPermCrt() { monster->validateAc(); monster->addToRoom(BaseRoom::downcasted_shared_from_this(), 0); - if(!players.empty()) + if (!players.empty()) gServer->addActive(monster); + + monster.reset(); } } } - -//********************************************************************* -// addPermObj -//********************************************************************* -// This function checks a room to see if any permanent objects need to -// be loaded. If so, the objects are loaded to the room, and their -// permanent flag is set. - void UniqueRoom::addPermObj() { std::map::iterator it, nt; - CRLastTime* crtm=nullptr; + CRLastTime* crtm = nullptr; std::map checklist; - std::shared_ptr object=nullptr; - long t = time(nullptr); - int j=0, m=0, n=0; + std::shared_ptr object = nullptr; + long t = time(nullptr); + std::vector groupedTimers; - for(it = permObjects.begin(); it != permObjects.end() ; it++) { + for (it = permObjects.begin(); it != permObjects.end(); ++it) { crtm = &(*it).second; - if(checklist[(*it).first]) + if (checklist[(*it).first]) continue; - if(!crtm->cr.id) + if (!crtm->cr.id) continue; - if(crtm->ltime + crtm->interval > t) + if (crtm->ltime + crtm->interval > t) continue; - n = 1; - m = 0; + groupedTimers.clear(); + groupedTimers.push_back(crtm); + nt = it; - nt++; - for(; nt != permObjects.end() ; nt++) { - if( crtm->cr == (*nt).second.cr && - ((*nt).second.ltime + (*nt).second.interval) < t ) - { - n++; + ++nt; + for (; nt != permObjects.end(); ++nt) { + if (crtm->cr == (*nt).second.cr && + ((*nt).second.ltime + (*nt).second.interval) < t) { checklist[(*nt).first] = true; + groupedTimers.push_back(&(*nt).second); } } - if(!loadObject(crtm->cr, object)) + // Load object once to check name/info for comparison + if (!loadObject(crtm->cr, object)) continue; - for(const auto& obj : objects) { - if(obj->flagIsSet(O_PERM_ITEM)) { - if(obj->getName() == object->getName() && obj->info == object->info) - m++; - else if( object->getName() == obj->droppedBy.getName() && - object->info.str() == obj->droppedBy.getIndex()) - m++; - } + int existingCount = 0; + for (const auto& obj : objects) { + if (obj->flagIsSet(O_PERM_ITEM)) { + if (obj->getName() == object->getName() && obj->info == object->info) + ++existingCount; + else if (object->getName() == obj->droppedBy.getName() && + object->info.str() == obj->droppedBy.getIndex()) + ++existingCount; + } } - object.reset(); - for(j=0; jcr, object)) + int missing = groupedTimers.size() - existingCount; + if (missing <= 0) + continue; + + for (size_t i = 0; i < groupedTimers.size(); ++i) { + if (i < existingCount) continue; - if (!object->randomObjects.empty()) - object->init(); - else - object->setDroppedBy(shared_from_this(), "PermObject"); + if (!loadObject(crtm->cr, object)) + continue; - if(object->flagIsSet(O_RANDOM_ENCHANT)) - object->randomEnchant(); + int spawnChance = object->getPermSpawnChance(); + if (spawnChance > 0) { + int roll = Random::get(1, 1000); + if (roll > spawnChance) { + // Spawn failed + broadcast(isCt, "^y### PermObj '%s' in room %s (%s) FAILED spawn chance (%d on %d/1000) and did not spawn.^x", + object->getCName(), info.displayStr().c_str(), getCName(), roll, spawnChance); + groupedTimers[i]->ltime = t; + object.reset(); + continue; + } - object->setFlag(O_PERM_ITEM); + broadcast(isCt, "^y### PermObj '%s' in room %s (%s) PASSED spawn chance (%d on %d/1000) and has spawned.^x", + object->getCName(), info.displayStr().c_str(), getCName(), roll, spawnChance); + } else { + // Guaranteed spawn + broadcast(isCt, "^y### PermObj '%s' ^yhas respawned in room %s (%s).^x", + object->getCName(), info.displayStr().c_str(), getCName()); + } + if (!object->randomObjects.empty()) + object->init(); + else + object->setDroppedBy(shared_from_this(), (spawnChance?"VariablePermObject":"PermObject")); + if (object->flagIsSet(O_RANDOM_ENCHANT)) + object->randomEnchant(); + + object->setFlag(O_PERM_ITEM); object->addToRoom(BaseRoom::downcasted_shared_from_this()); + + groupedTimers[i]->ltime = t; + object.reset(); } } } + + //********************************************************************* // roomEffStr //********************************************************************* diff --git a/areas/startlocs.cpp b/areas/startlocs.cpp index ba8b8dd1..f1b04a2a 100644 --- a/areas/startlocs.cpp +++ b/areas/startlocs.cpp @@ -100,9 +100,16 @@ bool startingChoices(std::shared_ptr player, std::string str, char *loca // set race equal to their parent race, use player->getRace() if checking // for subraces - const RaceData *r = gConfig->getRace(race); - if (r && r->getParentRace()) - race = r->getParentRace(); + + //********************************************** + // Not sure why we wanted subraces to always have same startloc options as parent race. + // Doesn't make sense when subraces are very often not even close to the same as + // their parent race. i.e. Dwarf vs Duergar or Elf vs Aquatic Elf. Removing this for now. -TC + + //const RaceData *r = gConfig->getRace(race); + //if (r && r->getParentRace()) + // race = r->getParentRace(); + //********************************************** if (player->getClass() == CreatureClass::DRUID) { @@ -118,11 +125,15 @@ bool startingChoices(std::shared_ptr player, std::string str, char *loca // religious states options.emplace_back("sigil"); - } else if (player->getDeity() == ARAMON || player->getRace() == CAMBION) { + } else if (player->getDeity() == ARAMON) { // religious states options.emplace_back("caladon"); + } else if (player->getDeity() == LINOTHAN || player->getDeity() == MARA) { + + options.emplace_back("eldinwood"); + } else if (race == HUMAN && player->getClass() == CreatureClass::CLERIC) { // all other human clerics have to start in HP because @@ -142,7 +153,8 @@ bool startingChoices(std::shared_ptr player, std::string str, char *loca options.emplace_back("schnai"); - } else if (race == DWARF) { + // When duergar city built, and/or Ironguard is expanded, this will change + } else if (race == DWARF || race == DUERGAR) { options.emplace_back("highport"); @@ -176,11 +188,12 @@ bool startingChoices(std::shared_ptr player, std::string str, char *loca options.emplace_back("caladon"); options.emplace_back("orc"); - } else if (race == ORC) { + } else if (race == ORC || race == OROG) { options.emplace_back("orc"); - } else if (race == ELF || player->getDeity() == LINOTHAN) { + } else if (race == ELF || + race == GREYELF || race == WILDELF) { options.emplace_back("eldinwood"); @@ -201,6 +214,7 @@ bool startingChoices(std::shared_ptr player, std::string str, char *loca switch (player->getClass()) { case CreatureClass::RANGER: options.emplace_back("druidwood"); + options.emplace_back("highport"); break; // even seraphs of these classes cannot start in Sigil case CreatureClass::ASSASSIN: diff --git a/bards/identify.cpp b/bards/identify.cpp index d620c3da..99b690a1 100644 --- a/bards/identify.cpp +++ b/bards/identify.cpp @@ -131,19 +131,18 @@ int cmdIdentify(const std::shared_ptr& player, cmd* cmnd) { player->checkImprove("identify", false); broadcast(player->getSock(), player->getParent(), "%M carefully studies %P.",player.get(), object.get()); player->lasttime[LT_IDENTIFY].ltime = t; - player->lasttime[LT_IDENTIFY].interval = 45L; + player->lasttime[LT_IDENTIFY].interval = 15L; return(0); } else { broadcast(player->getSock(), player->getParent(), "%M carefully studies %P.", player.get(), object.get()); broadcast(player->getSock(), player->getParent(), "%s successfully identifies it!", player->upHeShe()); - player->printColor("You carefully study %P.\n",object.get()); - player->printColor("You manage to learn about %P!\n", object.get()); + player->printColor("You managed to learn about %P !\n\n", object.get()); player->checkImprove("identify", true); object->clearFlag(O_JUST_BOUGHT); player->lasttime[LT_IDENTIFY].ltime = t; - player->lasttime[LT_IDENTIFY].interval = 45L; + player->lasttime[LT_IDENTIFY].interval = 60L; if(object->increase) { @@ -168,6 +167,11 @@ int cmdIdentify(const std::shared_ptr& player, cmd* cmnd) { } else if(object->getType() == ObjectType::WEAPON) { player->printColor("%O is a %s, with an average damage of %d.\n", object.get(), object->getTypeName().c_str(), std::max(1, object->damage.average() + object->getAdjustment())); + + if(object->getMagicpower() && object->flagIsSet(O_WEAPON_CASTS)) { + player->printColor("%O will %s the ^W%s^x spell on its enemies.\n", object.get(), object->getCastChance()?"sometimes cast":"cast", get_spell_name(object->getMagicpower()-1)); + } + } else if(object->getType() == ObjectType::POISON) { player->printColor("%O is a poison.\n", object.get()); player->print("It has a maximum duration of %d seconds.\n", object->getEffectDuration()); @@ -395,7 +399,7 @@ int cmdIdentify(const std::shared_ptr& player, cmd* cmnd) { output = object->value.str(); player->print("It is worth %s", output.c_str()); if(object->getType() != ObjectType::CONTAINER && object->getType() != ObjectType::MONEY) { - player->print(", and is ", object.get()); + player->print(", and it is ", object.get()); if(object->getShotsCur() >= object->getShotsMax() * .99) player->print("brand new"); else if(object->getShotsCur() >= object->getShotsMax() * .90) diff --git a/combat/attack.cpp b/combat/attack.cpp index d634f424..df78c266 100644 --- a/combat/attack.cpp +++ b/combat/attack.cpp @@ -466,7 +466,8 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a if(isMagicallyHeld(false)) return(0); - if(attackType != ATTACK_BASH && attackType != ATTACK_AMBUSH && attackType != ATTACK_MAUL && attackType != ATTACK_KICK && attackType != ATTACK_GORE) { + if(attackType != ATTACK_BASH && attackType != ATTACK_AMBUSH && attackType != ATTACK_MAUL && + attackType != ATTACK_KICK && attackType != ATTACK_GORE && attackType != ATTACK_SLAM) { if(!checkAttackTimer()) return(0); @@ -527,7 +528,7 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a broadcast(getSock(), pVictim->getSock(), getRoomParent(), "%M attacked %N!", this, pVictim.get()); } - if(attackType != ATTACK_KICK && attackType != ATTACK_MAUL && attackType != ATTACK_GORE) { + if(attackType != ATTACK_KICK && attackType != ATTACK_MAUL && attackType != ATTACK_GORE && attackType != ATTACK_BASH && attackType != ATTACK_SLAM) { // A monk that has no weapon, no holding item, but is wearing gloves gets to use the enchant off of them if (cClass == CreatureClass::MONK && !ready[WIELD - 1] && !ready[HELD - 1] && ready[HANDS - 1]) { //enchant = abs(ready[HANDS-1]->adjustment); @@ -541,34 +542,49 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a loc = WIELD; // No multiple attacks for bash - if (attackType != ATTACK_BASH) { + // if (attackType != ATTACK_BASH) { <--- This will get changed to ATTACK_SLAM // Two attacks for duel wield if (ready[HELD - 1] && ready[HELD - 1]->getWearflag() == WIELD) { duelWield = true; attacks++; } - } + + // } + } } else if(attackType == ATTACK_KICK) { // kick if(ready[FEET-1]) { weapon = ready[FEET-1]; - //enchant = abs(weapon->adjustment); duelWield = false; loc = FEET; } + } else if(attackType == ATTACK_BASH) { + // shield bash + if(ready[SHIELD-1]) { + weapon = ready[SHIELD-1]; + duelWield = false; + loc = SHIELD; + } } else if(attackType == ATTACK_GORE) { - // kick + // gore if(ready[HEAD-1]) { weapon = ready[HEAD-1]; - //enchant = abs(weapon->adjustment); + duelWield = false; + loc = HEAD; + } + } else if(attackType == ATTACK_SLAM) { + // gore + if(ready[WIELD-1]) { + weapon = ready[WIELD-1]; duelWield = false; loc = HEAD; } } - // No numAttacks for ambush - if(weapon && weapon->getNumAttacks() && attackType != ATTACK_AMBUSH && attackType != ATTACK_BASH && attackType != ATTACK_MAUL) { + // No numAttacks for ambush, bash, maul, slam, gore, kick + if(weapon && weapon->getNumAttacks() && attackType != ATTACK_AMBUSH && attackType != ATTACK_BASH && attackType != ATTACK_MAUL && + attackType != ATTACK_SLAM && attackType != ATTACK_GORE && attackType != ATTACK_KICK) { // Random number of attacks 1-numAttacks attacks = Random::get(1, weapon->getNumAttacks()); // TODO: maybe add a flag so always certain # of attacks @@ -601,13 +617,19 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a // Can't fumble your boots when kicking resultFlags |= NO_FUMBLE; altSkillLevel = (int)getSkillGained("kick"); - } else if(attackType == ATTACK_MAUL) { + } else if(attackType == ATTACK_BASH) { + // Can't fumble shield when bashing + resultFlags |= NO_FUMBLE; + altSkillLevel = (int)getSkillGained("bash"); + }else if(attackType == ATTACK_MAUL) { resultFlags |= NO_FUMBLE; altSkillLevel = (int)getSkillGained("maul"); } else if(attackType == ATTACK_GORE) { resultFlags |= NO_FUMBLE; altSkillLevel = (int)getSkillGained("gore"); - } + } else if(attackType == ATTACK_SLAM) { + altSkillLevel = (int)getSkillGained("slam"); + } AttackResult result = getAttackResult(victim, weapon, resultFlags, altSkillLevel); // We can only fumble on the first hit of a multi weapon attack, @@ -620,6 +642,12 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a if(result == ATTACK_HIT || result == ATTACK_CRITICAL || result == ATTACK_BLOCK || result == ATTACK_GLANCING) { if(!pVictim && victim->flagIsSet(M_NO_BACKSTAB)) result = ATTACK_MISS; + + if(victim->getRace() == BARBARIAN && Random::get(1,100) <= 15) { + *this << setf(CAP) << victim << " noticed you at the last second! Your ambush failed!\n"; + *victim << "You noticed " << this << "'s ambush just in the nick of time!\n"; + result = ATTACK_MISS; + } } } @@ -644,6 +672,15 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a if(ready[SHIELD-1] && ready[SHIELD-1]->flagIsSet(O_ENHANCE_BASH)) multiplier += .1; } + else if(attackType == ATTACK_SLAM) { + if(cClass == CreatureClass::BERSERKER || cClass == CreatureClass::FIGHTER) + multiplier = 0.5; + else + multiplier = 0.4; + + if(ready[WIELD-1] && ready[WIELD-1]->flagIsSet(O_ENHANCE_SLAM)) + multiplier += .1; + } bool computeBonus = (!multiWeapon || (multiWeapon && attacked==1)); @@ -678,7 +715,7 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a bool wasKilled = false, freeTarget = false, meKilled; if(attackType == ATTACK_BASH) { - atk ="bashed"; + atk ="shield bashed"; showToRoom = true; } else if(attackType == ATTACK_KICK) { atk ="kicked"; @@ -690,6 +727,10 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a atk ="gored"; showToRoom = true; } + else if(attackType == ATTACK_SLAM) { + atk ="slammed"; + showToRoom = true; + } else { atk = getDamageString(Containable::downcasted_shared_from_this(), weapon, result == ATTACK_CRITICAL ? true : false); } @@ -704,7 +745,7 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a meKilled = doReflectionDamage(attackDamage, victim); - if(!meKilled && weapon && weapon->getMagicpower() && weapon->flagIsSet(O_WEAPON_CASTS) && weapon->getChargesCur() > 0) + if(!meKilled && attackType != ATTACK_SLAM && weapon && weapon->getMagicpower() && weapon->flagIsSet(O_WEAPON_CASTS) && weapon->getChargesCur() > 0) wcdmg += castWeapon(victim, weapon, wasKilled); broadcastGroup(false, victim, "^M%M^x %s ^M%N^x for *CC:DAMAGE*%d^x damage, %s%s\n", this, atk.c_str(), victim.get(), attackDamage.get()+drain, victim->heShe(), victim->getStatusStr(attackDamage.get()+drain)); @@ -735,10 +776,17 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a if(attackType == ATTACK_BASH) { if(victim->isPlayer()) - victim->stun(Random::get(4,6)); + victim->stun(Random::get(2,3)); else - victim->stun(Random::get(5,8)); + victim->stun(Random::get(3,5)); checkImprove("bash", true); + } else if(attackType == ATTACK_SLAM) { + if (Random::get(1,10) < 3) { + *this << ColorOn << "^y" << setf(CAP) << victim << " is momentarily stunned by your slam!\n" << ColorOff; + *victim << ColorOn << "^y" << setf(CAP) << this << "'s slam momentarily stunned you!\n" << ColorOff; + victim->stun(Random::get(1,2)); + } + checkImprove("slam", true); } else if(attackType == ATTACK_MAUL) { int dur = 0; if(!pVictim) { @@ -792,10 +840,16 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a setAttackDelay(getAttackDelay() * 2); break; } else if(attackType == ATTACK_BASH) { - *this << "Your bash failed.\n"; + *this << "Your shield bash was ineffective.\n"; checkImprove("bash", false); - *victim << this << " tried to bash you.\n"; - broadcast(getSock(), victim->getSock(), victim->getRoomParent(), "%M tried to bash %N.", this, victim.get()); + *victim << this << " tried to shield bash you.\n"; + broadcast(getSock(), victim->getSock(), victim->getRoomParent(), "%M tried to shield bash %N.", this, victim.get()); + break; + } else if(attackType == ATTACK_SLAM) { + *this << "Your slam was ineffective.\n"; + checkImprove("slam", false); + *victim << this << " tried to slam you.\n"; + broadcast(getSock(), victim->getSock(), victim->getRoomParent(), "%M tried to slam %N.", this, victim.get()); break; } else if(attackType == ATTACK_KICK) { *this << "Your kick was ineffective.\n"; @@ -845,7 +899,7 @@ int Player::attackCreature(const std::shared_ptr &victim, AttackType a } } else if(result == ATTACK_FUMBLE) { statistics.fumble(); - *this << ColorOn << "^gYou FUMBLED your weapon.\n" << ColorOff; + *this << ColorOn << "^gYou FUMBLED " << ((weapon && !weapon->flagIsSet(O_NO_PREFIX))?"your ":" ") << (weapon?weapon->getName():"your attack") << ".\n" << ColorOff; broadcast(getSock(), getRoomParent(), "^g%M fumbled %s weapon.", this, hisHer()); checkWeapon(Containable::downcasted_shared_from_this(), weapon, true, &loc, &attacks, &wielding, multiWeapon); @@ -893,6 +947,19 @@ int Creature::castWeapon(const std::shared_ptr& target, std::shared_pt if(getRoomParent()->flagIsSet(R_NO_MAGIC) || weapon->getMagicpower() < 1) return(0); + if(weapon->getChargesCur() <=0) + return(0); + + if(weapon->getCastChance()) { + short roll = Random::get(1,1000); + + if(roll > weapon->getCastChance()) { + if(isCt()) + *this << ColorOn << "^DWeapon cast chance roll (d1000): " << roll << " (" << weapon->getCastChance() << "/1000)^x\n"; + return(0); + } + } + splno = weapon->getMagicpower() - 1; // Do we have sufficient charges to cast? if(weapon->getChargesCur() <= 0) @@ -1129,7 +1196,29 @@ void Creature::modifyDamage(const std::shared_ptr& enemy, int dmgType, } } - // players take less damage while berserked + if (enemy && enemy.get() != this) { + int num = 1, den = 1; + + // defender (this) takes less damage while berserked + if (isEffected("berserk")) { + if (cClass == CreatureClass::BERSERKER) { num *= 4; den *= 5; } // -20% => 4/5 + else { num *= 6; den *= 7; } // ~-14% => 6/7 + } + + // attacker (enemy) deals more damage while berserking + if (enemy->isEffected("berserk")) { + num *= 3; den *= 2; // +50% => 3/2 + } + + if (num != den) { + int dmg = attackDamage.get(); + dmg = (dmg * num) / den; // single rounding point + attackDamage.set(std::max(1, dmg)); + } + } + +/* + // target takes less damage while berserked if(enemy && isEffected("berserk")) { // zerkers: 1/5 // everyone else: 1/7 @@ -1137,10 +1226,10 @@ void Creature::modifyDamage(const std::shared_ptr& enemy, int dmgType, attackDamage.set(std::max(1, attackDamage.get())); } - // monsters do more damage while berserked - if(enemy && isEffected("berserk")) + // target takes more damage if attacker berserked + if(enemy && enemy->isEffected("berserk")) attackDamage.set(attackDamage.get() * 3 / 2); - +*/ // armor damage reduction if(enemy) { const float damageReduction = enemy->getDamageReduction(Containable::downcasted_shared_from_this()); @@ -1150,6 +1239,12 @@ void Creature::modifyDamage(const std::shared_ptr& enemy, int dmgType, // Werewolf silver vulnerability if(weapon && weapon->isSilver() && isEffected("lycanthropy")) attackDamage.set(attackDamage.get() * 2); + + + // IRON material does extra damage to mtypes FAERIE and DEMON and DEVIL + if(weapon && isMonster() && (type == FAERIE || type == DEMON || type == DEVIL) && + (weapon->getMaterial() == IRON || weapon->getMaterial() == METEORIC_IRON)) + attackDamage.set(attackDamage.get()*115/100); } // if it's a pet, check elemental pRealm resistance diff --git a/combat/combat.cpp b/combat/combat.cpp index 50970bd5..1fdfc7bc 100644 --- a/combat/combat.cpp +++ b/combat/combat.cpp @@ -81,7 +81,6 @@ bool Monster::updateCombat() { std::shared_ptr mTarget=nullptr; char atk[30]; int n, rtn=0, yellchance=0, num=0, breathe=0; - int x=0; bool monstervmonster, casted=false; bool resistPet=false, immunePet=false, vulnPet=false; bool willCast=false, antiMagic=false, isCharmed=false; @@ -352,16 +351,14 @@ bool Monster::updateCombat() { if(flagIsSet(M_DISEASES)) tryToDisease(target); - if( (flagIsSet(M_WOUNDING) && x <= 15) && - (target->getClass() != CreatureClass::LICH) && - !target->isEffected("stoneskin") - ) { + if( (flagIsSet(M_WOUNDING) && Random::get(1,1000) <= target->getWoundingChance())) { if(!target->chkSave(DEA, Containable::downcasted_shared_from_this(),0)) { - target->printColor("^RThe wound is festering and unclean.\n"); - broadcastGroup(false, target, "%M wounds are festering and unclean.\n", target.get()); - broadcast(getSock(), target->getSock(), room, "%M's wounds are festering and unclean.\n", target.get()); - target->addEffect("wounded", -1, 1, Containable::downcasted_shared_from_this(), false, target); + target->printColor("^RThe wound is festering and unclean.^x\n"); + broadcastGroup(false, target, "^R%M's wounds are festering and unclean.^x\n", target.get()); + broadcast(getSock(), target->getSock(), room, "^R%M's wounds are festering and unclean.^x\n", target.get()); + + target->addEffect("wounded", (long)getLevel()/5, getLevel(), Containable::downcasted_shared_from_this(), false, target); } } @@ -388,7 +385,7 @@ bool Monster::updateCombat() { if(flagIsSet(M_WILL_BLIND)) tryToBlind(target); - if(pTarget && flagIsSet(M_DISOLVES_ITEMS) && Random::get(1,100) <= 15) + if(pTarget && hasAcidDissolveAttack() && Random::get(1,100) <= 10) pTarget->dissolveItem(Containable::downcasted_shared_from_this()); if( doDamage(target, attackDamage.get(), CHECK_DIE_ROB, PHYSICAL_DMG, freeTarget) || @@ -882,6 +879,9 @@ bool Creature::chkSave(short savetype, const std::shared_ptr& target, chance -= ( 5*((target->intelligence.getCur()+target->piety.getCur())/2) - ((intelligence.getCur()+piety.getCur())/2)); else chance += (intelligence.getCur()+piety.getCur())/2; + + if(isSimpleMinded()) + chance -= (chance*10)/100; break; case SPL: if(opposing) @@ -893,6 +893,9 @@ bool Creature::chkSave(short savetype, const std::shared_ptr& target, natural = 0; chance += 2500; } + + if(isSimpleMinded()) + chance -= (chance*10)/100; break; case LCK: chance += 100*((saves[POI].chance + saves[DEA].chance + @@ -1274,7 +1277,7 @@ bool Monster::tryToPoison(const std::shared_ptr& target, SpecialAttack if(target->isPoisoned()) return(false); - if(!pAttack && Random::get(1, 100) > 15) + if(!pAttack && Random::get(1, 100) > (target->resistantToPoison()?5:15)) return(false); if(target->immuneToPoison()) { @@ -1298,6 +1301,9 @@ bool Monster::tryToPoison(const std::shared_ptr& target, SpecialAttack duration = (Random::get(2,3)*60) - 12*bonus(target->constitution.getCur()); } + if(target->resistantToPoison()) + duration = (duration*80)/100; + target->poison(isPet() ? getMaster() : Containable::downcasted_shared_from_this(), poison_dmg ? poison_dmg : level, duration); return(true); } else if(pAttack) { @@ -1368,7 +1374,7 @@ bool Monster::tryToDisease(const std::shared_ptr& target, SpecialAttac return(false); if(target->isPlayer() && target->isEffected("stoneskin")) return(false); - if(!pAttack && Random::get(1, 100) > 15) + if(!pAttack && Random::get(1, 100) > (target->resistantToDisease()?5:15)) return(false); if(target->immuneToDisease()) { diff --git a/combat/combatSystem.cpp b/combat/combatSystem.cpp index 5f5a5660..4b7b9c56 100644 --- a/combat/combatSystem.cpp +++ b/combat/combatSystem.cpp @@ -448,11 +448,11 @@ int Player::getRacialWeaponskillBonus(const std::shared_ptr weapon) con rBonus=10; break; case GREYELF: - if (weaponType=="arcane-weapon" || weaponType=="bow") + if (weaponType=="arcane-weapon" || weaponType=="dagger" || weaponType=="staff") rBonus=10; break; case WILDELF: - if (weaponType=="sword" || weaponType=="bow" || weaponType=="spear") + if (weaponType=="dagger" || weaponType=="bow" || weaponType=="spear") rBonus=10; break; case AQUATICELF: @@ -473,7 +473,11 @@ int Player::getRacialWeaponskillBonus(const std::shared_ptr weapon) con break; case OGRE: if (weaponType=="club") - rBonus=10; + rBonus=20; + break; + case OROG: + if(weapon->needsTwoHands()) + rBonus=20; break; case DARKELF: if ( !isPureArcaneCaster() && !isPureDivineCaster() && @@ -526,7 +530,7 @@ int Player::getWeaponSkill(const std::shared_ptr weapon) const { // Bless improves your chance to hit if(isEffected("bless")) - bonus += 10; + bonus += std::max(10,getEffect("bless")->getStrength()); if (weapon) bonus += (getClassWeaponskillBonus(weapon) + getRacialWeaponskillBonus(weapon)); @@ -554,7 +558,8 @@ int Player::getWeaponSkill(const std::shared_ptr weapon) const { //********************************************************************** int Monster::getDefenseSkill() const { - return(defenseSkill); + + return(defenseSkill + getDefenseSkillModifier()); } //********************************************************************** @@ -562,15 +567,20 @@ int Monster::getDefenseSkill() const { //********************************************************************** int Player::getDefenseSkill() const { + + /* int bonus = 0; // Protection makes you harder to hit if(isEffected("protection")) bonus += 10; + if(isEffected("shield")) + bonus += 40; */ + Skill* defenseSkill = getSkill("defense"); if(!defenseSkill) return(-1); else - return(defenseSkill->getGained() + bonus); + return(defenseSkill->getGained() + getDefenseSkillModifier()); } double Creature::getMisschanceModifier(const std::shared_ptr& victim, double& missChance) { @@ -587,6 +597,8 @@ double Creature::getMisschanceModifier(const std::shared_ptr& victim, if(victim->isEffected("blur") && !isEffected("true-sight")) mod += victim->getEffect("blur")->getStrength(); + + if (victim->isEffected("faerie-fire")) mod -= victim->getEffect("faerie-fire")->getStrength(); @@ -1077,6 +1089,27 @@ double Creature::getDodgeChance(const std::shared_ptr& attacker, const default: break; } + + // Racial adjustments! + switch(race) { + case HALFLING: + case KOBOLD: + chance += 4; + break; + case KATARAN: + chance += 3; + break; + case KENKU: + chance += 2; + break; + case OGRE: + chance -= 3; + break; + default: + break; + } + + } else { // Not a player chance = 5.0; @@ -1254,20 +1287,26 @@ bool Creature::canHit(const std::shared_ptr& victim, std::shared_ptrflagIsSet(M_ENCHANTED_WEAPONS_ONLY) || victim->flagIsSet(M_PLUS_TWO) || - victim->flagIsSet(M_PLUS_THREE) + victim->flagIsSet(M_PLUS_THREE) || + victim->flagIsSet(M_PLUS_FOUR) ) { - // At night level 19+ wolves can hit + // At night level 10+ wolves can hit if( isEffected("lycanthropy") && !isDay() && - level > 19 && - victim->flagIsSet(M_ENCHANTED_WEAPONS_ONLY) + ( (level >= 10 && victim->flagIsSet(M_ENCHANTED_WEAPONS_ONLY)) || + (level >= 16 && victim->flagIsSet(M_PLUS_TWO)) || + (level >= 35 && victim->flagIsSet(M_PLUS_THREE)) || + (level >= 50 && victim->flagIsSet(M_PLUS_FOUR)) + ) ) { - if(glow) printColor("^WYour claws glow radiantly in the night against %N.\n", victim.get()); + if(glow) printColor("^RYour claws throw magical red sparks as they strike %N.^x\n", victim.get()); } else if(cClass == CreatureClass::MONK && flagIsSet(P_FOCUSED) && - ( (level >= 16 && victim->flagIsSet(M_ENCHANTED_WEAPONS_ONLY)) || - (level >= 16 && victim->flagIsSet(M_PLUS_TWO)) + ( (level >= 10 && victim->flagIsSet(M_ENCHANTED_WEAPONS_ONLY)) || + (level >= 16 && victim->flagIsSet(M_PLUS_TWO)) || + (level >= 35 && victim->flagIsSet(M_PLUS_THREE)) || + (level >= 50 && victim->flagIsSet(M_PLUS_FOUR)) ) ) { if(glow) *this << ColorOn << "^WYour fists glow with power against " << victim << ".\n" << ColorOff; @@ -1275,7 +1314,8 @@ bool Creature::canHit(const std::shared_ptr& victim, std::shared_ptrflagIsSet(M_ENCHANTED_WEAPONS_ONLY) && enchant > 0) || (victim->flagIsSet(M_PLUS_TWO) && enchant > 1) || - (victim->flagIsSet(M_PLUS_THREE) && enchant > 2) + (victim->flagIsSet(M_PLUS_THREE) && enchant > 2) || + (victim->flagIsSet(M_PLUS_FOUR) && enchant > 3) ) ) { if(glow && weapon) { @@ -1386,12 +1426,31 @@ int Player::computeDamage(std::shared_ptr victim, std::shared_ptrisMartial()) + attackDamage.add((strength.getCur() / 15) + (int)(getSkillLevel("bash") / 4)); + else + attackDamage.add((int)(getSkillLevel("bash") / 5)); + } + else if(attackType == ATTACK_SLAM) { + if(computeBonus) + bonusDamage.set(getBaseDamage()/4); + attackDamage.set(Random::get(2,3)); + if(getAsCreature()->isMartial()) + attackDamage.add((strength.getCur() / 15) + (int)(getSkillLevel("slam") / 5)); + else + attackDamage.add((int)(getSkillLevel("slam") / 6)); + } else { // Any non kick attack for now if(computeBonus) @@ -1407,6 +1466,8 @@ int Player::computeDamage(std::shared_ptr victim, std::shared_ptr victim, std::shared_ptrgetWeaponType() == "bow") attackDamage.set(attackDamage.get() + (attackDamage.get())/4); + + //Half-giants do +10% damage with large weapons, so long as they are as large or larger than the weapon + if(race == HALFGIANT && getSize() >= weapon->getSize() && weapon->getSize() >= SIZE_LARGE) + attackDamage.set(attackDamage.get() + (attackDamage.get())/10); + } if(isEffected("lycanthropy")) @@ -1482,7 +1548,7 @@ int Player::computeDamage(std::shared_ptr victim, std::shared_ptrflagIsSet(O_SEL_MONK)) || (isEffected("lycanthropy") && ( @@ -1502,7 +1568,7 @@ int Player::computeDamage(std::shared_ptr victim, std::shared_ptr 0.0) { attackDamage.set((int) ((float)attackDamage.get() * multiplier)); if(computeBonus) { - if(attackType != ATTACK_BACKSTAB) + if(attackType != ATTACK_BACKSTAB && attackType != ATTACK_SMASH) bonusDamage.set((int)((float)bonusDamage.get() * multiplier)); // else // bonus = static_cast(static_cast(bonus) * (multiplier/2.0)); @@ -1515,21 +1581,33 @@ int Player::computeDamage(std::shared_ptr victim, std::shared_ptr victim, std::shared_ptrflagIsSet(O_ALWAYS_CRITICAL) && !weapon->flagIsSet(O_NEVER_SHATTER)) { + broadcastGroup(false, victim, "^g%M made a CRITICAL %s!\n", this, atk); + if( attackType != ATTACK_KICK && attackType != ATTACK_GORE && attackType != ATTACK_SLAM && attackType != ATTACK_BASH && weapon && !isDm() && weapon->flagIsSet(O_ALWAYS_CRITICAL) && !weapon->flagIsSet(O_NEVER_SHATTER)) { printColor("^YYour %s shatters.\n", weapon->getCName()); broadcast(getSock(), getRoomParent(),"^Y%s %s shattered.", upHisHer(), weapon->getCName()); retVal = 1; @@ -1611,7 +1689,7 @@ int Monster::computeDamage(std::shared_ptr victim, std::shared_ptr)nullptr, getRoomParent(), "%M made a critical hit.", this); + broadcast((std::shared_ptr)nullptr, getRoomParent(), "%M made a CRITICAL HIT!", this); int mult = Random::get(2, 5); attackDamage.set(attackDamage.get() * mult); drain *= mult; diff --git a/combat/die.cpp b/combat/die.cpp index 25935ad4..a5d74bd5 100644 --- a/combat/die.cpp +++ b/combat/die.cpp @@ -67,6 +67,7 @@ #include "xml.hpp" // for loadObject + class Property; //******************************************************************** @@ -223,10 +224,22 @@ void Monster::dropCorpse(const std::shared_ptr& killer) { } checkDarkness(); + if(!destroy) { if(!coins.isZero()) { loadObject(MONEY_OBJ, object); - object->value.set(coins); + + Money tempCoins = coins; + + // Human players get +10% to gold drops, but not if their pet did the killing! + // Can re-adjust this later if we add the rest of the coins -TC + if(player && player->getRace()==HUMAN && tempCoins[GOLD]>0 && !killer->isPet()) { + unsigned long goldBonus = (tempCoins[GOLD]*10)/100; + goldBonus = std::max(1,goldBonus); + tempCoins.add(goldBonus,GOLD); + } + + object->value.set(tempCoins); object->setDroppedBy(Containable::downcasted_shared_from_this(), "MobDeath"); @@ -832,7 +845,7 @@ int Player::clanKill(const std::shared_ptr& killer) { // Checks for doctor killers void Creature::checkDoctorKill(const std::shared_ptr& victim) { - if(victim->getName() == "doctor") { + if(victim->flagIsSet(M_BENEVOLENT_SPELLCASTER)) { if( (isPlayer() && !isStaff()) || (isMonster() && isPet() && !getMaster()->isStaff())) { @@ -840,7 +853,7 @@ void Creature::checkDoctorKill(const std::shared_ptr& victim) { target->setFlag(P_DOCTOR_KILLER); if(!target->flagIsSet(P_DOCTOR_KILLER)) - broadcast("### %s is a doctor this!", target->getCName()); + broadcast("### %s is a doctor killer!", target->getCName()); target->lasttime[LT_KILL_DOCTOR].ltime = time(nullptr); target->lasttime[LT_KILL_DOCTOR].interval = 72000L; } @@ -1532,6 +1545,7 @@ void Monster::distributeExperience(const std::shared_ptr&killer) { } } + //******************************************************************** // adjustExperience //******************************************************************** @@ -1550,9 +1564,13 @@ void Creature::adjustExperience(const std::shared_ptr& victim, int& ex return; } - if(player->getRace() == HUMAN && expAmount) - expAmount += std::max(Random::get(4,6),expAmount/3/10); + //Adjust xp amount based on any racial or class XP modifiers + int xpmod = player->getXPModifiers(); + + if(xpmod) + expAmount += std::max(Random::get(1,2), (expAmount * (100 + xpmod)) / 100); + //Multi-class receives exp penalty based on their level range if(player->hasSecondClass()) { // Penalty is 12.5% at level 30 and above if(player->level >= 30) @@ -1560,9 +1578,6 @@ void Creature::adjustExperience(const std::shared_ptr& victim, int& ex else // and 25% below 30 expAmount = (expAmount*3)/4; } -// // All experience is multiplied by 3/4 for a multi-classed player -// if(player->hasSecondClass()) -// expAmount = (expAmount*3)/4; int levelDiff = abs((int)player->getLevel() - (int)victim->getLevel()); float multiplier=1.0; @@ -1585,7 +1600,6 @@ void Creature::adjustExperience(const std::shared_ptr& victim, int& ex else multiplier = 0.10; if(multiplier < 1.0) { -// player->printColor("^YExp Adjustment: %d%% (%d level difference) %d -> %d\n", (int)(multiplier*100), levelDiff, expAmount, (int)(expAmount*multiplier)); expAmount = (int)(expAmount * multiplier); expAmount = std::max(1, expAmount); } diff --git a/combat/singers.cpp b/combat/singers.cpp index 219afd17..08c26e04 100644 --- a/combat/singers.cpp +++ b/combat/singers.cpp @@ -827,6 +827,7 @@ int cmdCharm(const std::shared_ptr& player, cmd* cmnd) { return(0); } creature->printColor("^m%M tried to charm you.\n", player.get()); + player->lasttime[LT_HYPNOTIZE].interval = 30L; return(0); } @@ -835,6 +836,7 @@ int cmdCharm(const std::shared_ptr& player, cmd* cmnd) { player->printColor("^yYour charm failed!\n"); creature->print("Your mind tingles as you brush off %N's charm.\n", player.get()); player->checkImprove("charm", false); + player->lasttime[LT_HYPNOTIZE].interval = 30L; return(0); } } diff --git a/combat/specials.cpp b/combat/specials.cpp index 1cad618a..6cdebf25 100644 --- a/combat/specials.cpp +++ b/combat/specials.cpp @@ -371,12 +371,12 @@ bool Creature::doSpecial(SpecialAttack &attack, const std::shared_ptr& // SPECIAL-realm matches up with the corresponding Realm so we can just // pass that, after casting it to a Realm attackDamage.set(victim->checkRealmResist(attackDamage.get(), (Realm)attack.type)); + + //Trolls take +20% damage from fire special attacks + if(attack.type == SPECIAL_FIRE && victim->getRace() == TROLL) + attackDamage.set((attackDamage.get()*120)/100); } - // TODO: Check for warmth/heat-protection on elemental attacks -// if( pVictim && ( -// (realm == COLD && (pVictim->isEffected("warmth") || pVictim->isEffected("alwayscold"))) || -// (realm == FIRE && pVictim->isEffected("heat-protection") || pVictim->isEffected("alwayswarm")) -// ) ) + } if(attack.flagIsSet(SA_EARTH_SHIELD_REDUCE)) @@ -665,6 +665,7 @@ int dmSpecials(const std::shared_ptr& player, cmd* cmnd) { player->print(" petrifying-gaze petrifying-breath\n"); player->print(" confusing-gaze zap-mana\n"); player->print(" death-gaze gore\n"); + player->print(" smash\n"); return(0); } else { player->printColor("Added special ^W%s^x to ^W%M^x.\n", attack->getName().c_str(), target.get()); @@ -887,6 +888,30 @@ SpecialAttack* Creature::addSpecial(std::string_view specialName) { return &specials.emplace_back(attack); + } else if(specialName == "smash") { + SpecialAttack attack; + attack.name = "Smash"; + attack.verb = "smashed"; + + attack.type = SPECIAL_WEAPON; + attack.targetStr = "^Y*ATTACKER* SMASHED you for ^W*DAMAGE*^Y damage.^x"; + attack.roomStr = "^Y*ATTACKER* SMASHED *LOW-TARGET*.^x"; + attack.targetSaveStr = attack.targetFailStr = "^Y*ATTACKER* tried to SMASH you.^x"; + attack.roomSaveStr = attack.roomFailStr = "^Y*ATTACKER* tried to SMASH *LOW-TARGET*!^x"; + attack.saveType = SAVE_DEXTERITY; + attack.chance = 101; // Always goes off + attack.delay = 20; + + attack.setFlag(SA_SINGLE_TARGET); + attack.setFlag(SA_CHECK_DIE_ROB); + attack.setFlag(SA_UNDEAD_WARD_REDUCE); + attack.setFlag(SA_SAVE_NO_DAMAGE); + attack.damage.setNumber(level); + attack.damage.setSides(3); + attack.damage.setPlus((isMartial() ? ((strength.getCur()/25)+(level/4)) : level/6) + 1); + + return &specials.emplace_back(attack); + } else if(specialName == "bite") { SpecialAttack attack; if(isEffected("vampirism")) { diff --git a/combat/undead.cpp b/combat/undead.cpp index 7321e786..34f7b44f 100644 --- a/combat/undead.cpp +++ b/combat/undead.cpp @@ -382,10 +382,12 @@ int cmdHypnotize(const std::shared_ptr& player, cmd* cmnd) { target->getAsMonster()->addEnemy(player); if(player->flagIsSet(P_LAG_PROTECTION_SET)) { // Activates lag protection. player->setFlag(P_LAG_PROTECTION_ACTIVE); - } + } return(0); } target->printColor("^m%M tried to hypnotize you.\n", player.get()); + //Less time to recover for failed hypnotize attempt + player->lasttime[LT_HYPNOTIZE].interval = 30L; return(0); } @@ -400,6 +402,7 @@ int cmdHypnotize(const std::shared_ptr& player, cmd* cmnd) { player->print("%M avoided your hypnotizing gaze.\n", target.get()); player->checkImprove("hypnotize", false); target->print("You avoided %s's hypnotizing gaze.\n", player->getCName()); + player->lasttime[LT_HYPNOTIZE].interval = 30L; return(0); } } diff --git a/combat/warriors.cpp b/combat/warriors.cpp index f21451a1..b311660e 100644 --- a/combat/warriors.cpp +++ b/combat/warriors.cpp @@ -45,6 +45,7 @@ #include "track.hpp" // for Track #include "unique.hpp" // for Unique #include "wanderInfo.hpp" // for WanderInfo + #include "commands.hpp" // for isPtester() //********************************************************************* @@ -662,28 +663,25 @@ int cmdCircle(const std::shared_ptr& player, cmd* cmnd) { int cmdBash(const std::shared_ptr& player, cmd* cmnd) { std::shared_ptr creature; std::shared_ptr pCreature=nullptr; - long t = time(nullptr); + long i=0, t = time(nullptr); int chance; double level; if(!player->ableToDoCommand()) return(0); + if(!player->knowsSkill("bash")) { - player->print("You lack the skills to effectively bash anything!\n"); + *player << "You lack the skills to effectively shield bash enemies.\n"; return(0); } - if(!player->isCt()) { - if(!player->ready[WIELD-1] && player->getSize() < SIZE_LARGE) { - player->print("You are too small to bash without a weapon.\n"); - return(0); - } - if(player->getPrimaryWeaponCategory() == "ranged") { - player->print("You can't use a ranged weapon to bash someone!\n"); - return(0); - } + + if(!player->isCt() && !player->ready[SHIELD-1]) { + *player << "Shield bashing enemies requires equipping a shield.\n"; + return(0); } - if(!(creature = player->findVictim(cmnd, 1, true, false, "Bash whom?\n", "You don't see that here.\n"))) + + if(!(creature = player->findVictim(cmnd, 1, true, false, "Shield bash whom?\n", "You don't see that here.\n"))) return(0); if(creature) @@ -692,28 +690,86 @@ int cmdBash(const std::shared_ptr& player, cmd* cmnd) { if(!player->canAttack(creature)) return(0); - if(!player->isCt()) { - if(!pCreature) { - if(creature->getAsMonster()->isEnemy(player)) { - player->print("Not while you're already fighting %s.\n", creature->himHer()); - return(0); - } + // Certain mtypes cannot be smashed, and colossal and gargantuan mobs cannot ever be smashed + if (creature->isMonster()) { + switch(creature->getType()) { + case ETHEREAL: + case ENERGY: + case GASEOUS: + case INSECT: + case SLIME: + case PUDDING: + *player << ColorOn << "^y" << "Creatures of type '" << monType::getName(creature->getType()) << "' cannot be shield bashed!\n" << ColorOff; + return(0); + break; + default: + break; + } + + if(creature->getSize() == SIZE_COLOSSAL || creature->getSize() == SIZE_GARGANTUAN) { + *player << "Are you kidding? " << setf(CAP) << creature << " is of " << + getSizeName(creature->getSize()) << " size! You can't shield bash " << creature->himHer() << "!\n"; + return(0); } } - if(!player->checkAttackTimer(true)) + i = LT(player, LT_BASH); + t = time(nullptr); + + if(i > t && !player->isCt()) { + player->pleaseWait(i - t); return(0); + } + level = player->getSkillLevel("bash"); + if(player->getClass() == CreatureClass::CLERIC && player->getSecondClass() == CreatureClass::FIGHTER) + level = std::max(1, (int)level-2); + else if(player->getClass() == CreatureClass::CLERIC && player->getDeity() == ARES) + level = std::max(1, (int)level-3); + else if((player->getClass() == CreatureClass::FIGHTER && player->getSecondClass() != CreatureClass::NONE) || player->getClass() == CreatureClass::PALADIN) + level = std::max(1, (int)level-1); + + long bashDelay = 0; + if (level <= 40) + bashDelay = 13L; + else if (level <= 30) + bashDelay = 14L; + else if (level <= 20) + bashDelay = 15L; + else if (level <= 10) + bashDelay = 16L; + player->updateAttackTimer(); - player->lasttime[LT_KICK].ltime = t; - player->lasttime[LT_KICK].interval = (player->getPrimaryDelay()/10); - player->lasttime[LT_GORE].ltime = t; - player->lasttime[LT_GORE].interval = (player->getPrimaryDelay()/10); + player->lasttime[LT_BASH].ltime = t; + player->lasttime[LT_BASH].interval = bashDelay; - if(player->getClass() == CreatureClass::CLERIC && player->getDeity() == ARES) { - player->lasttime[LT_SPELL].ltime = t; + i = LT(player, LT_SPELL); + if (t >= i) player->lasttime[LT_SPELL].interval = 3L; + + if(player->knowsSkill("slam")) { + i = LT(player, LT_SLAM); + if (t >= i) + player->lasttime[LT_SLAM].interval = player->getPrimaryDelay()/10; } + + if(player->knowsSkill("gore")) { + i = LT(player, LT_GORE); + if (t >= i) + player->lasttime[LT_GORE].interval = player->getPrimaryDelay()/10; + } + + if(player->knowsSkill("smash")) { + i = LT(player, LT_SMASH); + if (t >= i) + player->lasttime[LT_SMASH].interval = 3L; + } + + //player->lasttime[LT_KICK].ltime = t; + //player->lasttime[LT_KICK].interval = (player->getPrimaryDelay()/10); + //player->lasttime[LT_GORE].ltime = t; + //player->lasttime[LT_GORE].interval = (player->getPrimaryDelay()/10); + // All the logic to check if a weapon can hit the target is inside the attackCreature function // so only put anything specific to bash here, or check for ATTACK_BASH in attackCreature @@ -721,20 +777,196 @@ int cmdBash(const std::shared_ptr& player, cmd* cmnd) { player->smashInvis(); player->interruptDelayedActions(); + chance = 50 + (int)((level-creature->getLevel())*10) + + bonus(player->strength.getCur()) * 3 + + (bonus(player->dexterity.getCur()) - bonus(creature->dexterity.getCur())) * 2; + chance += player->getClass() == CreatureClass::BERSERKER ? 10:0; + + chance = player->getClass() == CreatureClass::BERSERKER ? std::min(90, chance) : std::min(85, chance); + + if(player->isBlind()) + chance = std::min(20, chance); + if(creature->isMonster()) { creature->getAsMonster()->addEnemy(player); + + if(player->flagIsSet(P_LAG_PROTECTION_SET)) // Activates Lag protection. + player->setFlag(P_LAG_PROTECTION_ACTIVE); + + if (player->ready[SHIELD-1]->flagIsSet(O_ALWAYS_CRITICAL) && creature->flagIsSet(M_NO_AUTO_CRIT)) + chance = 0; // automatic miss if autocrit is set on shield. + + if (creature->flagIsSet(M_NO_BASH)) + chance = 0; + + // A shield bash, regardless of outcome, adds 2.5% of target's max hp to threat + creature->getAsMonster()->adjustThreat(player, std::max((long)(creature->hp.getMax()*0.025), 2)); + } - if(player->flagIsSet(P_LAG_PROTECTION_SET)) // Activates Lag protection. - player->setFlag(P_LAG_PROTECTION_ACTIVE); + if(player->isCt()) chance = 101; + // For bash we have a bash chance, and then a normal attack miss chance + if(Random::get(1,100) <= chance) { + // We made the bash check, do the attack + player->attackCreature(creature, ATTACK_BASH); - level = player->getSkillLevel("bash"); + } + else { + player->print("Your shield bash had no effect.\n"); + player->checkImprove("bash", false); + creature->print("%M tried to shield bash you.\n", player.get()); + broadcast(player->getSock(), creature->getSock(), creature->getRoomParent(), + "%M tried to shield bash %N.", player.get(), creature.get()); + } - if(player->getClass() == CreatureClass::CLERIC && player->getSecondClass() == CreatureClass::FIGHTER) - level = std::max(1, (int)level-2); - else if(player->getClass() == CreatureClass::CLERIC && player->getDeity() == ARES) - level = std::max(1, (int)level-3); + return(0); + +} +//********************************************************************* +// cmdSlam +//********************************************************************* +// This function allows a player to "slam" an opponent, doing a bit of +// extra damage, and possibly stunning them for 1-2 seconds. It is essentially +// like a pommel strike with a weapon, so the weapon subtype used is relevent. + +int cmdSlam(const std::shared_ptr& player, cmd* cmnd) { + std::shared_ptr creature; + long i=0, t = time(nullptr); + int chance; + double level; + bool noSlam = false; + + if(!player->ableToDoCommand()) + return(0); + + + std::shared_ptr weapon = player->ready[WIELD-1]; + std::string category = player->getPrimaryWeaponCategory(); + + if (weapon) { + noSlam = (category == "ranged"); + } + + if(!player->isCt()) { + + if(!player->knowsSkill("slam")) { + *player << "You lack the skills to effectively slam with a weapon.\n"; + return(0); + } + + if(!weapon) { + *player << "Slamming an opponent requires a wielded primary weapon.\n"; + return(0); + + } + else if ((noSlam && !weapon->flagIsSet(O_CAN_USE_SLAM)) || weapon->flagIsSet(O_NO_SLAM) ) { + *player << ColorOn << "You cannot slam an opponent with " << weapon << ".\n" << ColorOff; + return(0); + } + + } + + + if(!(creature = player->findVictim(cmnd, 1, true, false, "Slam whom?\n", "You don't see that here.\n"))) + return(0); + + if(!player->canAttack(creature)) + return(0); + + // Certain mtypes cannot be slammed, and colossal and gargantuan mobs cannot ever be slammed + if (creature->isMonster()) { + switch(creature->getType()) { + case ETHEREAL: + case ENERGY: + case GASEOUS: + case INSECT: + case SLIME: + case PUDDING: + *player << ColorOn << "^y" << "Creatures of type '" << monType::getName(creature->getType()) << "' cannot be slammed!\n" << ColorOff; + return(0); + break; + default: + break; + } + + if(creature->getSize() == SIZE_COLOSSAL || creature->getSize() == SIZE_GARGANTUAN) { + *player << "Are you kidding? " << setf(CAP) << creature << " is of " << + getSizeName(creature->getSize()) << " size! You can't slam " << creature->himHer() << "!\n"; + return(0); + } + } + + i = LT(player, LT_SLAM); + t = time(nullptr); + + if(i > t && !player->isCt()) { + player->pleaseWait(i - t); + return(0); + } + + level = player->getSkillLevel("slam"); + + switch (player->getClass()) { + case CreatureClass::FIGHTER: + if(player->getSecondClass() != CreatureClass::NONE) + level = std::max(1, (int)level-1); + break; + case CreatureClass::PALADIN: + level = std::max(1, (int)level-1); + break; + case CreatureClass::CLERIC: + if(player->getDeity() == ARES || player->getDeity() == LINOTHAN) + level = std::max(1, (int)level-2); + break; + case CreatureClass::THIEF: + case CreatureClass::ASSASSIN: + case CreatureClass::ROGUE: + level = std::max(1, (int)level-3); + break; + default: + break; + } + + long slamDelay = 0; + if (level <= 40) + slamDelay = 9L; + else if (level <= 30) + slamDelay = 10L; + else if (level <= 20) + slamDelay = 11L; + else if (level <= 10) + slamDelay = 12L; + + if(player->getClass() != CreatureClass::FIGHTER && player->getClass() != CreatureClass::BERSERKER) + slamDelay += 2L; + + player->updateAttackTimer(); + player->lasttime[LT_SLAM].ltime = t; + player->lasttime[LT_SLAM].interval = slamDelay; + + i = LT(player, LT_SPELL); + if (t >= i) + player->lasttime[LT_SPELL].interval = 3L; + + if(player->knowsSkill("gore")) { + i = LT(player, LT_GORE); + if (t >= i) + player->lasttime[LT_GORE].interval = player->getPrimaryDelay()/10; + } + + if(player->knowsSkill("smash")) { + i = LT(player, LT_SMASH); + if (t >= i) + player->lasttime[LT_SMASH].interval = 3L; + } + + // All the logic to check if a weapon can hit the target is inside the attackCreature function + // so only put anything specific to bash here, or check for ATTACK_BASH in attackCreature + + player->unhide(); + player->smashInvis(); + player->interruptDelayedActions(); chance = 50 + (int)((level-creature->getLevel())*10) + bonus(player->strength.getCur()) * 3 + @@ -746,22 +978,35 @@ int cmdBash(const std::shared_ptr& player, cmd* cmnd) { if(player->isBlind()) chance = std::min(20, chance); - if(creature->isMonster() && (player->ready[WIELD-1] && - player->ready[WIELD-1]->flagIsSet(O_ALWAYS_CRITICAL)) && creature->flagIsSet(M_NO_AUTO_CRIT)) - chance = 0; // automatic miss with autocrit weapon. + if(creature->isMonster()) { + creature->getAsMonster()->addEnemy(player); + + if(player->flagIsSet(P_LAG_PROTECTION_SET)) // Activates Lag protection. + player->setFlag(P_LAG_PROTECTION_ACTIVE); + + if (player->ready[WIELD-1]->flagIsSet(O_ALWAYS_CRITICAL) && creature->flagIsSet(M_NO_AUTO_CRIT)) + chance = 0; // automatic miss if autocrit is set on weapon. + + if (creature->flagIsSet(M_NO_SLAM)) + chance = 0; + + } if(player->isCt()) chance = 101; - // For bash we have a bash chance, and then a normal attack miss chance + // For slam we have a slam chance, and then a normal attack miss chance if(Random::get(1,100) <= chance) { - // We made the bash check, do the attack - player->attackCreature(creature, ATTACK_BASH); + // We made the slam check, do the attack + player->attackCreature(creature, ATTACK_SLAM); + // A successful slam , regardless of whether it does damge, adds 1% of target's max hp to threat + creature->getAsMonster()->adjustThreat(player, std::max((long)(creature->hp.getMax()*0.01), 2)); + } else { - player->print("Your bash failed.\n"); - player->checkImprove("bash", false); - creature->print("%M tried to bash you.\n", player.get()); + player->print("Your slam was ineffective.\n"); + player->checkImprove("slam", false); + creature->print("%M tried to slam you.\n", player.get()); broadcast(player->getSock(), creature->getSock(), creature->getRoomParent(), - "%M tried to bash %N.", player.get(), creature.get()); + "%M tried to slam %N.", player.get(), creature.get()); } return(0); @@ -778,13 +1023,14 @@ int cmdBash(const std::shared_ptr& player, cmd* cmnd) { int cmdGore(const std::shared_ptr& player, cmd* cmnd) { std::shared_ptr creature; - long lt_gore, lt_kick, t; + long i=0, t=0; int chance; if(!player->ableToDoCommand()) return(0); + if(!player->isStaff()) { if(!player->knowsSkill("gore")) { *player << "You don't know how to gore your opponent.\n"; @@ -798,18 +1044,19 @@ int cmdGore(const std::shared_ptr& player, cmd* cmnd) { if(!player->canAttack(creature)) return(0); + //player->updateAttackTimer(); - lt_gore = LT(player, LT_GORE); + i = LT(player, LT_GORE); t = time(nullptr); - if(lt_gore > t && !player->isDm()) { - player->pleaseWait(lt_gore - t); + if(i > t && !player->isCt()) { + player->pleaseWait(i - t); return(0); } // Gore long goreInterval = 0; - int goreSkill = player->getSkillLevel("gore"); + int goreSkill = (int)player->getSkillLevel("gore"); // Time between use decreases with skill level if (goreSkill >= 40) @@ -829,28 +1076,40 @@ int cmdGore(const std::shared_ptr& player, cmd* cmnd) { player->lasttime[LT_GORE].ltime = t; player->lasttime[LT_GORE].interval = goreInterval; + - // No gore-cast. Not going to smash your head into something and then cast after. That'd just be dumb. - player->lasttime[LT_SPELL].ltime = t; + i = LT(player, LT_SPELL); + if (t >= i) + player->lasttime[LT_SPELL].interval = 3L; + + if(player->knowsSkill("slam")) { + i = LT(player, LT_SLAM); + if (t >= i) + player->lasttime[LT_SLAM].interval = player->getPrimaryDelay()/10; + } - //For now, we're not going to be doing gore-kick combos if(player->knowsSkill("kick")) { - lt_kick = LT(player, LT_KICK); - if (lt_kick < t) { - player->lasttime[LT_KICK].ltime = t; - player->lasttime[LT_KICK].interval = std::max(lt_kick - t, 3); - } + i = LT(player, LT_KICK); + if (t >= i) + player->lasttime[LT_KICK].interval = player->getPrimaryDelay()/10; + } + + if(player->knowsSkill("smash")) { + i = LT(player, LT_SMASH); + if (t >= i) + player->lasttime[LT_SMASH].interval = player->getPrimaryDelay()/10; } player->unhide(); player->smashInvis(); player->interruptDelayedActions(); - if(player->flagIsSet(P_LAG_PROTECTION_SET)) // Activates Lag protection. - player->setFlag(P_LAG_PROTECTION_ACTIVE); - if(creature->isMonster()) { creature->getAsMonster()->addEnemy(player); + + if(player->flagIsSet(P_LAG_PROTECTION_SET)) // Activates Lag protection. + player->setFlag(P_LAG_PROTECTION_ACTIVE); + } // Agility = avg of dex + con @@ -910,6 +1169,489 @@ int cmdGore(const std::shared_ptr& player, cmd* cmnd) { return(0); } +//****************************************************************************************** +// cmdSmash +//****************************************************************************************** +// This allows some larger races to smash their opponents every so often, either with or without +// a weapon. It has a chance to stun, and size differences with attacker, target, and the +// weapon used also are considered. A smash attack is immune to parrying, +// turning those into either a miss or a dodge instead. The chance to block an attack is +// still there, but difference between attacker and target size are considered. A successful +// smash has a chance to stun, but that also has bonus/penalty depending on size difference. +// As of v2.63, only ogres and half-giants have this ability. -TC + +int cmdSmash(const std::shared_ptr& player, cmd* cmnd) { + std::shared_ptr pTarget=nullptr; + std::shared_ptr target=nullptr; + int n=0; + int squish=0, dur=0, dmg=0; + long i=0, t=0; + float smashMod = 0.0; + Damage damage; + + if(!player->ableToDoCommand()) + return(0); + + if(!player->isStaff()) { + if(!player->knowsSkill("smash")) { + *player << "You do not have the ability to smash your enemies.\n"; + return(0); + } + + if(!player->flagIsSet(P_PTEST_SMASH)) { + *player << "The smash ability is only available for ptesters right now.\n"; + return(0); + } + } + + + + std::shared_ptr weapon = player->ready[WIELD - 1]; + std::string category = player->getPrimaryWeaponCategory(); + + + if(!player->isCt()) { + if(weapon && category != "crushing" && !weapon->flagIsSet(O_CAN_USE_SMASH) && !player->isStaff()) { + *player << setf(CAP) << weapon << " is not suitable for smashing! You need a crushing weapon or no weapon at all!\n"; + return(0); + } + } + + int weaponSizeDiff = 0; + + // Size of weapon being used compared to size of attacker matters + if(weapon) { + + weaponSizeDiff = (weapon->getSize()?(player->getSize() - weapon->getSize()):0); + + if(abs(weaponSizeDiff) >= 2 && !player->isStaff()) { + *player << ColorOn << "^y" << setf(CAP) << weapon << " is too " + << (weaponSizeDiff>0?"small and awkward":"large and unwieldy") << " for you to smash with.^x\n" << ColorOff; + return(0); + } + + if (weapon->flagIsSet(O_NO_SMASH) && !player->checkStaff("%O cannot be used to smash.\n", weapon.get())) + return(0); + } + + if(!(target = player->findVictim(cmnd, 1, true, false, "Smash what?\n", "You don't see that here.\n"))) + return(0); + + if(!player->canAttack(target)) + return(0); + + // Certain mtypes cannot be smashed, and colossal and gargantuan mobs cannot ever be smashed + if (target->isMonster()) { + switch(target->getType()) { + case ETHEREAL: + case ENERGY: + case GASEOUS: + case INSECT: + case SLIME: + case PUDDING: + *player << ColorOn << "^y" << "Creatures of type '" << monType::getName(target->getType()) << "' cannot be smashed!\n" << ColorOff; + return(0); + break; + default: + break; + } + + if(target->getSize() == SIZE_COLOSSAL || target->getSize() == SIZE_GARGANTUAN) { + *player << "Are you kidding? " << setf(CAP) << target << " is of " << + getSizeName(target->getSize()) << " size! You can't smash " << target->himHer() << "!\n"; + return(0); + } + } + + player->updateAttackTimer(); + + i = LT(player, LT_SMASH); + t = time(nullptr); + + if(i > t && !player->isCt()) { + player->pleaseWait(i - t); + return(0); + } + + long smashInterval = 0; + int smashSkill = (int)player->getSkillLevel("smash"); + + // Time between smashes will decrease slightly with increased skill level + if (smashSkill >= 40) + smashInterval = 155; + else if (smashSkill >= 30) + smashInterval = 160; + else if (smashSkill >= 20) + smashInterval = 165; + else if (smashSkill >= 10) + smashInterval = 170; + else + smashInterval = 180; + + if(player->isStaff() || player->flagIsSet(P_PTESTER)) + smashInterval = 3; + + player->lasttime[LT_SMASH].ltime = t; + player->lasttime[LT_SMASH].interval = smashInterval; + + + i = LT(player, LT_SPELL); + if (t >= i) + player->lasttime[LT_SPELL].interval = 6L; + + if(player->knowsSkill("slam")) { + i = LT(player, LT_SLAM); + if (t >= i) + player->lasttime[LT_SLAM].interval = 6L; + } + + if(player->knowsSkill("bash")) { + i = LT(player, LT_BASH); + if (t >= i) + player->lasttime[LT_BASH].interval = 6L; + } + + if(player->knowsSkill("kick")) { + i = LT(player, LT_KICK); + if (t >= i) + player->lasttime[LT_KICK].interval = 6L; + } + + if(player->knowsSkill("gore")) { + i = LT(player, LT_GORE); + if (t >= i) + player->lasttime[LT_GORE].interval = 6L; + } + + + if(target->isMonster()) { + target->getAsMonster()->addEnemy(player); + + if(player->flagIsSet(P_LAG_PROTECTION_SET)) // Activates Lag protection. + player->setFlag(P_LAG_PROTECTION_ACTIVE); + } + + // If using a weapon, allow for the weapon breaking + if(weapon && player->breakObject(weapon, WIELD)) { + *player << ColorOn << "^yYour SMASH missed!\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "^y%M's SMASH missed!^x", player.get()); + player->lasttime[LT_SMASH].ltime = t; + player->lasttime[LT_SMASH].interval = 12L; + return(0); + } + + int skillLevel=0, wpnSkill=0, modifier=0; + + wpnSkill = player->getWeaponSkill(weapon)/10; + + skillLevel = (smashSkill + wpnSkill)/2; + + if(player->flagIsSet(P_PTESTER) || player->flagIsSet(P_PTEST_SMASH)) + *player << ColorOn << "^DwpnSkill: " << wpnSkill << "\nsmashSkill: " << smashSkill << "\nInitial skillLevel: " << skillLevel << "\n" << ColorOff; + + //High brutality (avg of str + con) gives a bonus + if(player->getBrutality() > 200) { + //modifier = 300 - player->getBrutality()/10; + modifier = (player->getBrutality() - 200)/10; + skillLevel += modifier; + } + //Low brutality (avg of str + con) gives a penalty + else if (player->getBrutality() < 100) { + modifier = (100 - player->getBrutality()) / 10; + skillLevel = std::max(1,skillLevel - modifier); + } + + // Now modify by level, since we dont use computeBonus for damage. + if (player->getLevel() > target->getLevel()) + skillLevel += player->getLevel() - target->getLevel(); + + if(player->flagIsSet(P_PTESTER) || player->flagIsSet(P_PTEST_SMASH)) + *player << ColorOn << "^DBrutality: " << player->getBrutality() << "\nModifier: " << modifier << "\nModified skillLevel: " << skillLevel << "\n" << ColorOff; + + // Much harder to smash opponents more than 1 size larger than you are, effectively reducing skill level + short sizeDiff = target->getSize() - player->getSize(); + if (sizeDiff > 1) + skillLevel -= (skillLevel*(25*sizeDiff))/100; + + //Using a wrong-sized weapon will have consequences also + if(weapon && abs(weaponSizeDiff) > 0) + skillLevel -= (skillLevel*10)/100; + + AttackResult result = player->getAttackResult(target, weapon, (weapon?NO_FLAG:NO_FUMBLE), skillLevel); + + // On occasion, mobtype or size is not enough to customize, so some mobs can be flagged so they're not able to be smashed. + // If that happens, the attack will be converted into a miss. + if(!pTarget && target->flagIsSet(M_NO_SMASH) && result != ATTACK_MISS) + result = ATTACK_MISS; + + if(player->isStaff() || player->flagIsSet(P_PTESTER) || player->flagIsSet(P_PTEST_SMASH)) { + *player << ColorOn << "^DskillLevel = " << skillLevel << "\n"; + if (result == ATTACK_MISS) + *player << "result = ATTACK_MISS\n"; + else if (result == ATTACK_HIT) + *player << "result = ATTACK_HIT\n"; + else if (result == ATTACK_PARRY) + *player << "result = ATTACK_PARRY\n"; + else if (result == ATTACK_BLOCK) + *player << "result = ATTACK_BLOCK\n"; + else if (result == ATTACK_GLANCING) + *player << "result = ATTACK_GLANCING\n"; + else + *player << "result = other\n"; + *player << "^x" << ColorOff; + } + + player->smashInvis(); + player->unhide(); + player->interruptDelayedActions(); + + std::string with = Statistics::damageWith(player, weapon); + + if(player->isDm() && result != ATTACK_CRITICAL) + result = ATTACK_HIT; + + player->statistics.swing(); + + // A blocked attack can turn into a hit depending on size difference between attacker and target + if (result == ATTACK_BLOCK && target->getSize() < player->getSize()) { + *player << ColorOn << "^y" << setf(CAP) << target << " was unable to block your smash!\n" << ColorOff; + *target << ColorOn << "^yYou were unable to block " << player << " 's SMASH!\n" << ColorOff; + result = ATTACK_HIT; + } + // An ATTACK_PARRY result will turn into a dodge if target has high enough elusiveness (dex,int average), otherwise, normal hit + else if (result == ATTACK_PARRY) { + *player << ColorOn << "^y" << setf(CAP) << target << " tried to parry, but " << target->heShe() << " failed!\n" << ColorOff; + *target << ColorOn << "^yYou tried to parry " << player << " 's SMASH, but you failed!\n" << ColorOff; + + if (Random::get(1,5) == 1 && target->getElusiveness() >= 160) + result = ATTACK_DODGE; + else + result = ATTACK_HIT; + } + + + if(result == ATTACK_HIT || result == ATTACK_CRITICAL || result == ATTACK_BLOCK || result == ATTACK_GLANCING) { + + // Smash does base damage generally 1.5x-3.0x either what attacker's unarmed attack + // damage is (no weapon), or damage of the wielded weapon if a weapon is being used + smashMod = Random::get(1.5,3.0); + + int drain = 0; + bool wasKilled = false, meKilled = false; + // Return of 1 means the weapon was shattered or otherwise rendered unsuable + if(player->computeDamage(target, weapon, ATTACK_SMASH, result, damage, false, drain, smashMod) == 1) { + player->unequip(WIELD, UNEQUIP_DELETE); + weapon = nullptr; + player->computeAttackPower(); + } + damage.includeBonus(); + + + n = damage.get(); + + if(result == ATTACK_CRITICAL) + player->statistics.critical(); + else + player->statistics.hit(); + + if(target->isPlayer()) + target->getAsPlayer()->statistics.wasHit(); + + + if(target->isMonster()) { + // A successful smash hit, gives 10% of the target's max health as threat + target->getAsMonster()->adjustThreat(player, std::max((long)(target->hp.getMax()*0.1), 2)); + } + + + squish = target->hp.getCur() * 2; + + *player << ColorOn << "^YYou SMASHED " << target << " for " << player->customColorize("*CC:DAMAGE*") << damage.get() << "^Y damage!^x\n" << ColorOff; + + log_immort(false,player, "%s SMASHED %s for %d damage.\n", player->getCName(), target->getCName(), damage.get()); + + broadcast(player->getSock(), player->getParent(), "^Y%M SMASHED %M!^x", player.get(), target.get()); + + *target << ColorOn << "^Y" << player << " SMASHED you" << + (target->isBrittle() ? "r brittle body" : "") << " for " << + target->customColorize("*CC:DAMAGE*") << damage.get() << "^Y damage!^x\n" << ColorOff; + + broadcastGroup(false, target, "^Y%M SMASHED %N for *CC:DAMAGE*%d^Y damage!, %s%s^x\n", + player.get(), target.get(), damage.get(), target->heShe(), target->getStatusStr(damage.get())); + + player->statistics.attackDamage(damage.get(), with); + + + if( weapon && weapon->getMagicpower() && + weapon->flagIsSet(O_WEAPON_CASTS) && + Random::get(1,100) <= 10 && weapon->getChargesCur() > 0) + { + n += player->castWeapon(target, weapon, meKilled); + } + + meKilled = player->doReflectionDamage(damage, target) || meKilled; + + player->doDamage(target, damage.get(), NO_CHECK); + wasKilled = target->hp.getCur() < 1; + + // If killed, check for squishy squish special output! + if(wasKilled && n > squish && Random::get(1,100) <= 50) { + switch(Random::get(1,4)) { + case 1: + *player << ColorOn << "^RYou completely pulverized " << target << "!\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "%M completely pulverized %N!",player.get(), target.get()); + if(target->isPlayer()) + *target << ColorOn << "^R" << setf(CAP) << player << " completely pulverized you! You're dead!\n" << ColorOff; + break; + case 2: + *player << ColorOn << "^RYou battered " << target << " into a pulpy unrecognizeable mess!\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "^R%M battered %N into a pulpy unrecognizeable mess!^x",player.get(), target.get()); + if(target->isPlayer()) + *target << ColorOn << "^R" << setf(CAP) << player << " battered you into a pulpy unrecognizeable mess! You're dead!\n" << ColorOff; + + break; + case 3: + if(weapon) { + *player << ColorOn << "^R" << setf(CAP) << weapon << " ^Rsmashed " << target << " into mush!\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "^R%M's %s smashed %N into mush!^x", player.get(), weapon->getCName(), target.get()); + + if(target->isPlayer()) + *target << ColorOn << "^R" << setf(CAP) << player << "'s " << weapon << " ^Rsmashed you into mush! You're dead!\n" << ColorOff; + } + else + { + *player << ColorOn << "^RYou smashed " << target << " into mush!\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "^R%M smashed %N into mush!^x", + player.get(), target.get()); + + if(target->isPlayer()) + *target << ColorOn << "^R" << setf(CAP) << player << " smashed you into mush! You're dead!\n" << ColorOff; + } + + break; + case 4: + *player << ColorOn << "^RYou nearly flattened " << target << " into the ground!\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "^R%M nearly flattened %N into the ground!^x",player.get(), target.get()); + if(target->isPlayer()) + *target << ColorOn << "^R" << setf(CAP) << player << " nearly flattened you into the ground! You're dead!\n" << ColorOff; + break; + } + } + + player->checkImprove("smash", true); + + if(weapon && Random::get(0,1)) + weapon->decShotsCur(); + Creature::simultaneousDeath(player, target, false, false); + + // Stun chance here + if(!wasKilled && !meKilled && (result == ATTACK_HIT || result == ATTACK_CRITICAL || result == ATTACK_GLANCING)) { + int stunChance = (result == ATTACK_GLANCING?200:400); // base 20% glancing, 40% otherwise + + if(sizeDiff > 0) + stunChance += (sizeDiff * 100); + + if(result == ATTACK_CRITICAL) + stunChance += 500; + + if(target->isEffected("berserk")) + stunChance /= 3; + + if(player->isCt()) + stunChance = 1001; + + int roll = Random::get(1,1000); + if(isPtester(player) || player->flagIsSet(P_PTEST_SMASH)) + *player << ColorOn << "^DstunChance (1-1000): " << stunChance << "\nRoll: " << roll << "\n" << ColorOff; + + if(roll <= stunChance) { + target->stun((result==ATTACK_GLANCING?Random::get(3,4):Random::get(6,8))); + *player << ColorOn << "^Y" << setf(CAP) << target << " is knocked senseless!\n" << ColorOff; + *target << ColorOn << "^YYou've been knocked senseless!\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "^Y%M is knocked senseless!^x", target.get()); + } + + } + + // Only monsters flee, and not when their attacker was killed + if(!wasKilled && !meKilled && target->getAsMonster()) { + if(target->flee() == 2) + return(0); + } + + } else if(result == ATTACK_MISS) { + player->statistics.miss(); + if(target->isPlayer()) + target->getAsPlayer()->statistics.wasMissed(); + *player << ColorOn << "^yYour SMASH missed!\n" << ColorOff; + player->checkImprove("smash", false); + broadcast(player->getSock(), player->getParent(), "^y%M tried to SMASH %M!^x", player.get(), target.get()); + player->setAttackDelay(Random::get(3,7)); + + } else if(result == ATTACK_DODGE) { + *player << ColorOn << "^yYour SMASH missed!\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "^y%M tried to SMASH %M!^x", player.get(), target.get()); + target->dodge(player); + } else if(result == ATTACK_FUMBLE) { + player->statistics.fumble(); + *player << ColorOn << "^gYou FUMBLED " << ((weapon && !weapon->flagIsSet(O_NO_PREFIX))?"your ":" ") << (weapon?weapon->getName():"your attack") << ".\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "^g%M fumbled %s weapon.", player.get(), player->hisHer()); + + //While envenom usually requires piercing or slashing weapons, and smash is only for crushing or specifically + //flagged smashing weapons, the below check needs to be here for completeness in case envenom is ever changed, + //or if certain exotic or slashing/piercing weapons are flagged as able to use smash with. + if(weapon->flagIsSet(O_ENVENOMED)) { + if(!player->immuneToPoison() && + !player->chkSave(POI, player, -5) && !induel(player, target->getAsPlayer()) + ) { + *player << ColorOn << "^G^#You poisoned yourself!!\n" << ColorOff; + broadcast(player->getSock(), player->getParent(), "%M poisoned %sself!!", + player.get(), player->himHer()); + + if(weapon->getEffectStrength()) { + dmg = (Random::get(1,3) + (weapon->getEffectStrength()/10)); + player->hp.decrease(dmg); + *player << ColorOn << "^gYou take ^G" << dmg << "^g damage as the poison takes effect!\n" << ColorOff; + } + + weapon->clearFlag(O_ENVENOMED); + + if(player->hp.getCur() < 1) { + n = 0; + player->addObj(weapon); + weapon = nullptr; + player->computeAttackPower(); + player->die(POISON_PLAYER); + return(0); + } + + dur = standardPoisonDuration(weapon->getEffectDuration(), player->constitution.getCur()); + player->poison(player, weapon->getEffectStrength(), dur); + + } else { + *player << "You almost poisoned yourself!\n"; + broadcast(player->getSock(), player->getParent(), "%M almost poisoned %sself!", player.get(), player->himHer()); + } + } + + n = 0; + player->addObj(weapon); + player->ready[WIELD-1] = nullptr; + weapon = nullptr; + player->computeAttackPower(); + } else { + *player << ColorOn << "^GError: Unhandled attack result! Result: " << result << "\n" << ColorOff; + } + + // On miss, dodge, or fumble (if weapon was used) reset timer to 9s instead of the normal smashInterval based on skill + // Makes smash able to be used more often to help level the skill up + if(result == ATTACK_MISS || result == ATTACK_DODGE || result == ATTACK_FUMBLE) { + player->lasttime[LT_SMASH].ltime = t; + player->lasttime[LT_SMASH].interval = 9L; + } + + return(0); +} //********************************************************************* // cmdKick @@ -919,7 +1661,7 @@ int cmdGore(const std::shared_ptr& player, cmd* cmnd) { int cmdKick(const std::shared_ptr& player, cmd* cmnd) { std::shared_ptr creature; - long lt_kick,lt_gore,t; + long i=0,t=0; int chance; @@ -941,11 +1683,11 @@ int cmdKick(const std::shared_ptr& player, cmd* cmnd) { return(0); - lt_kick = LT(player, LT_KICK); + i = LT(player, LT_KICK); t = time(nullptr); - if(lt_kick > t && !player->isDm()) { - player->pleaseWait(lt_kick - t); + if(i > t && !player->isDm()) { + player->pleaseWait(i - t); return(0); } @@ -962,13 +1704,32 @@ int cmdKick(const std::shared_ptr& player, cmd* cmnd) { player->lasttime[LT_DISARM].interval = 6; } - //For now, we're not going to be doing kick-gore combos + i = LT(player, LT_SPELL); + if (t >= i) + player->lasttime[LT_SPELL].interval = 3L; + + if(player->knowsSkill("slam")) { + i = LT(player, LT_SLAM); + if (t >= i) + player->lasttime[LT_SLAM].interval = player->getPrimaryDelay()/10; + } + if(player->knowsSkill("gore")) { - lt_gore = LT(player, LT_GORE); - if (lt_gore < t) { - player->lasttime[LT_GORE].ltime = t; - player->lasttime[LT_GORE].interval = std::max(lt_gore - t, 3); - } + i = LT(player, LT_GORE); + if (t >= i) + player->lasttime[LT_GORE].interval = player->getPrimaryDelay()/10; + } + + if(player->knowsSkill("smash")) { + i = LT(player, LT_SMASH); + if (t >= i) + player->lasttime[LT_SMASH].interval = player->getPrimaryDelay()/10; + } + + if(player->knowsSkill("bash")) { + i = LT(player, LT_BASH); + if (t >= i) + player->lasttime[LT_BASH].interval = player->getPrimaryDelay()/10; } player->unhide(); diff --git a/combat/weaponless.cpp b/combat/weaponless.cpp index 892c7a70..bb46fceb 100644 --- a/combat/weaponless.cpp +++ b/combat/weaponless.cpp @@ -294,8 +294,8 @@ int cmdFocus(const std::shared_ptr& player, cmd* cmnd) { i = player->lasttime[LT_FOCUS].ltime; t = time(nullptr); - if(t - i < 600L) { - player->pleaseWait(600L-t+i); + if(t - i < 360L) { + player->pleaseWait(360L-t+i); return(0); } chance = std::min(80, (int)(player->getSkillLevel("focus") * 20) + bonus(player->piety.getCur())); @@ -321,7 +321,7 @@ int cmdFocus(const std::shared_ptr& player, cmd* cmnd) { player->checkImprove("focus", false); broadcast(player->getSock(), player->getParent(), "%M tried to focus %s energy.", player.get(), player->hisHer()); - player->lasttime[LT_FOCUS].ltime = t - 590L; + player->lasttime[LT_FOCUS].ltime = t - 360L; } return(0); @@ -467,13 +467,6 @@ int cmdMaul(const std::shared_ptr& player, cmd* cmnd) { player->unhide(); player->smashInvis(); - //Wwolves can only wield claw weapons now...ok to maul with them. - TC -/* if(player->ready[WIELD - 1]) { - player->print("How can you do that with your claws full?\n"); - return(0); - } -*/ - player->lasttime[LT_MAUL].ltime = t; if(creature->isMonster()) player->lasttime[LT_MAUL].interval = 15L; @@ -508,7 +501,10 @@ int cmdMaul(const std::shared_ptr& player, cmd* cmnd) { if(player->isBlind()) chance = std::min(20, chance); - if((creature->flagIsSet(M_ENCHANTED_WEAPONS_ONLY) || creature->flagIsSet(M_PLUS_TWO) || creature->flagIsSet(M_PLUS_THREE)) && (not_initial==1)) { + if(( (creature->flagIsSet(M_ENCHANTED_WEAPONS_ONLY) && (int)level < 10) || + (creature->flagIsSet(M_PLUS_TWO) && (int)level < 16) || + (creature->flagIsSet(M_PLUS_THREE) && (int)level < 35) || + (creature->flagIsSet(M_PLUS_FOUR) && (int)level < 50)) && (not_initial==1)) { chance /= 2; chance = std::min(chance, 50); } @@ -516,15 +512,13 @@ int cmdMaul(const std::shared_ptr& player, cmd* cmnd) { if(player->isDm()) chance = 101; - if(Random::get(1, 100) > player->getLuck() + (int)(level * 2)) - chance = 5; - if(Random::get(1, 100) <= chance) { player->attackCreature(creature, ATTACK_MAUL); + /* TODO: re-enable/re-consider this for later if(!induel(player, pCreature)) { - // 5% chance to get lycanthropy when mauled by a werewolf creature->addLycanthropy(player, 5); } + */ } else { player->print("You failed to maul %N.\n", creature.get()); player->checkImprove("maul", false); diff --git a/commands/cmd.cpp b/commands/cmd.cpp index 50001f8f..8f1f79e2 100644 --- a/commands/cmd.cpp +++ b/commands/cmd.cpp @@ -1,4 +1,4 @@ -/* + /* * cmd.cpp * Handle player/pet input commands. * ____ _ @@ -636,10 +636,12 @@ bool Config::initCommands() { playerCommands.emplace("save", 100, cmdSave, nullptr, "Save your player"); playerCommands.emplace("time", 100, cmdTime, nullptr, "Show the current time"); playerCommands.emplace("circle", 50, cmdCircle, nullptr, "Circle an opponent"); - playerCommands.emplace("bash", 50, cmdBash, nullptr, "Bash an opponent"); + playerCommands.emplace("bash", 50, cmdBash, nullptr, "Shield bash an opponent"); + playerCommands.emplace("slam", 50, cmdSlam, nullptr, "Slam an opponent"); playerCommands.emplace("barkskin", 100, cmdBarkskin, nullptr, "Use barkskin ability"); playerCommands.emplace("kick", 100, cmdKick, nullptr, "Kick an opponent"); playerCommands.emplace("gore", 100, cmdGore, nullptr, "Gore an opponent (Minotaur only)"); + playerCommands.emplace("smash", 100, cmdSmash, nullptr, "SMASH an opponent (selective large races only)"); playerCommands.emplace("gamestat", 100, infoGamestat, nullptr, "Game time statistics"); playerCommands.emplace("list", 100, cmdList, nullptr, "Show items for sale"); @@ -705,7 +707,8 @@ bool Config::initCommands() { playerCommands.emplace("charm", 100, cmdCharm, nullptr, "Use bard charm"); playerCommands.emplace("identify", 100, cmdIdentify, nullptr, "Use bard identify"); playerCommands.emplace("songs", 100, cmdSongs, nullptr, "List your known bard songs"); - playerCommands.emplace("levitate", 100, innateLevitate, nullptr, "Call on innate ability to levitate"); + playerCommands.emplace("levitate", 100, cmdInnateLevitate, nullptr, "Call on innate levitation"); + playerCommands.emplace("invisible", 100, cmdInnateLevitate, nullptr, "Call on innate invisibility"); playerCommands.emplace("enthrall", 100, cmdEnthrall, nullptr, "Attempt to enthrall undead"); playerCommands.emplace("meditate", 100, cmdMeditate, nullptr, "Use monk meditate"); @@ -750,7 +753,8 @@ bool Config::initCommands() { playerCommands.emplace("keep", 100, cmdKeep, nullptr, "Prevent accidentially throwing away an item"); playerCommands.emplace("unkeep", 100, cmdUnkeep, nullptr, "Unkeep an item"); playerCommands.emplace("label", 100, cmdLabel, nullptr, "Set a custom label on an item"); - playerCommands.emplace("alignment", 100, cmdChooseAlignment, nullptr, "Choose your alignment"); + playerCommands.emplace("choosealignment", 100, cmdChooseAlignment, nullptr, "Choose your alignment"); + playerCommands.emplace("alignment", 100, cmdAlignment, nullptr, "Check your alignment or a target's alignment"); playerCommands.emplace("push", 100, cmdPush, nullptr, "Push an object"); playerCommands.emplace("pull", 100, cmdPull, nullptr, "Pull an object"); playerCommands.emplace("press", 100, cmdPress, nullptr, "Press a rune"); diff --git a/commands/command2.cpp b/commands/command2.cpp index b22efe28..cacb29cc 100644 --- a/commands/command2.cpp +++ b/commands/command2.cpp @@ -1347,6 +1347,9 @@ int cmdBreak(const std::shared_ptr& player, cmd* cmnd) { if(player->immuneToPoison()) chance = 101; + if(player->resistantToPoison()) + chance /= 2; + if(Random::get(1,100) <= chance) { player->printColor("^r^#You accidentally poisoned yourself!\n"); broadcast(player->getSock(), player->getParent(), "%M accidentally poisoned %sself!", diff --git a/commands/command5.cpp b/commands/command5.cpp index 345e2bee..e002f6fe 100644 --- a/commands/command5.cpp +++ b/commands/command5.cpp @@ -56,6 +56,7 @@ #include "stats.hpp" // for Stat #include "structs.hpp" // for StatsContainer #include "web.hpp" // for updateRecentActivity, webUnass... +#include "levelGain.hpp" // for statStr[] //********************************************************************* // who @@ -646,6 +647,21 @@ int cmdQuit(const std::shared_ptr& player, cmd* cmnd) { } +std::string getFullStatName(int stat, bool cap) { + std::string fullStatName[7] = { "noStat", "strength", "dexterity", "constitution", "intelligence", "piety", "charisma"}; + + if (stat < 0 || stat >= 7) + return fullStatName[0]; + + std::string statName = fullStatName[stat]; + + if (cap) { + statName[0] = std::toupper(statName[0]); // Capitalize first letter + } + + return statName; +} + //********************************************************************* // changeStats //********************************************************************* @@ -688,12 +704,13 @@ void Player::changingStats(std::string str) { std::vector statInput; std::shared_ptr sock = getSock(); std::shared_ptr player = sock->getPlayer(); + short statAdjustment=0; switch(sock->getState()) { case CON_CHANGING_STATS: // empty input would cause crash here if (str.length() < 1) { - sock->print("Aborted.\n"); + sock->print("Aborting. Please try changestats again.\n"); sock->setState(CON_PLAYING); return; } @@ -703,29 +720,45 @@ void Player::changingStats(std::string str) { std::transform(inputArgs.begin(), inputArgs.end(), std::back_inserter(statInput), [](const std::string &s) { return std::stoi(s); }); } catch (std::invalid_argument &) { sock->print("Invalid input\n"); - sock->print("Aborted.\n"); + sock->print("Aborting. Please try changestats again.\n"); sock->setState(CON_PLAYING); return; } if(statInput.size() < 5) { sock->print("Please enter all 5 numbers.\n"); - sock->print("Aborted.\n"); + sock->print("Aborting. Please try changestats again.\n"); sock->setState(CON_PLAYING); return; } if (std::any_of(statInput.begin(), statInput.end(), [](int i){ return i < 3 || i > 18; })){ sock->print("No stats < 3 or > 18 please.\n"); - sock->print("Aborted.\n"); + sock->print("Aborting. Please try changestats again.\n"); sock->setState(CON_PLAYING); return; } + //Validate that no initially chosen stats would be adjusted below 1 due to possible large racial adjustments of -3 or less + //This is also checked during initial character creation + for(int x=0; x<5; x++) { + statAdjustment = gConfig->getRace(player->getRace())->getStatAdj(x+1); + if(((statInput[x]*10)+statAdjustment) < 10) { + sock->printColor("\nThe initial stat adjustments for your race (^W%s^x) are: %s\n", + gConfig->getRace(player->getRace())->getName().c_str(),getRacialBonusesString(player->getRace()).c_str()); + sock->printColor("After adjustments for race ^W%s^x are applied to those initial stats, the value of %s would be adjusted to 0.\n", + gConfig->getRace(player->getRace())->getName().c_str(), getFullStatName(x+1).c_str()); + sock->printColor("The initially chosen value for %s cannot be less than %d.\n", getFullStatName(x+1).c_str(), abs(statAdjustment/10)+1); + sock->print("Aborting. Please try changestats again.\n"); + sock->setState(CON_PLAYING); + return; + } + } + sum = std::accumulate(statInput.begin(), statInput.end(), 0); if(sum != 56) { sock->print("Stat total must equal 56 points.\n"); - sock->print("Aborted.\n"); + sock->print("Aborting. Please try changestats again.\n"); sock->setState(CON_PLAYING); return; } @@ -808,7 +841,7 @@ void Player::changingStats(std::string str) { ); broadcast(::isCt,"^y### %s aborted choosing new stats.", getCName()); - sock->print("Aborted.\n"); + sock->print("Changestats aborted.\n"); sock->setState(CON_PLAYING); return; } @@ -1025,9 +1058,9 @@ int cmdTime(const std::shared_ptr& player, cmd* cmnd) { if(pet->isMonster() && pet->isPet()) { i = 1; if(pet->isUndead()) - *player << "Time left before creature following you leaves/fades: " << (timestr(pet->lasttime[LT_ANIMATE].ltime+pet->lasttime[LT_ANIMATE].interval-t)) << "\n"; + *player << "Time left before creature following you leaves/fades: " << (timestr(std::max(0,(pet->lasttime[LT_ANIMATE].ltime+pet->lasttime[LT_ANIMATE].interval-t)))) << "\n"; else - *player << "Time left before creature following you leaves/fades: " << (timestr(pet->lasttime[LT_INVOKE].ltime+pet->lasttime[LT_INVOKE].interval-t)) << "\n"; + *player << "Time left before creature following you leaves/fades: " << (timestr(std::max(0,(pet->lasttime[LT_INVOKE].ltime+pet->lasttime[LT_INVOKE].interval-t)))) << "\n"; } } diff --git a/commands/communication.cpp b/commands/communication.cpp index eb0b7aa6..6a0215be 100644 --- a/commands/communication.cpp +++ b/commands/communication.cpp @@ -143,6 +143,8 @@ channelInfo channelList[] = { }; + + //********************************************************************* // confusionChar //********************************************************************* @@ -659,10 +661,9 @@ int communicate(const std::shared_ptr& creature, cmd* cmnd) { strcpy(speak, "recite"); else if(chan->type == COM_GT) strcpy(speak, "group mentioned"); - else if(chan->type == COM_YELL) { + else if(chan->type == COM_YELL) strcpy(speak, "yell"); - text += "!"; - } else + else strcpy(speak, get_language_verb(lang)); @@ -712,8 +713,12 @@ int communicate(const std::shared_ptr& creature, cmd* cmnd) { else sprintf(intro, "You %s in %s,", speak, get_language_adj(lang)); - creature->printColor("%s%s \"%s%s\".\n^x", ((!chan->ooc && creature->flagIsSet(P_LANGUAGE_COLORS)) ? get_lang_color(lang) : ""), - intro, ooc_str, text.c_str()); + // Append punctuation if needed, but not if already there + if (text.back() != '.' && text.back() != '?' && text.back() != '!') + text += (chan->type == COM_YELL) ? "!" : "."; + + creature->printColor("%s%s \"%s%s\"\n^x", + ((!chan->ooc && creature->flagIsSet(P_LANGUAGE_COLORS)) ? get_lang_color(lang) : ""), intro, ooc_str, text.c_str()); } @@ -1120,288 +1125,124 @@ void sendGlobalComm(const std::shared_ptr player, const std::string &tex } } +const std::map languageMap = { + {"abyssal", LABYSSAL}, + {"arcanic", LARCANIC}, + {"schnai", LBARBARIAN}, + {"barbarian", LBARBARIAN}, + {"brownie", LBROWNIE}, + {"bugbear", LBUGBEAR}, + {"cambion", LABYSSAL}, + {"celestial", LCELESTIAL}, + {"seraph", LCELESTIAL}, + {"centaur", LCENTAUR}, + {"common", LCOMMON}, + {"dark-elf", LDARKELVEN}, + {"deepgnome", LSVIRFNEBLIN}, + {"demonic", LABYSSAL}, + {"devil", LINFERNAL}, + {"drow", LDARKELVEN}, + {"draconic", LDRACONIC}, + {"druidic", LDRUIDIC}, + {"duergar", LDUERGAR}, + {"dwarvish", LDWARVEN}, + {"elvish", LELVEN}, + {"elf", LELVEN}, + {"fey", LFEY}, + {"firbolg", LFIRBOLG}, + {"giantkin",LGIANTKIN}, + {"greydwarf",LDUERGAR}, + {"half-giant", LGIANTKIN}, + {"gith", LGITH}, + {"gnoll", LGNOLL}, + {"gnomish", LGNOMISH}, + {"goblinoid", LGOBLINOID}, + {"grugach", LGRUGACH}, + {"halfling", LHALFLING}, + {"hobgoblin", LHOBGOBLIN}, + {"infernal", LINFERNAL}, + {"kataran", LKATARAN}, + {"kenku", LKENKU}, + {"kobold", LKOBOLD}, + {"leprechaun", LLEPRECHAUN}, + {"lizardman", LLIZARDMAN}, + {"minotaur", LMINOTAUR}, + {"modron", LMODRON}, + {"necril", LNECRIL}, + {"noctis", LNOCTIS}, + {"ogrish", LOGRISH}, + {"orcish", LORCISH}, + {"pixie", LPIXIE}, + {"primordial", LPRIMORDIAL}, + {"quickling", LQUICKLING}, + {"satyr", LSATYR}, + {"sphinx", LSPHINX}, + {"svirfneblin", LSVIRFNEBLIN}, + {"sylvan", LSYLVAN}, + {"thieves' cant", LTHIEFCANT}, + {"thri-kreen", LTHRIKREEN}, + {"tiefling", LTIEFLING}, + {"trollish", LTROLL}, + {"undercommon", LUNDERCOMMON}, + {"wild-elf", LGRUGACH}, + {"wolfen", LWOLFEN} + }; + //********************************************************************* // cmdSpeak //********************************************************************* int cmdSpeak(const std::shared_ptr& player, cmd* cmnd) { - int lang=0; - - if(!player->ableToDoCommand()) + if (!player->ableToDoCommand()) return(0); - if(cmnd->num < 2 ) { - player->print("Speak what?\n"); + if (cmnd->num < 2) { + *player << "Speak in what language?\n"; return(0); } - lowercize(cmnd->str[1],0); + std::string inputLang = toLower(cmnd->str[1]); - switch (cmnd->str[1][0]) { + //We want a string of at least 2 characters for the language + if (inputLang.length() < 2) { + *player << "Speak in what language?\n"; + return(0); + } - case 'a': - switch (cmnd->str[1][1]) { - case 'b': - lang = LABYSSAL; - break; - case 'l': - lang = 0; - break; - case 'r': - lang = LARCANIC; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'b': - switch (cmnd->str[1][1]) { - case 'a': - lang = LBARBARIAN; - break; - case 'r': - lang = LBROWNIE; - break; - case 'u': - lang = LBUGBEAR; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'c': - switch (cmnd->str[1][1]) { - case 'a': - lang = LINFERNAL; - break; - case 'e': - switch (cmnd->str[1][2]) { - case 'l': - lang = LCELESTIAL; - break; - case 'n': - lang = LCENTAUR; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'o': - lang = LCOMMON; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'd': - switch (cmnd->str[1][1]) { - case 'a': - lang = LDARKELVEN; - break; - case 'r': - lang = LDRUIDIC; - break; - case 'u': - lang = LDUERGAR; - break; - case 'w': - lang = LDWARVEN; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'e': - lang = LELVEN; - break; - case 'f': - switch (cmnd->str[1][1]) { - case 'e': - lang = LFEY; - break; - case 'i': - lang = LFIRBOLG; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'g': - switch (cmnd->str[1][1]) { - case 'i': - lang = LGIANTKIN; - break; - case 'n': - switch (cmnd->str[1][2]) { - case 'o': - switch (cmnd->str[1][3]) { - case 'l': - lang = LGNOLL; - break; - case 'm': - lang = LGNOMISH; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'o': - lang = LGOBLINOID; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'h': - switch (cmnd->str[1][1]) { - case 'o': - lang = LHOBGOBLIN; - break; - case 'u': - lang = LCOMMON; - break; - default: - switch (cmnd->str[1][4]) { - case 'e': - player->print("Half elves have no language of their own.\n"); - player->print("They speak the elven or common tongues.\n"); - return(0); - break; - case 'o': - player->print("Half orcs have no language of their own.\n"); - player->print("They speak the orcish or common tongues.\n"); - return(0); - break; - case 'l': - lang = LHALFLING; - break; - case 'g': - lang = LGIANTKIN; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - } - break; - case 'i': - lang = LINFERNAL; - break; - case 'k': - switch (cmnd->str[1][1]) { - case 'a': - lang = LKATARAN; - break; - case 'e': - lang = LKENKU; - break; - case 'o': - lang = LKOBOLD; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'l': - lang = LLIZARDMAN; - break; - case 'm': - lang = LMINOTAUR; - break; - case 'o': - switch (cmnd->str[1][1]) { - case 'g': - lang = LOGRISH; - break; - case 'r': - lang = LORCISH; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; - } - break; - case 'q': - lang = LQUICKLING; - break; - case 's': - switch (cmnd->str[1][1]) { - case 'a': - lang = LSATYR; - break; - case 'e': - lang = LCELESTIAL; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; + std::vector> matches; + + // Find all languages that start with the input + for (const auto& pair : languageMap) { + if (toLower(pair.first).find(inputLang) == 0) { + matches.push_back(pair); } - break; - case 't': - switch (cmnd->str[1][1]) { - case 'i': - lang = LTIEFLING; - break; - case 'r': - lang = LTROLL; - break; - case 'h': - lang = LTHIEFCANT; - break; - default: - player->print("You do not know that language.\n"); - return(0); - break; + } + + if (matches.empty()) { + player->print("Nobody knows that language.\n"); + return(0); + } else if (matches.size() > 1) { + *player << "Language is not unique. Did you mean one of these?\n"; + for (const auto& match : matches) { + *player << " - " << match.first << "\n"; } - break; - case 'w': - lang = LWOLFEN; - break; - default: - player->print("You do not know that language.\n"); return(0); - break; } - if(lang == player->current_language) { - player->print("You're already speaking %s!\n", get_language_adj(lang)); + // At this point, we have exactly one match + const auto& [langName, lang] = matches.front(); + + if (lang == player->current_language) { + *player << "You're already speaking " << get_lang_color(lang) << get_language_adj(lang) << "^x.\n"; return(0); } - if(!player->isEffected("tongues") && !player->languageIsKnown(LUNKNOWN+lang) && !player->isStaff()) { - player->print("You do not know how to speak %s.\n", get_language_adj(lang)); + if (!player->isEffected("tongues") && !player->languageIsKnown(lang) && !player->isStaff()) { + *player << "You do not know how to speak " << get_lang_color(lang) << get_language_adj(lang) << "^x.\n"; return(0); } else { - player->print("You will now speak in %s.\n", get_language_adj(lang)); + *player << "You will now speak in " << get_lang_color(lang) << get_language_adj(lang) << "^x.\n"; player->current_language = lang; } diff --git a/creatures/afflictions.cpp b/creatures/afflictions.cpp index 7b749450..f8fa8de5 100644 --- a/creatures/afflictions.cpp +++ b/creatures/afflictions.cpp @@ -331,6 +331,74 @@ bool Creature::immuneToPoison() const { return(false); } +bool Creature::resistantToPoison() const { + + if(getRace()==DWARF || getRace()==GOBLIN || getRace()==OGRE || + getRace()==OROG || getRace()==DUERGAR || getRace() == KOBOLD || + getRace() == ORC || getRace() == HALFORC) + return(true); + + return(false); +} + +int Creature::getWoundingChance() { + int woundingChance = 250; // default base; percentage: 1d1000 roll + + // Conditions for no wounding + if(getClass() == CreatureClass::LICH || // Liches immune! + isEffected("stoneskin") || isEffected("armor") || isEffected("regeneration") || // These effects prevent wounding + isEffected("wounding")) // If already wounded, do not wound again! + return(0); + + //Race based resistance + switch(getRace()) { + case TROLL: + woundingChance = 20; + break; + case OGRE: + case MINOTAUR: + woundingChance = 100; + break; + case OROG: + woundingChance = 120; + break; + case HALFGIANT: + case HALFORC: + case ORC: + case DUERGAR: + case KATARAN: + woundingChance = 150; + break; + default: + break; + } + + //Beat up means more receptable + if(hp.getCur() < hp.getMax()/4) + woundingChance *= 2; + + return(woundingChance); +} + + + +bool Creature::resistantToDisease() const { + + if(getRace()==GOBLIN || getRace()==OGRE || getRace()==OROG || + getRace() == KOBOLD || getRace() == ORC || + getRace() == HALFORC || getRace() == TROLL) + return(true); + + return(false); +} + +bool Creature::isSimpleMinded() const { + + if(getRace() == OGRE || getRace()==OROG) + return(true); + + return(false); +} //********************************************************************* // isPoisoned //********************************************************************* diff --git a/creatures/alignment.cpp b/creatures/alignment.cpp index f24160d8..94ae11e8 100644 --- a/creatures/alignment.cpp +++ b/creatures/alignment.cpp @@ -263,13 +263,57 @@ std::string Creature::alignString() const { } } +int cmdAlignment(const std::shared_ptr& player, cmd* cmnd) { + std::shared_ptr target=nullptr; + int alignValue = 0; + std::string alignValString; + + std::string syntax = "Syntax: alignment\n" + " alignment (player|monster)\n"; + + + if(!player->ableToDoCommand()) + return(0); + + if(!player->isCt() && !player->isEffected("know-aura") && player->getClass() != CreatureClass::PALADIN) { + *player << "You must be under a know-aura spell to use the alignment command.\n"; + return(0); + } + + if(cmnd->num < 2) { + alignValue = (int)player->getAlignment(); + alignValString = (player->isEffected("empathy") || player->isCt())?(" (" + std::to_string(alignValue) + ")"):""; + + *player << ColorOn << "Your alignment is currently: " << player->alignColor() << player->alignString() << ColorOff << alignValString << ".\n"; + + } else { + + cmnd->str[1][0] = up(cmnd->str[1][0]); + target = player->getParent()->findCreature(player, cmnd->str[1], cmnd->val[1], false); + + if(!target) { + *player << "You don't see that here.\n\n"; + *player << syntax; + return(0); + } + + alignValue = (int)target->getAlignment(); + alignValString = (player->isEffected("empathy") || player->isCt())?(" (" + std::to_string(alignValue) + ")"):""; + + *player << ColorOn << setf(CAP) << target << "'s alignment is: " << target->alignColor() << target->alignString() << ColorOff << alignValString << ".\n"; + + } + + return(0); +} + //******************************************************************** // cmdChooseAlignment //******************************************************************** int cmdChooseAlignment(const std::shared_ptr& player, cmd* cmnd) { - char syntax[] = "Syntax: alignment lawful\n" - " alignment chaotic\n" + char syntax[] = "Syntax: choosealignment lawful\n" + " choosealignment chaotic\n" "Note: Tieflings must be chaotic.\n\n"; if(player->isStaff() && !player->isCt()) @@ -452,7 +496,9 @@ bool antiGradius(int race) { race == OGRE || race == GOBLIN || race == TROLL || - race == KOBOLD ); + race == KOBOLD || + race == DUERGAR || + race == OROG); } @@ -461,11 +507,11 @@ bool antiGradius(int race) { //******************************************************************** void Player::adjustAlignment(std::shared_ptr victim) { - auto adjust = (short)(victim->getAlignment() / 8); + auto adjust = (short)(victim->getAlignment() / 10); - if(victim->getAlignment() < 0 && victim->getAlignment() > -8) + if(victim->getAlignment() < 0 && victim->getAlignment() > -10) adjust = -1; - if(victim->getAlignment() > 0 && victim->getAlignment() < 8) + if(victim->getAlignment() > 0 && victim->getAlignment() < 10) adjust = 1; // bool toNeutral = ((alignment > 0 && adjust > 0) || @@ -481,7 +527,7 @@ void Player::adjustAlignment(std::shared_ptr victim) { adjust=0; // no alignment change alignment -= adjust; - alignment = std::max(-1000, std::min(1000, alignment)); + alignment = std::max(MIN_ALIGN, std::min(MAX_ALIGN, alignment)); } //********************************************************************* diff --git a/creatures/creature.cpp b/creatures/creature.cpp index a07cd685..289528c3 100644 --- a/creatures/creature.cpp +++ b/creatures/creature.cpp @@ -898,7 +898,8 @@ int Player::displayCreature(const std::shared_ptr& target) { << target->coins[GOLD] << " gold coin" << (target->coins[GOLD] != 1 ? "s" : "") << ".^x\n"; - if(isEffected("know-aura") || cClass==CreatureClass::PALADIN) { + if(isEffected("know-aura") || cClass==CreatureClass::PALADIN || + (getRace()==SERAPH && target->isEvil())) { space = true; oStr << target->getCrtStr(pThis, flags | CAP, 0) << " "; oStr << "has a " << target->alignColor() << target->alignString() << "^x aura."; diff --git a/creatures/creature2.cpp b/creatures/creature2.cpp index dcd4d714..5028c165 100644 --- a/creatures/creature2.cpp +++ b/creatures/creature2.cpp @@ -119,16 +119,16 @@ balanceStat balancedStats[41] = { {920,-8,7000,13,5,5}, // 28 {960,-9,8500,15,4,10}, // 29 {1000,-10,10000,18,3,16}, // 30 - {1040,-10,11625,19,3,17}, // 31 - {1080,-10,12000,20,3,17}, // 32 - {1120,-10,12375,21,3,18}, // 33 - {1160,-11,12750,22,3,18}, // 34 - {1200,-12,13125,23,3,19}, // 35 - {1240,-12,13500,24,3,19}, // 36 - {1280,-12,13875,25,3,20}, // 37 - {1320,-12,14250,26,3,20}, // 38 - {1360,-13,14625,27,3,21}, // 39 - {1400,-14,15000,28,3,23} // 40 + {1080,-10,11625,19,3,17}, // 31 + {1160,-10,12000,20,3,17}, // 32 + {1240,-10,12375,21,3,18}, // 33 + {1320,-11,12750,22,3,18}, // 34 + {1400,-12,13125,23,3,19}, // 35 + {1480,-12,13500,24,3,19}, // 36 + {1560,-12,13875,25,3,20}, // 37 + {1640,-12,14250,26,3,20}, // 38 + {1720,-13,14625,27,3,21}, // 39 + {2000,-14,15000,28,3,23} // 40 }; std::map maleToFemale = { @@ -501,121 +501,167 @@ void Monster::validateAc() { if(armor > ac) armor = ac; } +//********************************************************************** +// Monster::printAuraDmgMsg +//********************************************************************** +// Standardized aura damage message - for doHarmfulAuras() below +void Monster::printAuraDmgMsg(const std::shared_ptr& player, + const char* color, + const char* text, // e.g. "fiery aura singes you" + int dmg) { + *player << ColorOn << color << setf(CAP) << this + << "'s " << text << " for " + << player->customColorize("*CC:DAMAGE*") << dmg + << color << " damage!\n" << ColorOff; +} //*********************************************************************** // doHarmfulAuras //*********************************************************************** int Monster::doHarmfulAuras() { - int a=0,dmg=0,aura=0, saved=0; + int dmg=0, saved=0; long i=0,t=0; std::shared_ptr inRoom=nullptr; std::shared_ptr player=nullptr; - if(isPet()) - return(0); - if(hp.getCur() < hp.getMax()/10) - return(0); - if(flagIsSet(M_CHARMED)) - return(0); - - for(a=0;a kAuraFlags = { + M_FIRE_AURA, + M_COLD_AURA, + M_NAUSEATING_AURA, + M_NEGATIVE_LIFE_AURA, + M_TURBULENT_WIND_AURA, + M_ELECTRICAL_AURA, + M_WATERY_AURA, + M_EARTHY_AURA + }; + + // Any aura set? + bool anyAura = false; + for (auto flag : kAuraFlags) { + if (flagIsSet(flag)) { anyAura = true; break; } } - - if(!aura) - return(0); + if (!anyAura) + return 0; i = lasttime[LT_M_AURA_ATTACK].ltime; t = time(nullptr); - - if(t - i < 20L) // Mob has to wait 20 seconds. - return(0); - else { - lasttime[LT_M_AURA_ATTACK].ltime = t; - lasttime[LT_M_AURA_ATTACK].interval = 20L; - } + if (t - i < 20L) // Mob has to wait 20 seconds. + return 0; + lasttime[LT_M_AURA_ATTACK].ltime = t; + lasttime[LT_M_AURA_ATTACK].interval = 20L; inRoom = getRoomParent(); - if(!inRoom) - return(0); + if (!inRoom) + return 0; + auto cThis = Containable::downcasted_shared_from_this(); - for(a=0;aplayers.begin(); + + // Iterate players (same as before) + auto pIt = inRoom->players.begin(); auto pEnd = inRoom->players.end(); - while(pIt != pEnd) { + while (pIt != pEnd) { player = (*pIt++).lock(); - - if(!player || player->isEffected("petrification") || player->isCt()) + if (!player || player->isEffected("petrification") || player->isCt()) continue; dmg = Random::get(level/2, (level*3)/2); - - dmg = std::max(2,dmg); - - switch(a+M_FIRE_AURA) { - case M_FIRE_AURA: - if(player->isEffected("heat-protection") || player->isEffected("alwayswarm")) - continue; - - saved = player->chkSave(BRE, cThis, 0); - if(saved) - dmg /=2; - player->printColor("^R%M's firey aura singes you for %s%d^R damage!\n", this, player->customColorize("*CC:DAMAGE*").c_str(), dmg); - break; - case M_COLD_AURA: - if(player->isEffected("warmth") || player->isEffected("alwayscold")) - continue; - - saved = player->chkSave(BRE, cThis, 0); - if(saved) - dmg /=2; - player->printColor("^C%M's freezing aura chills you for %s%d^C damage!\n", this, player->customColorize("*CC:DAMAGE*").c_str(), dmg); - break; - case M_NAUSEATING_AURA: - if(player->immuneToPoison()) - continue; - - saved = player->chkSave(POI, cThis, 0); - if(saved) - dmg /=2; - player->printColor("^g%M's foul stench chokes and nauseates you for %s%d^g damage.\n", this, player->customColorize("*CC:DAMAGE*").c_str(), dmg); - break; - case M_NEGATIVE_LIFE_AURA: - if(player->isUndead() || player->isEffected("drain-shield")) - continue; - - saved = player->chkSave(DEA, cThis, 0); - if(saved) - dmg /=2; - player->printColor("^m%M's negative aura taps your life for %s%d^m damage.\n", this, player->customColorize("*CC:DAMAGE*").c_str(), dmg); - break; - case M_TURBULENT_WIND_AURA: - if(player->isEffected("wind-protection")) - continue; - - saved = player->chkSave(BRE, cThis, 0); - if(saved) - dmg /=2; - player->printColor("^W%M's turbulent winds buff you about for %s%d^W damage.\n", this, player->customColorize("*CC:DAMAGE*").c_str(), dmg); - break; + dmg = std::max(2, dmg); + + switch (flag) { + case M_FIRE_AURA: + if (player->isEffected("heat-protection") || player->isEffected("alwayswarm")) + continue; + if (player->chkSave(BRE, cThis, 0)) + dmg /= 2; + printAuraDmgMsg(player, "^R", "fiery aura singes you", dmg); + break; + + case M_COLD_AURA: + if (player->isEffected("warmth") || player->isEffected("alwayscold")) + continue; + if (player->chkSave(BRE, cThis, 0)) + dmg /= 2; + printAuraDmgMsg(player, "^C", "freezing aura chills you", dmg); + break; + + case M_NAUSEATING_AURA: + if (player->immuneToPoison()) + continue; + else if (player->resistantToPoison() && Random::get(1,100) <= 15) + continue; + if (player->chkSave(POI, cThis, 0)) + dmg /= 2; + else + player->stun(Random::get(1,2)); + printAuraDmgMsg(player, "^g", "foul stench causes you to choke and retch", dmg); + break; + + case M_NEGATIVE_LIFE_AURA: + if (player->isUndead() || player->isEffected("drain-shield")) + continue; + if (player->chkSave(DEA, cThis, 0)) + dmg /= 2; + printAuraDmgMsg(player, "^m", "negative aura taps your life", dmg); + break; + + case M_TURBULENT_WIND_AURA: + if (player->isEffected("wind-protection")) + continue; + if (player->chkSave(BRE, cThis, 0)) + dmg /= 2; + printAuraDmgMsg(player, "^W", "turbulent winds buff you about", dmg); + break; + + case M_ELECTRICAL_AURA: + if (player->isEffected("static-field")) + continue; + if (player->chkSave(BRE, cThis, 0)) + dmg /= 2; + printAuraDmgMsg(player, "^c", "highly charged electrical aura zaps you", dmg); + break; + + case M_WATERY_AURA: + if (player->isEffected("breathe-water")) + continue; + if (player->chkSave(BRE, cThis, 0)) + dmg /= 2; + printAuraDmgMsg(player, "^B", "watery aura causes you to cough and sputter", dmg); + break; + + case M_EARTHY_AURA: + if (player->isEffected("earth-shield")) + continue; + if (player->chkSave(BRE, cThis, 0)) + dmg /= 2; + printAuraDmgMsg(player, "^y", "earthy aura pelts you with flying debris and jagged detritus", dmg); + break; } player->hp.decrease(dmg); - if(player->checkDie(cThis)) - return(1); - - }// End while - }// End for + if (player->checkDie(cThis)) + return 1; + } + } - return(0); + return 0; } + //*********************************************************************** // isGuardLoot //*********************************************************************** diff --git a/creatures/creatureAttr.cpp b/creatures/creatureAttr.cpp index bcbcc26a..7ccac0c7 100644 --- a/creatures/creatureAttr.cpp +++ b/creatures/creatureAttr.cpp @@ -201,7 +201,7 @@ void Creature::setClan(unsigned short c) { clan = c; } void Creature::setLevel(unsigned short l, bool isDm) { level = std::max(1, std::min(l, isDm ? 127 : MAXALVL)); } -void Creature::setAlignment(short a) { alignment = std::max(-1000, std::min(1000, a)); } +void Creature::setAlignment(short a) { alignment = std::max(MIN_ALIGN, std::min(MAX_ALIGN, a)); } void Creature::subAlignment(unsigned short a) { setAlignment(alignment - a); } @@ -299,6 +299,7 @@ int Monster::getSkillLevel() const { return(skillLevel); } int Monster::getMaxLevel() const { return(maxLevel); } unsigned short Monster::getNumWander() const { return(numwander); } +unsigned short Monster::getPermSpawnChance() const { return(permSpawnChance); } unsigned short Monster::getLoadAggro() const { return(loadAggro); } unsigned short Monster::getUpdateAggro() const { return(updateAggro); } unsigned short Monster::getCastChance() const { return(cast); } @@ -313,6 +314,7 @@ void Monster::setMagicResistance(unsigned short m) { magicResistance = std::max< void Monster::setLoadAggro(unsigned short a) { loadAggro = std::max(0, std::min(a, 99)); } void Monster::setUpdateAggro(unsigned short a) { updateAggro = std::max(1, std::min(a, 99)); } void Monster::setNumWander(unsigned short n) { numwander = std::max(0, std::min(6, n)); } +void Monster::setPermSpawnChance(unsigned short n) { permSpawnChance = std::max(0, std::min(1000, n)); } void Monster::setSkillLevel(int l) { skillLevel = std::max(0, std::min(100, l)); } void Monster::setMobTrade(unsigned short t) { mobTrade = std::max(0,std::min(MOBTRADE_COUNT-1, t)); } void Monster::setPrimeFaction(std::string_view f) { primeFaction = f; } @@ -575,7 +577,7 @@ void Monster::monReset() { baseRealm = NO_REALM; defenseSkill = weaponSkill = attackPower = 0; - numwander = loadAggro = 0; + numwander = loadAggro = permSpawnChance = 0; memset(last_mod, 0, sizeof(last_mod)); mobTrade = 0; @@ -962,6 +964,8 @@ void Monster::monCopy(const Monster& cr, bool assign) { updateAggro = cr.updateAggro; numwander = cr.numwander; + permSpawnChance = cr.permSpawnChance; + for(i=0; i<10; i++) carry[i] = cr.carry[i]; diff --git a/creatures/creatures.cpp b/creatures/creatures.cpp index a3883f11..fadea463 100644 --- a/creatures/creatures.cpp +++ b/creatures/creatures.cpp @@ -95,14 +95,26 @@ bool Creature::canSeeRoom(const std::shared_ptr& room, bool p) const { // room is affected by normal darkness if(room->isNormalDark()) { // there are several sources of normal vision - bool normal_sight = gConfig->getRace(race)->hasInfravision() || - isUndead() || isEffected("lycanthropy") || (player && player->getLight()); + bool normal_sight = ( + gConfig->getRace(race)->hasInfravision() || + isUndead() || isEffected("lycanthropy") || + (player && (player->getLight())) + ); // if they can't see, maybe someone else in the room has light for them if(!normal_sight) { for(const auto& pIt: room->players) { if(auto ply = pIt.lock()) { - if (ply->getAsPlayer()->getLight()) { + if (ply->getAsPlayer()->getLight() || ply->getAsPlayer()->isEffected("light")) { + normal_sight = true; + break; + } + } + } + //now check for any mobs with light effects + if (!normal_sight) { + for(const auto& mons: room->monsters) { + if(mons->getAsMonster()->isEffected("light")) { normal_sight = true; break; } @@ -1054,6 +1066,9 @@ bool Monster::getEnchantmentImmunity(const std::shared_ptr& caster, co if(monType::isImmuneEnchantments(getType())) monsterImmune=true; + + + if (spell != "hold-undead" && getAsCreature()->isUndeadImmuneEnchantments()) monsterImmune=true; @@ -1241,12 +1256,10 @@ bool Creature::getRaceEnchantmentResist(const std::shared_ptr& caster, case HALFELF: if (Random::get(1,100) <= (getRace()==HALFELF?30:90)) { if (print) { - *this << ColorOn << "^yYour " << (getRace()==HALFELF?"Elven half":"Fey ancestry") << " protected you from " << caster << "'s spell.\n" << ColorOff; - *caster << ColorOn << "^y" << setf(CAP) << this << " resisted your spell due to " << hisHer() << (getRace()==HALFELF?" Elven half":" Fey origins") << ".\n" << ColorOff; - if(caster->isStaff()) - *caster << "Race Number = " << getRace() << "\n"; + *this << ColorOn << "^yYour " << (getRace()==HALFELF?"Elven half":"Elven ancestry") << " protected you from " << caster << "'s spell.\n" << ColorOff; + *caster << ColorOn << "^y" << setf(CAP) << this << " resisted your spell due to " << hisHer() << (getRace()==HALFELF?" Elven half":" Elven ancestry") << ".\n" << ColorOff; broadcast(caster->getSock(), getSock(), caster->getParent(), "^y%M resisted %N's spell due to %s %s!^x", - this, caster.get(), hisHer(), (getRace()==HALFELF?"Elven half":"Fey ancestry")); + this, caster.get(), hisHer(), (getRace()==HALFELF?"Elven half":"Elven ancestry")); } resist=true; } @@ -1286,6 +1299,18 @@ bool Creature::getRaceEnchantmentResist(const std::shared_ptr& caster, willAggro=false; } break; + case OGRE: + case OROG: + case DUERGAR: + if (spell == "fear" || spell == "scare") { + if (print) { + *this << ColorOn << "^y" << setf(CAP) << caster << "'s " << spell << " spell dissipated. Your vicious nature makes you immune!\n" << ColorOff; + *caster << ColorOn << "^y" << this << " is too vicious to be affected by " << spell << " spells. Your spell dissipated.\n" << ColorOff; + broadcast(caster->getSock(), caster->getParent(), "^y%M is immune to %s spells. %M's spell dissipated.^x", this, spell.c_str(), caster.get()); + } + resist=true; + } + break; default: resist=false; break; @@ -1769,6 +1794,14 @@ int Creature::getElusiveness() { return((intelligence.getCur() + dexterity.getCur())/2); } +//******************************************************************** +// getBrutality() +//******************************************************************** +//Brutality combo stat +int Creature::getBrutality() { + return((strength.getCur() + constitution.getCur())/2); +} + //******************************************************************** // getWillpower() //******************************************************************** @@ -1918,7 +1951,7 @@ bool Creature::hatesEnemy(std::shared_ptr enemy) const { case DWARF: case HILLDWARF: if (eRace==ORC || eRace==GOBLIN || eRace==OGRE || - eRace==DUERGAR || eRace==TROLL || eRace==KOBOLD) + eRace==DUERGAR || eRace==TROLL || eRace==KOBOLD || eRace==OROG) return(true); if (eMtype == GIANTKIN || eMtype == GOBLINOID) return(true); @@ -1934,12 +1967,19 @@ bool Creature::hatesEnemy(std::shared_ptr enemy) const { break; case ORC: if(eRace==DWARF || eRace==ELF || eRace==BUGBEAR || - eRace==HALFELF || eRace==GNOME || eRace==HALFLING) + eRace==HALFELF || eRace==GNOME || eRace==HALFLING || + eRace==GREYELF || eRace==WILDELF) return(true); //Orcs hate good fey if (eMtype == FAERIE && eAlign > 0) return(true); break; + case OROG: + if(eRace==DWARF || eRace==ELF || eRace==BUGBEAR || + eRace==HALFELF || eRace==GNOME || eRace==HALFLING || + eRace==GREYELF || eRace==WILDELF) + return(true); + break; case GNOME: if(eRace==GOBLIN || eRace==ORC || eRace==KOBOLD) return(true); @@ -1963,14 +2003,15 @@ bool Creature::hatesEnemy(std::shared_ptr enemy) const { //A lot of the evil humanoid races tend to torture goblins and/or enslave them... //Plus, they're generally just hateful little bastards if(eRace==ELF || eRace==DWARF || eRace==HOBGOBLIN || eRace==BUGBEAR || - eRace==OGRE || eRace==GNOME || eRace==TROLL || eRace==GNOLL || eRace == HALFLING) + eRace==OGRE || eRace==GNOME || eRace==TROLL || eRace==GNOLL || eRace == HALFLING || + eRace==GREYELF || eRace==WILDELF || eRace==OROG) return(true); //Goblins hate good fey if (eMtype == FAERIE && eAlign > 0) return(true); break; case HALFLING: - if (eRace==ORC || eRace==GOBLIN || eRace==KOBOLD) + if (eRace==ORC || eRace==GOBLIN || eRace==KOBOLD || eRace==OGRE || eRace==OROG) return(true); break; case MINOTAUR: @@ -1979,7 +2020,7 @@ bool Creature::hatesEnemy(std::shared_ptr enemy) const { return(true); break; case KOBOLD: - if(eRace==OGRE || eRace==GNOME || eRace==HALFLING) + if(eRace==OGRE || eRace==GNOME || eRace==HALFLING || eRace==OROG) return(true); break; case KATARAN: @@ -2235,4 +2276,25 @@ bool Creature::isIndoors() { return (room->flagIsSet(R_INDOORS)); } +int Creature::getDefenseSkillModifier() const { + + int mod=0; + + if(isEffected("protection")) { + if(isMonster()) + mod += std::max(10,getLevel()); + else + mod += std::max(10,getEffect("protection")->getStrength()); + } + + if(isEffected("shield")) { + if(isMonster()) + mod += std::max(10,getLevel()); + else + mod += std::max(10,getEffect("shield")->getStrength()); + } + + return(mod); +} + diff --git a/creatures/monsters.cpp b/creatures/monsters.cpp index 607ccf9f..21da755e 100644 --- a/creatures/monsters.cpp +++ b/creatures/monsters.cpp @@ -926,6 +926,57 @@ bool Monster::willAssist(const std::shared_ptr victim) const { return(false); } +bool Monster::hasAcidDissolveAttack() const { + static const std::array dissolveFlags = { + M_DISSOLVES_ALL, M_DISSOLVES_ALL_METAL, + M_DISSOLVES_FERROUS_METAL, M_DISSOLVES_NONFERROUS_METAL, + M_DISSOLVES_ORGANIC, M_DISSOLVES_STONE + }; + + return(std::any_of(dissolveFlags.begin(), dissolveFlags.end(), + [this](int flag) { return flagIsSet(flag); })); +} + + +//******************************************************************* +// findObjectToDissolve +//******************************************************************* +// This will search all objects on the ground in a room and randomly +// choose one for a mob to eat, if it can dissolve it - would be used +// primarily for oozes/slimes,etc +std::shared_ptr Monster::findObjectToDissolve() { + std::shared_ptr room = getRoomParent(); + + if (room->objects.empty()) { + return(nullptr); + } + + std::shared_ptr selectedObject = nullptr; + int count = 0; + + for (const auto& obj : room->objects) { + if (!canScavange(obj, true)) continue; // When passing true, hidden objects are fair game! + if (obj->flagIsSet(O_RESIST_DISOLVE)) continue; // Ignore dissolve-resistant objects + + if (flagIsSet(M_DISSOLVES_ALL) || + (flagIsSet(M_DISSOLVES_ALL_METAL) && obj->isMetal()) || + (flagIsSet(M_DISSOLVES_FERROUS_METAL) && obj->isFerrousMetal()) || + (flagIsSet(M_DISSOLVES_NONFERROUS_METAL) && obj->isNonFerrousMetal()) || + (flagIsSet(M_DISSOLVES_ORGANIC) && obj->isOrganic()) || + (flagIsSet(M_DISSOLVES_STONE) && obj->isStone())) { + + // Random selection with equal probability + if (Random::get(0, count++) == 0) { + selectedObject = obj; + } + } + } + + return(selectedObject); +} + + + //******************************************************************* // findObjectToScavenge //******************************************************************* @@ -933,61 +984,52 @@ bool Monster::willAssist(const std::shared_ptr victim) const { // choose one for a mob to scavenge std::shared_ptr Monster::findObjectToScavenge() { std::shared_ptr room = getRoomParent(); - std::vector> scavengableObject; - - if (room->objects.empty()) - return(nullptr); - if(getWeight() >= maxWeight()) + if (room->objects.empty() || getWeight() >= maxWeight() || getTotalBulk() >= getMaxBulk()) { return(nullptr); + } - if (getTotalBulk() >= getMaxBulk()) - return(nullptr); + std::shared_ptr selectedObject = nullptr; + int count = 0; for (const auto& obj : room->objects) { - if (!canScavange(obj)) - continue; - if (obj->getType() == ObjectType::WEAPON && flagIsSet(M_WILL_WIELD)) - continue; - if ((getWeight() + obj->getActualWeight()) > maxWeight()) - continue; - if ((getTotalBulk() + obj->getActualBulk()) > getMaxBulk()) - continue; - - scavengableObject.push_back(obj); - } - - if(scavengableObject.empty()) { - return(nullptr); + if (!canScavange(obj)) continue; + if (obj->getType() == ObjectType::WEAPON && flagIsSet(M_WILL_WIELD)) continue; + if ((getWeight() + obj->getActualWeight()) > maxWeight()) continue; + if ((getTotalBulk() + obj->getActualBulk()) > getMaxBulk()) continue; + + // Reservoir Sampling: randomly select one object from valid candidates + if (Random::get(0, count++) == 0) { + selectedObject = obj; + } } - return(*Random::get(scavengableObject)); + return(selectedObject); } + //******************************************************************* // findScavengedObject //******************************************************************* // This will search all of a mob's scavenged objects and return one // of them chosen at random std::shared_ptr Monster::findScavengedObject() { - std::vector> scavengedObjects; + std::shared_ptr selectedObject = nullptr; + int count = 0; - // Collect all objects with the O_WAS_SCAVENGED flag for (const auto& obj : objects) { - if (obj->flagIsSet(O_WAS_SCAVENGED)) { - scavengedObjects.push_back(obj); - } - } + if (!obj->flagIsSet(O_WAS_SCAVENGED)) continue; - // If no flagged objects are found, return nullptr - if (scavengedObjects.empty()) { - return(nullptr); + // Randomly select one, ensuring equal probability among all valid objects + if (Random::get(0, count++) == 0) { + selectedObject = obj; + } } - // Pick one at random - return(*Random::get(scavengedObjects)); + return(selectedObject); } + //********************************************************************** // countScavengedObjects //********************************************************************** @@ -1020,7 +1062,7 @@ void Monster::checkScavange(long t) { if(isMagicallyHeld()) return; - + // If already scavenged, might decide to drop! if(flagIsSet(M_HAS_SCAVENGED) && !flagIsSet(M_SCAVENGE_NO_DROP) && countScavengedObjects() > 0) { i = lasttime[LT_MON_SCAVENGE].ltime; @@ -1060,7 +1102,21 @@ void Monster::checkScavange(long t) { } if(t - i > 20) lasttime[LT_MON_SCAVENGE].ltime = t; - } + } + + if(flagIsSet(M_DISSOLVES_ROOM_OBJ)) { + i = lasttime[LT_MON_SCAVENGE].ltime; + if( t - i > 20 && Random::get(0.15)) { + object = findObjectToDissolve(); + if (object) { + broadcast((std::shared_ptr )nullptr, room, "^g%M dissolved %1P^x.", this, object.get()); + object->deleteFromRoom(); + object.reset(); + } + } + if(t - i > 20) + lasttime[LT_MON_SCAVENGE].ltime = t; + } // thief code if(flagIsSet(M_TAKE_LOOT) || flagIsSet(M_STREET_SWEEPER)) { @@ -1121,10 +1177,10 @@ void Monster::checkScavange(long t) { // canScavange //********************************************************************* -bool Monster::canScavange(const std::shared_ptr& object) { +bool Monster::canScavange(const std::shared_ptr& object, bool scavengeHiddenObjects) { return( !object->flagIsSet(O_NO_TAKE) && !object->flagIsSet(O_SCENERY) && - !object->flagIsSet(O_HIDDEN) && + !(object->flagIsSet(O_HIDDEN) && !scavengeHiddenObjects) && !object->flagIsSet(O_PERM_INV_ITEM) && !object->flagIsSet(O_PERM_ITEM) && !Unique::is(object) diff --git a/creatures/raceData.cpp b/creatures/raceData.cpp index 5a95cfe6..72a4475c 100644 --- a/creatures/raceData.cpp +++ b/creatures/raceData.cpp @@ -90,6 +90,7 @@ RaceData::RaceData(xmlNodePtr rootNode) { size = NO_SIZE; isParentRace = false; playable = gendered = true; + xpAdjustment = 0; zero(stats, sizeof(stats)); zero(saves, sizeof(saves)); zero(classes, sizeof(classes)); @@ -108,6 +109,7 @@ RaceData::RaceData(xmlNodePtr rootNode) { if(NODE_NAME(curNode, "Adjective")) xml::copyToString(adjective, curNode); else if(NODE_NAME(curNode, "Abbr")) xml::copyToString(abbr, curNode); else if(NODE_NAME(curNode, "NotPlayable")) playable = false; + else if(NODE_NAME(curNode, "XPAdjustment")) xml::copyToNum(xpAdjustment, curNode); else if(NODE_NAME(curNode, "ParentRace")) { xml::copyToNum(parentRace, curNode); if(gConfig->races.find(parentRace) != gConfig->races.end()) @@ -259,7 +261,7 @@ int Config::stattoNum(std::string_view str) { if(str == "Con") return(CON); if(str == "Dex") return(DEX); if(str == "Int") return(INT); - if(str == "Pty") return(PTY); + if(str == "Pty" || str == "Pie") return(PTY); return(0); } @@ -564,6 +566,12 @@ Size RaceData::getSize() const { return(size); } int RaceData::getStartAge() const { return(startAge); } +//********************************************************************* +// getXPAdjustment +//********************************************************************* + +int RaceData::getXPAdjustment() const { return(xpAdjustment); } + //********************************************************************* // getStatAdj //********************************************************************* diff --git a/effects/effects.cpp b/effects/effects.cpp index dc5428cc..1a574f06 100644 --- a/effects/effects.cpp +++ b/effects/effects.cpp @@ -139,8 +139,8 @@ int dmEffectList(const std::shared_ptr& player, cmd* cmnd) { if(id != 0 && i != id) continue; - player->printPaged(fmt::format("{})\tName: ^W{:<20}^x Use Str: {}^x Display: {}\n", i, - effect.getName(), effect.usesStrength() ? "^gY" : "^rN", effect.getDisplay())); + player->printPaged(fmt::format("{})\tName: ^W{:<20}^x Use Str: {}^x Use vStr: {}^x Display: {}\n", i, + effect.getName(), effect.usesStrength() ? "^gY" : "^rN", effect.usesVariableStrength() ? "^gY" : "^rN", effect.getDisplay())); if(!all && i != id) continue; @@ -868,8 +868,6 @@ std::string Effects::getEffectsString(const std::shared_ptr & viewer) if(viewer->isStaff()) { -// if(!effectInfo->getBaseEffect().empty()) -// effStr << " Base(" << effectInfo->getBaseEffect() << ")"; effStr << " ^WStrength:^x " << effectInfo->getStrength(); if(effectInfo->getExtra()) { effStr << " ^WExtra:^x " << effectInfo->getExtra(); @@ -1356,13 +1354,22 @@ bool Effect::isSpell() const { } //********************************************************************* -// isSpell +// usesStrength //********************************************************************* bool Effect::usesStrength() const { return(useStrength); } +//********************************************************************* +// usesVariableStrength +//********************************************************************* + +bool Effect::usesVariableStrength() const { + return(useVariableStrength); +} + + //********************************************************************* // getRoomDelStr //********************************************************************* diff --git a/effects/effectsLoader.cpp b/effects/effectsLoader.cpp index 6510fc4c..31f37e8c 100644 --- a/effects/effectsLoader.cpp +++ b/effects/effectsLoader.cpp @@ -63,6 +63,19 @@ bool Config::loadEffects() { .useStrength(true), effects ); + addToSet( + EffectBuilder() + .name("shield") + .display("^MShield^x") + .pulsed(false) + .type("Positive") + .selfAddStr("^MA magical energy shield forms around you.^x") + .roomAddStr("^MA magical energy shield forms around *LOW-ACTOR*.^x") + .selfDelStr("^MYour magical energy shield has faded.^x") + .roomDelStr("^M*ACTOR*'s magical energy shield has faded.^x") + .isSpellEffect(true), + effects + ); addToSet( EffectBuilder() .name("avianaria") @@ -118,7 +131,25 @@ bool Config::loadEffects() { .roomAddStr("^WA halo appears over *LOW-ACTOR*'s head.^x") .selfDelStr("^YYou feel less holy.^x") .roomDelStr("^Y*ACTOR* is no longer blessed.^x") - .isSpellEffect(true), + .isSpellEffect(true) + .useVariableStrength(true), + effects + ); + addToSet( + EffectBuilder() + .name("light") + .addBaseEffect("light") + .oppositeEffect("darkness") + .display("^CLight^x") + .computeScript("effectLib.computeBeneficial(actor, effect, applier)") + .pulsed(false) + .type("Positive") + .selfAddStr("^CBright light pierces the darkness.^x") + .roomAddStr("^CA bright orb of light appears above *LOW-ACTOR*, pushing away the darkness.^x") + .selfDelStr("^CYour magical light has winked out.^x") + .roomDelStr("^C*ACTOR*'s orb of magical light has winked out.^x") + .isSpellEffect(true) + .useStrength(true), effects ); addToSet( @@ -295,6 +326,7 @@ bool Config::loadEffects() { EffectBuilder() .name("darkness") .addBaseEffect("darkness") + .oppositeEffect("light") .display("^DDarkness^x") .computeScript("effectLib.computeDarkInfra(actor, effect, applier)") .preApplyScript("actor.getRoom().setTempNoKillDarkmetal(True)") @@ -306,7 +338,8 @@ bool Config::loadEffects() { .roomAddStr("^D*ACTOR* is engulfed in darkness.^x") .selfDelStr("^YThe globe of darkness around you fades.^x") .roomDelStr("^YThe globe of darkness around *LOW-ACTOR* fades.^x") - .isSpellEffect(true), + .isSpellEffect(true) + .useStrength(true), effects ); addToSet( @@ -320,7 +353,7 @@ bool Config::loadEffects() { .selfAddStr("^yYou lose your hearing!^x") .roomAddStr("^y*ACTOR* has gone deaf.^x") .selfDelStr("^yYou can hear again!^x") - .roomDelStr("^yYou can hear again!^x"), + .roomDelStr("^y*ACTOR* can hear again.^x"), effects ); addToSet( @@ -684,7 +717,8 @@ bool Config::loadEffects() { .roomAddStr("^M*ACTOR*'s skin toughens.^x") .selfDelStr("^yYou are no longer protected from fire.^x") .roomDelStr("^y*ACTOR*'s skin softens.^x") - .isSpellEffect(true), + .isSpellEffect(true) + .useVariableStrength(true), effects ); addToSet( @@ -1003,6 +1037,21 @@ bool Config::loadEffects() { .isSpellEffect(true), effects ); + addToSet( + EffectBuilder() + .name("empathy") + .addBaseEffect("empathy") + .display("^bEmpathy^x") + .computeScript("effectLib.computeDetect(actor, effect, applier)") + .pulsed(false) + .type("Positive") + .selfAddStr("^bYou suddenly feel very empathic.^x") + .roomAddStr("^b*ACTOR* seems more empathic.^x") + .selfDelStr("^bYour magical empathy has dissipated.^x") + .roomDelStr("^b*ACTOR*'s empathy has faded.^x") + .isSpellEffect(true), + effects + ); addToSet( EffectBuilder() .name("levitate") @@ -1155,13 +1204,14 @@ bool Config::loadEffects() { .roomAddStr("^W*ACTOR* is surrounded by a faint magical aura.^x") .selfDelStr("^yYour aura of protection fades.^x") .roomDelStr("^y*ACTOR* is no longer protected.^x") - .isSpellEffect(true), + .isSpellEffect(true) + .useVariableStrength(true), effects ); addToSet( EffectBuilder() .name("benediction") - .addBaseEffect("benediction") + .oppositeEffect("malediction") .display("^BBenediction^x") .computeScript("effectLib.computeBeneficial(actor, effect, applier)") .pulsed(false) @@ -1176,7 +1226,7 @@ bool Config::loadEffects() { addToSet( EffectBuilder() .name("malediction") - .addBaseEffect("malediction") + .oppositeEffect("benediction") .display("^RMalediction^x") .computeScript("effectLib.computeBeneficial(actor, effect, applier)") .pulsed(false) diff --git a/include/builders/effectBuilder.hpp b/include/builders/effectBuilder.hpp index 5a05f24c..17f8f424 100644 --- a/include/builders/effectBuilder.hpp +++ b/include/builders/effectBuilder.hpp @@ -54,6 +54,7 @@ class EffectBuilder { BOOL_BUILDER(pulsed); BOOL_BUILDER(isSpellEffect); BOOL_BUILDER(useStrength); + BOOL_BUILDER(useVariableStrength); INT_BUILDER(pulseDelay); INT_BUILDER(baseDuration); diff --git a/include/commands.hpp b/include/commands.hpp index aaf8b0fe..2e2869b9 100644 --- a/include/commands.hpp +++ b/include/commands.hpp @@ -74,7 +74,7 @@ int cmdDeleteStatement(const std::shared_ptr& player, cmd* cmnd); // color.c int cmdColors(const std::shared_ptr& player, cmd* cmnd); -int innateLevitate(const std::shared_ptr& player, cmd* cmnd); + // command2.c int cmdTraffic(const std::shared_ptr& player, cmd* cmnd); @@ -123,6 +123,7 @@ int cmdTelOpts(const std::shared_ptr& player, cmd* cmnd); int cmdQuit(const std::shared_ptr& player, cmd* cmnd); int cmdChangeStats(const std::shared_ptr& player, cmd* cmnd); void changingStats(std::shared_ptr sock, const std::string& str ); +std::string getFullStatName(int stat, bool cap=false); // command7.c @@ -241,7 +242,9 @@ int cmdRepair(const std::shared_ptr& player, cmd* cmnd); int cmdCircle(const std::shared_ptr& player, cmd* cmnd); int cmdBash(const std::shared_ptr& player, cmd* cmnd); +int cmdSlam(const std::shared_ptr& player, cmd* cmnd); int cmdGore(const std::shared_ptr& player, cmd* cmnd); +int cmdSmash(const std::shared_ptr& player, cmd* cmnd); int cmdKick(const std::shared_ptr& player, cmd* cmnd); int cmdMaul(const std::shared_ptr& player, cmd* cmnd); int cmdTalk(const std::shared_ptr& player, cmd* cmnd); @@ -412,6 +415,7 @@ int cmdSurname(const std::shared_ptr& player, cmd* cmnd); int cmdVisible(const std::shared_ptr& player, cmd* cmnd); int cmdDice(const std::shared_ptr& player, cmd* cmnd); int cmdChooseAlignment(const std::shared_ptr& player, cmd* cmnd); +int cmdAlignment(const std::shared_ptr& player, cmd* cmnd); int cmdKeep(const std::shared_ptr& player, cmd* cmnd); int cmdUnkeep(const std::shared_ptr& player, cmd* cmnd); int cmdLabel(const std::shared_ptr& player, cmd* cmnd); diff --git a/include/communication.hpp b/include/communication.hpp index 71c8be08..1b60ed3e 100644 --- a/include/communication.hpp +++ b/include/communication.hpp @@ -21,6 +21,8 @@ #include "commands.hpp" #include "flags.hpp" #include "proto.hpp" +#include +#include class Creature; class Player; @@ -72,7 +74,6 @@ typedef struct sayInfo { extern sayInfo sayList[]; - typedef struct channelInfo { const char *channelName; // Name of the channel bool useLanguage; // Should this channel use languages? @@ -103,3 +104,5 @@ void sendGlobalComm(const std::shared_ptr player, const std::string &tex channelPtr getChannelByName(const std::shared_ptr& player, const std::string &chanStr); channelPtr getChannelByDiscordChannel(unsigned long discordChannelID); + + diff --git a/include/effects.hpp b/include/effects.hpp index ce5a9771..c9503fc3 100644 --- a/include/effects.hpp +++ b/include/effects.hpp @@ -62,6 +62,7 @@ class Effect { bool isSpellEffect{}; // Decides if the effect will show up under "spells under" bool useStrength{}; + bool useVariableStrength{}; int baseDuration{}; // Base duration of the effect float potionMultiplyer{}; // Multiplier of duration for potion @@ -92,6 +93,7 @@ class Effect { [[nodiscard]] bool isPulsed() const; [[nodiscard]] bool isSpell() const; [[nodiscard]] bool usesStrength() const; + [[nodiscard]] bool usesVariableStrength() const; // Base effect(s) - for multiple effects that confer the same type of effect (fly, etc) const std::list &getBaseEffects(); @@ -172,6 +174,8 @@ class EffectInfo { void setStrength(int pStrength); void setExtra(int pExtra); void setDuration(long pDuration); + + void setParent(MudObject *parent); bool isOwner(const std::shared_ptr &owner) const; @@ -193,6 +197,7 @@ class EffectInfo { int pulseModifier = 0; // Adjustment to base pulse timer long duration = 0; // How much longer will this effect last int strength = 0; // How strong is this effect (for overwriting effects) + int extra = 0; // Extra info const Effect *myEffect = nullptr; // Pointer to the effect listing @@ -227,8 +232,10 @@ class Effects { //EffectInfo* addEffect(std::string_view effect, std::shared_ptr applier, bool show, std::shared_ptr pParent=0, const std::shared_ptr & onwer=0, bool keepApplier=false); EffectInfo *addEffect(EffectInfo *newEffect, bool show, MudObject *parent = nullptr, bool keepApplier = false); + EffectInfo *addEffect(const std::string& effect, long duration, int strength, const std::shared_ptr&applier = nullptr, bool show = true, MudObject *parent = nullptr, const std::shared_ptr &onwer = nullptr, bool keepApplier = false); + void copy(const Effects *source, MudObject *pParent = nullptr); bool removeEffect(const std::string& effect, bool show, bool remPerm, const std::shared_ptr&fromApplier = nullptr); diff --git a/include/flags.hpp b/include/flags.hpp index 417c5ef9..a2a67000 100644 --- a/include/flags.hpp +++ b/include/flags.hpp @@ -193,7 +193,6 @@ #define P_LOG_WATCH 54 // DM is watching the log channel #define P_CLEAR_TARGET_ON_FLEE 55 // Clear target on flee #define P_NO_AUTO_TARGET 56 // Don't Automatically target anything you attack if you don't already have a target -// free 56 // UNUSED #define P_IGNORE_CLASS_SEND 57 // Player is ignoring class sends #define P_IGNORE_GROUP_BROADCAST 58 // Player is ignoring group broadcasts #define P_ENCHANT_ONLY 59 // Player resists-hands @@ -204,7 +203,7 @@ #define P_SHOW_TICK 64 // Show ticks #define P_JUST_REFUNDED 65 // No haggling until they leave the room! #define P_CT_CAN_KILL 66 // CT Can use *kill command -// free 67 +#define P_PTEST_SMASH 67 // Player able to ptest smash ability // free 68 // free 69 #define P_HIDE_FORUM_POSTS 70 // Player does not want to see forum post notifications @@ -345,7 +344,7 @@ #define P_DARKMETAL 205 // Player has a darkmetal item (DONT SET) #define P_SAVE_DEBUG 206 #define P_NO_GROUP_TARGET_MSG 207 // Player will not see individual group targeting changes -#define P_NO_MTARGET_ORDINALS 208 // Player will not see monster ordinals in group target messages +#define P_NO_MTARGET_ORDINALS 208 // Player will not see monster ordinals in group target messages // free 209 // free 210 // free 211 @@ -392,7 +391,7 @@ #define M_CAN_PLEDGE_TO 32 // players can pledge to monster #define M_CAN_RESCIND_TO 33 // players can rescind to monster #define M_DISEASES 34 // Monster causes disease -#define M_DISOLVES_ITEMS 35 // Monster can dissolve items +#define M_DISSOLVES_ALL 35 // Monster can dissolve items of any material #define M_CAN_PURCHASE_FROM 36 // player can purchase from monster #define M_TRADES 37 // monster will give items #define M_PASSIVE_EXIT_GUARD 38 // passive exit guard @@ -406,18 +405,18 @@ #define M_DM_FOLLOW 46 // Monster will follow DM // free 47 // free 48 -// free 49 +#define M_DISSOLVES_ROOM_OBJ 49 // Monster will dissolve objects on the ground #define M_CHARMED 50 // Monster is charmed #define M_MOBILE_MONSTER 51 // Monster will wander around the area #define M_LOGIC_MONSTER 52 // Logic Monster #define M_TAKE_LOOT 53 // Monster will try to take players' loot #define M_FAST_WANDER 54 // Monster will quickly wander around the area #define M_PET 55 // Monster is a pet -// free 56 -// free 57 -// free 58 -// free 59 -// free 60 +#define M_DISSOLVES_ALL_METAL 56 // Monster can dissolve any metal object +#define M_DISSOLVES_FERROUS_METAL 57 // Monster can dissolve ferrous metal objects +#define M_DISSOLVES_NONFERROUS_METAL 58 // Monster can dissolve non-ferrous metal objects +#define M_DISSOLVES_ORGANIC 59 // Monster can dissolve organic objects +#define M_DISSOLVES_STONE 60 // Monster can dissolve stone objects // free 61 #define M_SEXLESS 62 // sexless // free 63 @@ -450,7 +449,7 @@ #define M_REGENERATES 90 // monster regenerates #define M_PLUS_TWO 91 // monster needs +2 weapon or above to be hit #define M_PLUS_THREE 92 // monster needs +3 weapon or above to be hit -// free 93 +#define M_PLUS_FOUR 93 // monster needs +4 weapon or above to be hit #define M_STEAL_WHEN_ATTACKING 94 // monster steals if attacked #define M_STEAL_ALWAYS 95 // monster steals on sight // free 96 @@ -509,9 +508,9 @@ // free 149 // free 150 // free 151 -// free 152 -// free 153 -// free 154 +#define M_NO_SLAM 152 // Monster unaffected by slam attacks +#define M_NO_BASH 153 // Monster unaffected by bash attacks +#define M_NO_SMASH 154 // Monster unaffected by smash attacks #define M_NO_GORE 155 // Monster unaffected by gore attacks #define M_NO_LEVEL_ONE 156 // Monster uneffected by level 1 spells #define M_NO_LEVEL_TWO 157 // Monster uneffected by level 2 spells or less @@ -567,9 +566,9 @@ #define M_NAUSEATING_AURA 207 // Monster has a nauseating aura #define M_NEGATIVE_LIFE_AURA 208 // Monster has a negative life-draining aura #define M_TURBULENT_WIND_AURA 209 // Monster has a turbulent wind aura -// free 210 -// free 211 -// free 212 +#define M_ELECTRICAL_AURA 210 // Monster has a charged electrical aura +#define M_WATERY_AURA 211 +#define M_EARTHY_AURA 212 #define M_WOUNDING 213 // Monster has wounding attack // free 214 #define M_WILL_BERSERK 215 // Monster will berserk @@ -660,7 +659,7 @@ #define O_LAWFUL_ONLY 57 // Lawfuls Only #define O_TEMP_ENCHANT 58 // Temp Enchant #define O_STARTING 59 // Starting Item -// free 60 +#define O_NO_SMASH 60 // Cannot use smash ability with this object(weapon) #define O_CLAN_1 61 // Clan 1 can use #define O_CLAN_2 62 // Clan 2 can use #define O_CLAN_3 63 // Clan 3 can use @@ -697,10 +696,10 @@ #define O_EATABLE 92 // You can eat the object #define O_ALWAYS_DROPPED 93 // Monsters always drop this object #define O_CONSUME_HEAL 94 // Consumable object that will heal the player -// free 95 -// free 96 -// free 97 -// free 98 +#define O_SEL_GREYELF 95 // race selective: grey elf +#define O_SEL_WILDELF 96 // race selective: wild elf +#define O_SEL_DUERGAR 97 // race selective: duergar +#define O_SEL_OROG 98 // race selective: orog #define O_CLAN_11 99 // Clan 11 can use #define O_CLAN_12 100 // Clan 12 can use // free 101 @@ -713,12 +712,12 @@ #define O_CAN_HIT_MIST 108 // Object can hit misted vampire #define O_SILVER_OBJECT 109 // Object made of silver - extra damage to werewolf #define O_HOLD_BONUS 110 // Object confers a bonus to hit if held -// free 111 +#define O_ENHANCE_SLAM 111 // Object/weapon enhances slam attacks #define O_NO_STEAL 112 // Object cannot be stolen #define O_SMALL_SHIELD 113 // Object is a small shield -// free 114 -// free 115 -// free 116 +#define O_NO_SLAM 114 // Object cannot be used to slam (override normally allowed) +#define O_CAN_USE_SLAM 115 // Can use slam ability with object (override normal restricts) +#define O_CAN_USE_SMASH 116 // Can use smash ability with object #define O_SEL_KENKU 117 // race selective: kenku #define O_NO_PAWN 118 // Object not pawnable #define O_BREAK_ON_DROP 119 // Object breaks when drops @@ -871,5 +870,9 @@ #define X_NO_KNOCK_SPELL 96 // Exit immune to knock spell #define X_SEL_KENKU 97 // race selective: kenku #define X_NO_REMEMBER 98 // Unable to remember this exit -#define MAX_EXIT_FLAGS 99 // Incriment when adding... check structs.h for max +#define X_SEL_GREYELF 99 // race selective: grey elf +#define X_SEL_WILDELF 100 // race selective: wild elf +#define X_SEL_DUERGAR 101 // race selective: duergar +#define X_SEL_OROG 102 // race selective: orog +#define MAX_EXIT_FLAGS 103 // Incriment when adding... check structs.h for max diff --git a/include/global.hpp b/include/global.hpp index 4ed23707..1a8ea265 100644 --- a/include/global.hpp +++ b/include/global.hpp @@ -19,6 +19,8 @@ #pragma once #include "enums/bits.hpp" +#include +#include #define MAX_DIMEN_ANCHORS 5 @@ -159,7 +161,10 @@ enum Alignments { ROYALBLUE = 4 }; -#define P_TOP_ROYALBLUE 1000 +#define MAX_ALIGN 1000 +#define MIN_ALIGN -1000 + +#define P_TOP_ROYALBLUE MAX_ALIGN #define P_BOTTOM_ROYALBLUE 856 #define P_TOP_BLUE 855 #define P_BOTTOM_BLUE 399 @@ -176,9 +181,9 @@ enum Alignments { #define P_TOP_RED -399 #define P_BOTTOM_RED -855 #define P_TOP_BLOODRED -856 -#define P_BOTTOM_BLOODRED -1000 +#define P_BOTTOM_BLOODRED MIN_ALIGN -#define M_TOP_ROYALBLUE 1000 +#define M_TOP_ROYALBLUE MAX_ALIGN #define M_BOTTOM_ROYALBLUE 558 #define M_TOP_BLUE 557 #define M_BOTTOM_BLUE 299 @@ -195,7 +200,7 @@ enum Alignments { #define M_TOP_RED -299 #define M_BOTTOM_RED -557 #define M_TOP_BLOODRED -558 -#define M_BOTTOM_BLOODRED -1000 +#define M_BOTTOM_BLOODRED MIN_ALIGN // Attack types @@ -272,10 +277,26 @@ enum Languages { LFIRBOLG = 33, LSATYR = 34, LQUICKLING = 35, - - LANGUAGE_COUNT = 36 + LUNDERCOMMON = 36, + LSVIRFNEBLIN = 37, + LDRACONIC = 38, + LPRIMORDIAL = 39, + LTHRIKREEN = 40, + LSYLVAN = 41, + LGITH = 42, + LSPHINX = 43, + LPIXIE = 44, + LLEPRECHAUN = 45, + LNECRIL = 46, + LMODRON = 47, + LNOCTIS = 48, + LGRUGACH = 49, + + LANGUAGE_COUNT = 50 }; +extern const std::map languageMap; + // positions in the color array enum CustomColor { CUSTOM_COLOR_BROADCAST = 0, @@ -380,8 +401,12 @@ enum Races { KATARAN = 19, TIEFLING = 20, KENKU = 21, + GREYELF = 22, + DUERGAR = 23, + WILDELF = 24, + OROG = 25, - MAX_PLAYABLE_RACE = 22, + MAX_PLAYABLE_RACE = 26, // non-playable LIZARDMAN = 33, @@ -390,10 +415,10 @@ enum Races { // subraces, currently non-playable HALFFROSTGIANT = 35, HALFFIREGIANT = 36, - GREYELF = 37, - WILDELF = 38, + //GREYELF = 37, + //WILDELF = 38, AQUATICELF = 39, - DUERGAR = 40, + //DUERGAR = 40, HILLDWARF = 41, // non-playable diff --git a/include/login.hpp b/include/login.hpp index 9c755237..259222ec 100644 --- a/include/login.hpp +++ b/include/login.hpp @@ -81,6 +81,7 @@ typedef enum { CREATE_GET_SEX, CREATE_GET_RACE, CREATE_GET_SUBRACE, + CREATE_GET_VERIFY_RACE, CREATE_GET_CLASS, CREATE_GET_DEITY, CREATE_START_LOC, @@ -146,6 +147,7 @@ namespace Create { bool getSex(const std::shared_ptr& sock, std::string str, int mode); bool getRace(const std::shared_ptr& sock, std::string str, int mode); bool getSubRace(const std::shared_ptr& sock, std::string str, int mode); + bool getVerifyRace(const std::shared_ptr& sock, std::string str, int mode); void finishRace(const std::shared_ptr& sock); bool getClass(const std::shared_ptr& sock, std::string str, int mode); bool getDeity(const std::shared_ptr& sock, std::string str, int mode); @@ -181,3 +183,5 @@ void login(std::shared_ptr sock, const std::string& inStr); void createPlayer(std::shared_ptr sock, const std::string& str); void doSurname(std::shared_ptr sock, const std::string& str); void doTitle(std::shared_ptr sock, const std::string& str); +std::string getRacialBonusesString(short race, bool full=false); +bool usePredefinedStatsUnavailable(short race); diff --git a/include/magic.hpp b/include/magic.hpp index b0a2136b..8360cb37 100644 --- a/include/magic.hpp +++ b/include/magic.hpp @@ -63,6 +63,8 @@ enum DomainOfMagic { TRICKERY, TRAVEL, CREATION, + DAY, + NIGHT, NO_DOMAIN, @@ -272,8 +274,12 @@ std::string realmSkill(Realm realm); #define S_HOLD_PLANT 173 // hold-plant spell #define S_HOLD_ELEMENTAL 174 // hold-elemental spell #define S_HOLD_FEY 175 // hold-fey +#define S_LIGHT 176 // light spell +#define S_SHIELD 177 // shield spell +#define S_EMPATHY 178 // empathy spell -#define MAXSPELL 176 // Increment when you add a spell + +#define MAXSPELL 179 // Increment when you add a spell #define SpellFn const std::shared_ptr&, cmd*, SpellData* @@ -287,6 +293,7 @@ DomainOfMagic get_spell_domain(int nIndex); // spells int splAnnulMagic(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splArmor(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); +int splShield(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splAuraOfFlame(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splBarrierOfCombustion(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splBind(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); @@ -358,7 +365,9 @@ int splInvisibility(const std::shared_ptr& player, cmd* cmnd, SpellDat int splJudgement(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splKnock(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splKnowAura(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); +int splEmpathy(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splLevitate(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); +int splLight(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splMagicMissile(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splMendWounds(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); int splNecroDrain(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData); @@ -437,10 +446,14 @@ bool canEnchant(const std::shared_ptr& player, SpellData* spellData); bool canEnchant(const std::shared_ptr& player, std::shared_ptr object); bool decEnchant(const std::shared_ptr& player, CastType how); -int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData, const char* article, const char* spell, const std::string &effect, int strength=-2, long duration=-2); +int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData, const char* article, const char* spell, const std::string &effect, int strength=-2, long duration=-2, int maxStr=0); bool checkRefusingMagic(const std::shared_ptr& player, const std::shared_ptr& target, bool healing=false, bool print=true); bool replaceCancelingEffects(const std::shared_ptr& player, const std::shared_ptr& target, const std::string &effect); int cmdDispel(const std::shared_ptr& player, cmd* cmnd); bool isResistableEnchantment(const std::string spell); +//Innate racial magical abilities +int cmdInnateLevitate(const std::shared_ptr& player, cmd* cmnd); +int cmdInnateInvisible(const std::shared_ptr& player, cmd* cmnd); + diff --git a/include/mud.hpp b/include/mud.hpp index e3b66888..d3246038 100644 --- a/include/mud.hpp +++ b/include/mud.hpp @@ -60,9 +60,11 @@ class LastTime; #define DL_SILENCE 12 #define DL_HARM 13 // Daily harm casts #define DL_LEVITATE 14 // Daily innate levitates +#define DL_INVISIBLE 15 // Daily innate invisibles +#define DL_DEAFNESS 16 // Daily deafness -#define DAILYLAST 14 +#define DAILYLAST 16 // Object Last-time stuff #define LT_ENCHA 0 @@ -89,10 +91,10 @@ class LastTime; #define LT_TURN 15 #define LT_FRENZY 16 #define LT_INNATE 17 -// free 18 +#define LT_BASH 18 #define LT_PRAY 19 #define LT_PREPARE 20 -// free 21 +#define LT_SLAM 21 #define LT_PLAYER_SAVE 22 // free 23 // free 24 @@ -102,7 +104,7 @@ class LastTime; #define LT_AGE 28 // free 29 // free 30 -// free 31 +#define LT_SMASH 31 #define LT_SONG_PLAYED 33 #define LT_SING 35 #define LT_CHARMED 36 diff --git a/include/mudObjects/creatures.hpp b/include/mudObjects/creatures.hpp index 0ac5be07..ca26111d 100644 --- a/include/mudObjects/creatures.hpp +++ b/include/mudObjects/creatures.hpp @@ -60,6 +60,8 @@ enum AttackType { ATTACK_MAUL, ATTACK_KICK, ATTACK_GORE, + ATTACK_SMASH, + ATTACK_SLAM, ATTACK_TYPE_MAX }; @@ -598,6 +600,10 @@ class Creature: public virtual MudObject, public Streamable, public Container, p void makeWerewolf(); bool willBecomeWerewolf() const; bool addLycanthropy(const std::shared_ptr&killer, int chance); + bool resistantToPoison() const; + bool resistantToDisease() const; + bool isSimpleMinded() const; + int getWoundingChance(); // Get @@ -629,6 +635,8 @@ class Creature: public virtual MudObject, public Streamable, public Container, p int getWisdom(); int getAgility(); int getElusiveness(); + int getBrutality(); + int getDefenseSkillModifier() const; // Set void setClass(CreatureClass c); // * diff --git a/include/mudObjects/monsters.hpp b/include/mudObjects/monsters.hpp index dd864090..6dc9bc26 100644 --- a/include/mudObjects/monsters.hpp +++ b/include/mudObjects/monsters.hpp @@ -58,6 +58,7 @@ class Monster : public Creature { unsigned short updateAggro{}; unsigned short loadAggro{}; unsigned short numwander{}; + unsigned short permSpawnChance{}; unsigned short magicResistance{}; unsigned short mobTrade{}; int skillLevel{}; @@ -181,6 +182,7 @@ class Monster : public Creature { unsigned short getUpdateAggro() const; unsigned short getCastChance() const; unsigned short getMagicResistance() const; + unsigned short getPermSpawnChance() const; std::string getPrimeFaction() const; std::string getTalk() const; int getWeaponSkill(const std::shared_ptr weapon = nullptr) const; @@ -189,8 +191,10 @@ class Monster : public Creature { bool getEnchantmentVsMontype(const std::shared_ptr& caster, const std::string spell, bool print=false); void doCheckFailedCastAggro(const std::shared_ptr& caster, bool willAggro, bool print); std::shared_ptr findScavengedObject(); + std::shared_ptr findObjectToDissolve(); std::shared_ptr findObjectToScavenge(); int countScavengedObjects(); + bool hasAcidDissolveAttack() const; // Set void setMaxLevel(unsigned short l); @@ -199,6 +203,7 @@ class Monster : public Creature { void setLoadAggro(unsigned short a); void setUpdateAggro(unsigned short a); void setNumWander(unsigned short n); + void setPermSpawnChance(unsigned short n); void setSkillLevel(int l); void setMobTrade(unsigned short t); void setPrimeFaction(std::string_view f); @@ -219,7 +224,8 @@ class Monster : public Creature { void checkSpellWearoff(); void checkScavange(long t); int checkWander(long t); - bool canScavange(const std::shared_ptr& object); + bool canScavange(const std::shared_ptr& object, bool scavengeHiddenObjects=false); + void printAuraDmgMsg(const std::shared_ptr& player, const char* color, const char* text, int dmg); bool doTalkAction(const std::shared_ptr& target, std::string action, QuestInfo* quest = nullptr); diff --git a/include/mudObjects/objects.hpp b/include/mudObjects/objects.hpp index 3f0442da..079a4d52 100644 --- a/include/mudObjects/objects.hpp +++ b/include/mudObjects/objects.hpp @@ -192,9 +192,11 @@ class Object: public Container, public Containable { short chargesMax; // Maximum charges (Currently only used for casting weapons) short chargesCur; short magicpower; + unsigned short castChance; short level; int requiredSkill; // Required amount of skill gain to use this object short minStrength; + unsigned short permSpawnChance; short clan; short special; short questnum; // Quest fulfillment number @@ -338,9 +340,11 @@ class Object: public Container, public Containable { [[nodiscard]] short getChargesMax() const; [[nodiscard]] short getChargesCur() const; [[nodiscard]] short getMagicpower() const; + [[nodiscard]] unsigned short getCastChance() const; [[nodiscard]] short getLevel() const; [[nodiscard]] int getRequiredSkill() const; [[nodiscard]] short getMinStrength() const; + [[nodiscard]] unsigned short getPermSpawnChance() const; [[nodiscard]] short getClan() const; [[nodiscard]] short getSpecial() const; [[nodiscard]] short getQuestnum() const; @@ -401,10 +405,12 @@ class Object: public Container, public Containable { void setChargesMax(short s); void setChargesCur(short s); void setMagicpower(short m); + void setCastChance(unsigned short s); void setMagicrealm(short m); void setLevel(short l); void setRequiredSkill(int s); void setMinStrength(short s); + void setPermSpawnChance(unsigned short s); void setClan(short c); void setSpecial(short s); void setQuestnum(short q); @@ -465,6 +471,12 @@ class Object: public Container, public Containable { [[nodiscard]] bool isGemstone() const; [[nodiscard]] bool isSilver() const; [[nodiscard]] bool isDarkmetal() const; + [[nodiscard]] bool isMetal() const; + [[nodiscard]] bool isFerrousMetal() const; + [[nodiscard]] bool isNonFerrousMetal() const; + [[nodiscard]] bool isOrganic() const; + [[nodiscard]] bool isStone() const; + [[nodiscard]] bool isFlammable() const; [[nodiscard]] bool isTrashAtPawn(Money value) const; bool swap(const Swap& s); diff --git a/include/mudObjects/players.hpp b/include/mudObjects/players.hpp index 0e9ee1d8..1efc55fe 100644 --- a/include/mudObjects/players.hpp +++ b/include/mudObjects/players.hpp @@ -202,6 +202,7 @@ class Player : public Creature { void gainExperience(const std::shared_ptr &victim, const std::shared_ptr &killer, int expAmount, bool groupExp = false) override; void disarmSelf(); bool lagProtection(); + int getXPModifiers() const; void computeAC(); void alignAdjustAcThaco(); @@ -459,6 +460,7 @@ class Player : public Creature { int computeLuck(); int getArmorWeight() const; + bool checkClimbing(); int getFallBonus(); int getSneakChance(); int getLight() const; diff --git a/include/playerClass.hpp b/include/playerClass.hpp index 3a5e904c..5d2e7d6a 100644 --- a/include/playerClass.hpp +++ b/include/playerClass.hpp @@ -40,6 +40,7 @@ class PlayerClass { short getBaseMp(); bool needsDeity(); short numProfs(); + int getXPAdjustment(); std::list::const_iterator getSkillBegin(); std::list::const_iterator getSkillEnd(); std::map::const_iterator getLevelBegin(); @@ -71,6 +72,7 @@ class PlayerClass { bool needDeity; short numProf; + short xpAdjust; // Skills they start with std::list baseSkills; // Stuff gained on each additional level diff --git a/include/proto.hpp b/include/proto.hpp index 21eb7d23..79d1ff93 100644 --- a/include/proto.hpp +++ b/include/proto.hpp @@ -308,6 +308,8 @@ std::vector splitString(std::string s, std::string delimiter = ""); std::string joinVector(std::vector v, std::string delimiter = ""); int getIntFromStr(std::string& someString); std::string stripNonDigits(std::string someString); +std::string stripSpaces(std::string someString); +std::string toLower(const std::string& str); bool nameIsAllowed(std::string str, const std::shared_ptr& sock); int bonus(int num); @@ -365,6 +367,7 @@ void etherealTravel(std::shared_ptr player); void sendMail(const std::string &target, const std::string &message); +void sendSystemNotice(const std::string &target, const std::string &message); // room.cpp diff --git a/include/raceData.hpp b/include/raceData.hpp index 6e14d745..80638ee7 100644 --- a/include/raceData.hpp +++ b/include/raceData.hpp @@ -40,6 +40,7 @@ class RaceData { int startAge; bool playable; + int xpAdjustment; bool isParentRace; bool gendered; int parentRace; @@ -75,6 +76,7 @@ class RaceData { [[nodiscard]] bool bonusStat() const; [[nodiscard]] Size getSize() const; [[nodiscard]] int getStartAge() const; + [[nodiscard]] int getXPAdjustment() const; [[nodiscard]] int getStatAdj(int stat) const; [[nodiscard]] int getSave(int save) const; [[nodiscard]] short getPorphyriaResistance() const; diff --git a/include/version.hpp b/include/version.hpp index 69756cfc..87a9dbc2 100644 --- a/include/version.hpp +++ b/include/version.hpp @@ -21,7 +21,7 @@ #define VERSION_MINOR "6" -#define VERSION_SUB "2b" +#define VERSION_SUB "3" #define VERSION VERSION_MAJOR "." VERSION_MINOR VERSION_SUB diff --git a/json/objects-json.cpp b/json/objects-json.cpp index 552e4d7c..9ee2165d 100644 --- a/json/objects-json.cpp +++ b/json/objects-json.cpp @@ -71,6 +71,7 @@ void to_json(nlohmann::json &j, const Object &obj, bool permOnly, LoadType saveT {"subType", obj.subType}, {"wearFlag", obj.wearflag}, {"magicPower", obj.magicpower}, + {"castCjamce", obj.castChance}, {"level", obj.level}, {"quality", obj.quality}, {"requiredSkill", obj.requiredSkill}, @@ -85,6 +86,7 @@ void to_json(nlohmann::json &j, const Object &obj, bool permOnly, LoadType saveT {"keyVal", obj.keyVal}, {"material", obj.material}, {"minStrength", obj.minStrength}, + {"permSpawnChance", obj.permSpawnChance}, {"inBag", obj.in_bag} }); diff --git a/magic/divine/healmagic.cpp b/magic/divine/healmagic.cpp index dce7e761..d1418bf7 100644 --- a/magic/divine/healmagic.cpp +++ b/magic/divine/healmagic.cpp @@ -38,6 +38,7 @@ #include "mudObjects/creatures.hpp" // for Creature #include "mudObjects/monsters.hpp" // for Monster #include "mudObjects/players.hpp" // for Player +#include "mudObjects/objects.hpp" // for Object #include "mudObjects/rooms.hpp" // for BaseRoom #include "proto.hpp" // for broadcast, bonus, dec_daily, dice #include "random.hpp" // for Random @@ -862,6 +863,13 @@ int splHeal(const std::shared_ptr& player, cmd* cmnd, SpellData* spell creature->print("%M casts a heal spell on you.\n", player.get()); broadcast(player->getSock(), creature->getSock(), player->getRoomParent(), "%M casts a heal spell on %N.", player.get(), creature.get()); + // Heal also removes wounded (bleeding) effect, diseases, and poisons + creature->curePoison(); + creature->cureDisease(); + creature->removeCurse(); + creature->removeEffect("blindness"); + creature->removeEffect("deafness"); + logCast(player, creature, "heal"); return(1); } @@ -1163,21 +1171,27 @@ int splBloodfusion(const std::shared_ptr& player, cmd* cmnd, SpellData int splRestore(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData) { std::shared_ptr target=nullptr; + int strength = 0; if(spellData->how == CastType::CAST && player->isPlayer() && !player->isStaff()) { - player->print("You may not cast that spell.\n"); + *player << "The intricate casting of restore eludes you and is left to the gods.\nOnly wands or scrolls can cast it, or when imbibed, certain potions can.\n"; return(0); } + if (spellData->object) + strength = (spellData->object->getLevel()>0?spellData->object->getLevel():10); + else + strength = player->getLevel(); + // Cast restore on self if(cmnd->num == 2) { target = player; - if(spellData->how == CastType::CAST || spellData->how == CastType::WAND) { - player->print("Restore spell cast.\n"); + if(spellData->how == CastType::CAST || spellData->how == CastType::WAND || spellData->how == CastType::SCROLL) { + *player << "Restore spell cast.\n"; broadcast(player->getSock(), player->getParent(), "%M casts restore on %sself.", player.get(), player->himHer()); } else if(spellData->how == CastType::POTION) - player->print("You feel restored.\n"); + *player << "You feel restored.\n"; // Cast restore on another player } else { @@ -1188,28 +1202,54 @@ int splRestore(const std::shared_ptr& player, cmd* cmnd, SpellData* sp target = player->getParent()->findCreature(player, cmnd->str[2], cmnd->val[2], false); if(!target) { - player->print("That person is not here.\n"); + *player << "That person is not here.\n"; return(0); } - player->print("Restore spell cast on %N.\n", target.get()); - target->print("%M casts a restore spell on you.\n", player.get()); + *player << "Restore spell cast on " << target << ".\n"; + *target << setf(CAP) << player << " casts a restore spell on you.\n"; broadcast(player->getSock(), target->getSock(), player->getParent(), "%M casts a restore spell on %N.", player.get(), target.get()); logCast(player, target, "restore"); } - player->doHeal(target, dice(2, 10, 0)); - player->removeEffect("death-sickness"); - if(Random::get(.34)) - target->mp.restore(); + const bool isStaff = player->isStaff(); + + // Helper for 34% rolls + auto roll34 = []() { return Random::get(0.34); }; - if(player->isStaff()) { + if (isStaff) { target->mp.restore(); target->hp.restore(); + target->cureDisease(); + target->curePoison(); + target->removeCurse(); + } else { + if (roll34()) target->mp.restore(); + if (roll34()) target->cureDisease(); + if (roll34()) target->curePoison(); + if (roll34()) target->removeCurse(); } + // Effects via removeEffect(string) + static const std::vector kRestorableEffects = { + "death-sickness", "silence", "deafness", "blindness", "petrification" + }; + for (const auto& e : kRestorableEffects) { + if (isStaff || roll34()) { + target->removeEffect(e); + } + } + + // Healing for non-staff casters + if (!isStaff) { + player->doHeal(target, dice(4, strength, std::max(2, strength / 2))); + } + + // Always removes death-sickness + target->removeEffect("death-sickness"); + return(1); } diff --git a/magic/magic.cpp b/magic/magic.cpp index 9025a507..ffbfa161 100644 --- a/magic/magic.cpp +++ b/magic/magic.cpp @@ -60,8 +60,8 @@ struct { { "zephyr", S_ZEPHYR, (int (*)(SpellFn))splOffensive, -1, EVOCATION, DESTRUCTION }, { "infravision", S_INFRAVISION, splInfravision, -1, TRANSMUTATION, AUGMENTATION}, { "slow-poison", S_SLOW_POISON, splSlowPoison, 8, NO_SCHOOL, HEALING }, - { "bless", S_BLESS, splBless, 10, ABJURATION, PROTECTION }, - { "protection", S_PROTECTION, splProtection, 10, ABJURATION, PROTECTION }, + { "bless", S_BLESS, splBless, -1, ABJURATION, PROTECTION }, + { "protection", S_PROTECTION, splProtection, -1, ABJURATION, PROTECTION }, { "fireball", S_FIREBALL, (int (*)(SpellFn))splOffensive, -1, EVOCATION, DESTRUCTION }, { "invisibility", S_INVISIBILITY, splInvisibility, 15, ILLUSION, TRICKERY }, { "restore", S_RESTORE, splRestore, -1, SCHOOL_CANNOT_CAST, DOMAIN_CANNOT_CAST, }, @@ -180,7 +180,7 @@ struct { { "reduce", S_REDUCE, splReduce, 15, TRANSMUTATION, AUGMENTATION}, { "insight", S_INSIGHT, splInsight, 30, TRANSMUTATION, AUGMENTATION}, { "feeblemind", S_FEEBLEMIND, splFeeblemind, 30, TRANSMUTATION, AUGMENTATION}, - { "prayer", S_PRAYER, splPrayer, 30, TRANSMUTATION, AUGMENTATION}, + { "prayer", S_PRAYER, splPrayer, 30, SCHOOL_CANNOT_CAST, AUGMENTATION}, { "damnation", S_DAMNATION, splDamnation, 30, TRANSMUTATION, AUGMENTATION}, { "fortitude", S_FORTITUDE, splFortitude, 30, TRANSMUTATION, AUGMENTATION}, { "weakness", S_WEAKNESS, splWeakness, 30, TRANSMUTATION, AUGMENTATION}, @@ -233,6 +233,9 @@ struct { { "hold-plant", S_HOLD_PLANT, splHoldPlant, 15, ENCHANTMENT, TRICKERY }, { "hold-elemental", S_HOLD_ELEMENTAL, splHoldElemental, 35, ENCHANTMENT, TRICKERY }, { "hold-fey", S_HOLD_FEY, splHoldFey, 30, ENCHANTMENT, TRICKERY }, + { "light", S_LIGHT, splLight, 7, EVOCATION, DAY }, + { "shield", S_SHIELD, splShield, 15, ABJURATION, NO_DOMAIN }, + { "empathy", S_EMPATHY, splEmpathy, 15, DIVINATION, KNOWLEDGE }, { "@", -1, nullptr, 0, NO_SCHOOL, NO_DOMAIN } }; int spllist_size = sizeof(spllist)/sizeof(*spllist); @@ -595,58 +598,102 @@ bool checkRefusingMagic(const std::shared_ptr& player, const std::shar bool replaceCancelingEffects(const std::shared_ptr& player, const std::shared_ptr& target, const std::string &effect) { + if (!player || !target || effect.empty()) + return(false); + bool self = (player == target); - - // benediction vs malediction + + EffectInfo* tgtEffectInfo=nullptr; + + long effDuration = 0; + int effStrength = 0; + bool replace = false; + + std::string effect1, effect2; + if ((effect == "benediction" && target->isEffected("malediction")) || (effect == "malediction" && target->isEffected("benediction"))) { + effect1 = "benediction"; + effect2 = "malediction"; + replace = true; + } + else if ((effect == "light" && target->isEffected("darkness")) || + (effect == "darkness" && target->isEffected("light"))) { + effect1 = "light"; + effect2 = "darkness"; + replace = true; + } + + + if (replace) { + + if(effect1.empty() || effect2.empty()) { + if(effect1.empty()) *player << ColorOn << "^GERROR: effect1 is empty/undefined.\n"; + if(effect2.empty()) *player << ColorOn << "^GERROR: effect2 is empty/undefined.\n"; - EffectInfo* alignEffect=nullptr; - if (effect == "benediction") - alignEffect = target->getEffect("malediction"); + *player << "Aborting replacement routine.^x\n" << ColorOff; + + return(false); + } + + if (effect == effect1) + tgtEffectInfo = target->getEffect(effect2); else - alignEffect = target->getEffect("benediction"); - - int duration = alignEffect->getDuration(); - int strength = alignEffect->getStrength(); + tgtEffectInfo = target->getEffect(effect1); + + effDuration = tgtEffectInfo->getDuration(); + effStrength = tgtEffectInfo->getStrength(); + + if(tgtEffectInfo->isPermanent()) { + *player << "Permanent effects cannot be replaced.\n"; + return(true); + } if(self) { - *player << "Your " << ((effect == "benediction") ? "malediction":"benediction") << " was canceled by your " << ((effect == "benediction") ? "benediction":"malediction") << " spell.\n"; - broadcast(player->getSock(), player->getRoomParent(), "%M's %s spell has replaced %s %s.", player.get(), ((effect == "benediction") ? "malediction":"benediction"), player->hisHer(), ((effect == "benediction") ? "benediction":"malediction")); - player->removeEffect(((effect == "benediction") ? "malediction":"benediction")); + if (player->getLevel() < effStrength && player->getName() != tgtEffectInfo->getOwner() && !player->isCt()) { + *player << ColorOn << "^yYou are not powerful enough to replace the " << ((effect == effect1) ? effect2:effect1) << " spell effect on you.\nYour spell did not take hold.\n" << ColorOff; + broadcast(player->getSock(), target->getSock(), player->getRoomParent(), "%M's cast attempt failed.", player.get()); + return(true); + } + + *player << "The " << ((effect == effect1) ? effect2:effect1) << " effect on you was canceled by your " << ((effect == effect1) ? effect1:effect2) << " spell.\n"; + broadcast(player->getSock(), player->getRoomParent(), "%M's %s spell has replaced %s %s effect.", + player.get(), ((effect == effect1) ? effect2.c_str():effect1.c_str()), player->hisHer(), ((effect == effect1) ? effect1.c_str():effect2.c_str())); + + player->removeEffect(((effect == effect1) ? effect2:effect1)); //If under one, it is replaced by the other with the existing same duration and strength - player->addEffect(((effect == "benediction") ? "benediction":"malediction"),duration,strength,player,true); + player->addEffect(((effect == effect1) ? effect1:effect2),effDuration,effStrength,player,true,player); return(true); } else { + // Depending on effects involved, specific situational checks can go here // Can't allow people to remove somebody's room damage protection in the middle of fighting...That's just not nice.... - if (target->inCombat(true) && player->getRoomParent()->flagIsSet(((effect == "benediction") ? R_EVIL_DAMAGE:R_GOOD_DAMAGE))) { + if ((effect=="benediction" || effect=="malediction") && (target->inCombat(true) && player->getRoomParent()->flagIsSet(((effect == effect1) ? R_EVIL_DAMAGE:R_GOOD_DAMAGE)))) { *player << setf(CAP) << target << "'s erratic movements while fighting caused your spell to not take hold.\n"; return(true); } // Will use level difference check for now. Will change when put in more effect strength functionality for all the generic spells + // TODO: modify this section to account for opposing effects with useStrength() or useVariableStrength() if (player->getLevel() < target->getLevel() && !player->isCt()) { - *player << ColorOn << "^yYou must at least the same level as " << target << " to replace " << target->hisHer() << " " << ((effect == "benediction") ? "malediction":"benediction") << ".\nYour spell did not take hold.\n" << ColorOff; + *player << ColorOn << "^yYou must at least the same level as " << target << " to replace " << target->hisHer() << " " << ((effect == effect1) ? effect2:effect1) << ".\nYour spell did not take hold.\n" << ColorOff; *target << setf(CAP) << player << "'s spell did not do anything to you.\n"; broadcast(player->getSock(), target->getSock(), player->getRoomParent(), "%M's spell did not do anything to %N.", player.get(), target.get()); return(true); } - *player << "Your " << ((effect == "benediction") ? "benediction":"malediction") << " spell has replaced " << target << "'s " << ((effect == "benediction") ? "malediction":"benediction") << ".\n"; - *target << setf(CAP) << player << "'s " << ((effect == "benediction") ? "benediction":"malediction") << " spell has replaced your " << ((effect == "benediction") ? "malediction":"benediction") << ".\n"; + *player << "Your " << ((effect == effect1) ? effect1:effect2) << " spell has replaced " << target << "'s " << ((effect == effect1) ? effect2:effect1) << " effect.\n"; + *target << setf(CAP) << player << "'s " << ((effect == effect1) ? effect1:effect2) << " spell has replaced your " << ((effect == effect1) ? effect2:effect1) << " effect.\n"; broadcast(player->getSock(), target->getSock(), player->getRoomParent(), "%M's %s spell has replaced %N's %s effect.",player.get(), - ((effect == "benediction") ? "benediction":"malediction"), target.get(), ((effect == "benediction") ? "malediction":"benediction")); - target->removeEffect(((effect == "benediction") ? "malediction":"benediction")); + ((effect == effect1) ? effect1.c_str():effect2.c_str()), target.get(), ((effect == effect1) ? effect2.c_str():effect1.c_str())); + target->removeEffect(((effect == effect1) ? effect2:effect1)); //If under one, it is replaced by the other with the existing same duration and strength - target->addEffect(((effect == "benediction") ? "benediction":"malediction"),duration,strength,player,true); + target->addEffect(((effect == effect1) ? effect1:effect2),effDuration,effStrength,player,true,player); return(true); } - } - //TODO: Add other canceling effects that replace here if necessary return(false); } diff --git a/magic/magic1.cpp b/magic/magic1.cpp index 77bb7541..e28ffcd1 100644 --- a/magic/magic1.cpp +++ b/magic/magic1.cpp @@ -204,7 +204,7 @@ int cmdDispel(const std::shared_ptr& player, cmd* cmnd) { } effect = toDispel->getEffect(); - if(effect->getType() != "Positive") { + if(effect->getType() != "Positive" && player->getName() != toDispel->getOwner()) { *player << "On your person, only positive/beneficial effects may be dispelled.\n"; return(0); } @@ -1719,13 +1719,14 @@ bool noCastUndead(std::string_view effect) { // splGeneric //********************************************************************* -int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData, const char* article, const char* spell, const std::string &effect, int strength, long duration) { +int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData, const char* article, const char* spell, const std::string &effect, int strength, long duration, int maxStr) { std::shared_ptr target=nullptr; + if (spellData->object) strength = (spellData->object->getLevel() > 0 ? spellData->object->getLevel():10); else - strength = spellData->level; + strength = strength>0?strength:spellData->level; if(cmnd->num == 2) { target = player; @@ -1736,8 +1737,21 @@ int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* sp ) return(0); + if(player->isEffected(effect) && player->getName() != player->getEffect(effect)->getOwner()) { + const Effect* playerEffect = player->getEffect(effect)->getEffect(); + if ( playerEffect && ((playerEffect->usesVariableStrength() && maxStr < player->getEffect(effect)->getStrength()) || + (playerEffect->usesStrength() && strength < player->getEffect(effect)->getStrength())) ) { + if(isPtester(player)) { + *player << ColorOn << "^DEffect: " << effect << "\nusesVariableStrength() = " << (playerEffect->usesVariableStrength()?"true":"false") << "\nmaxStr = " << maxStr << "\n"; + *player << "effect owner = " << player->getEffect(effect)->getOwner() << "\nusesStrength() = " << (playerEffect->usesStrength()?"true":"false") << "\nstrength = " << strength << "^x\n" << ColorOff; + } + *player << ColorOn << "^c" << "The " << spell << " spell effect currently on you is too powerful for you to overpower.^x\n" << ColorOff; + return(0); + } + } + if(spellData->how == CastType::CAST) { - player->print("You cast %s %s spell.\n", article, spell); + *player << "You cast " << article << " " << spell << " spell.\n"; broadcast(player->getSock(), player->getParent(), "%M casts %s %s spell.", player.get(), article, spell); } @@ -1749,7 +1763,7 @@ int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* sp target = player->getParent()->findCreature(player, cmnd->str[2], cmnd->val[2], false); if(!target) { - player->print("You don't see that player here.\n"); + *player << "You don't see that target here.\n"; return(0); } @@ -1761,7 +1775,6 @@ int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* sp if(checkRefusingMagic(player, target)) return(0); - if (((effect == "benediction" && (target->isPlayer()?target->getAdjustedAlignment():target->getAsMonster()->getAdjustedAlignment()) < NEUTRAL) || (effect == "malediction" && (target->isPlayer()?target->getAdjustedAlignment():target->getAsMonster()->getAdjustedAlignment()) > NEUTRAL)) && !player->isCt()) { *player << setf(CAP) << target << " must be of " << ((effect=="benediction")?"good":"evil") << " or neutral alignment in order to receive that spell.\n"; @@ -1786,15 +1799,28 @@ int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* sp return(0); } - - if((effect == "drain-shield" || effect == "undead-ward") && target->isUndead()) { - player->print("The spell fizzles.\n%M naturally resisted your spell.\n", target.get()); + if ((effect == "drain-shield" || effect == "undead-ward") && target->isUndead() && !player->isCt()) { + *player << "Your spell fizzled with a loud bang! You cannot cast " << article << " " << spell << " spell on the undead.\n"; return(0); } + + if(target->isEffected(effect) && player->getName() != target->getEffect(effect)->getOwner()) { + const Effect* targetEffect = target->getEffect(effect)->getEffect(); + if ( targetEffect && ((targetEffect->usesVariableStrength() && maxStr < target->getEffect(effect)->getStrength()) || + (targetEffect->usesStrength() && strength < target->getEffect(effect)->getStrength())) ) { + if(isPtester(player)) { + *player << ColorOn << "^DEffect: " << effect << "\nusesVariableStrength() = " << (targetEffect->usesVariableStrength()?"true":"false") << "\nmaxStr = " << maxStr << "\n"; + *player << "effect owner = " << target->getEffect(effect)->getOwner() << "\nusesStrength() = " << (targetEffect->usesStrength()?"true":"false") << "\nstrength = " << strength << "^x\n" << ColorOff; + } + *player << ColorOn << "^c" << "The " << spell << " spell currently on " << target << " is too powerful for you to replace.^x\n" << ColorOff; + return(0); + } + } + broadcast(player->getSock(), target->getSock(), player->getParent(), "%M casts %s %s spell on %N.", player.get(), article, spell, target.get()); - target->print("%M casts %s on you.\n", player.get(), spell); - player->print("You cast %s %s spell on %N.\n", article, spell, target.get()); + *target << setf(CAP) << player << " casts " << article << " " << spell << " spell on you.\n"; + *player << "You cast " << article << " " << spell << " spell on " << target << ".\n"; } @@ -1806,7 +1832,7 @@ int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* sp (effect == "heat-protection" && target->hasPermEffect("alwayswarm")) || (effect == "warmth" && target->hasPermEffect("alwayscold")) ) { - player->print("The spell didn't take hold.\n"); + *player << "The spell didn't take hold.\n"; return(0); } @@ -1814,14 +1840,15 @@ int splGeneric(const std::shared_ptr& player, cmd* cmnd, SpellData* sp if((effect == "drain-shield" || effect == "undead-ward") && target->isEffected("porphyria")) target->removeEffect("porphyria"); - if (replaceCancelingEffects(player,target,effect)) - return(0); - + if (replaceCancelingEffects(player,target,effect)) + return(1); if(spellData->how == CastType::CAST) { - if(player->getRoomParent()->magicBonus()) - player->print("The room's magical properties increase the power of your spell.\n"); - if(!target->addEffect(effect, duration, strength, player, true)) + if(player->getRoomParent()->magicBonus()) { + *player << "The room's magical properties increase the power of your spell.\n"; + duration += duration>0?(duration*30)/100:0; + } + if(!target->addEffect(effect, duration, strength, player, true, player)) return(0); } else { target->addEffect(effect, duration, strength, nullptr, true); @@ -2375,11 +2402,11 @@ bool Creature::noPotion(SpellData* spellData) const { //********************************************************************* -// innateLevitate +// cmdInnateLevitate //********************************************************************* // Innate racial ability to levitate self on command -int innateLevitate(const std::shared_ptr& player, cmd* cmnd) { +int cmdInnateLevitate(const std::shared_ptr& player, cmd* cmnd) { long i,t; player->clearFlag(P_AFK); @@ -2389,6 +2416,11 @@ int innateLevitate(const std::shared_ptr& player, cmd* cmnd) { return(0); } + if(player->isEffected("levitate")) { + *player << "You are already levitating!\n"; + return(0); + } + // Check for daily limit here if( !dec_daily(&player->daily[DL_LEVITATE]) && !player->isCt()) { *player << "You have used your innate levitation enough times for today.\n"; @@ -2408,7 +2440,7 @@ int innateLevitate(const std::shared_ptr& player, cmd* cmnd) { return(0); player->lasttime[LT_INNATE].ltime = t; - player->lasttime[LT_INNATE].interval = 120L; + player->lasttime[LT_INNATE].interval = 60L; if(player->isStaff()) player->lasttime[LT_INNATE].interval = 1; @@ -2418,7 +2450,62 @@ int innateLevitate(const std::shared_ptr& player, cmd* cmnd) { *player << "You call upon your innate ability to levitate.\n"; if(!player->flagIsSet(P_DM_INVIS)) broadcast(player->getSock(), player->getRoomParent(), "%M calls upon %s innate ability to levitate.", player.get(), player->hisHer()); - player->addEffect("levitate", 600, player->getLevel(), player, true); + player->addEffect("levitate", 450, player->getLevel(), player, true); + + return(0); + +} + +//********************************************************************* +// cmdInnateInvisible +//********************************************************************* +// Innate racial ability to turn invisible on command + +int cmdInnateInvisible(const std::shared_ptr& player, cmd* cmnd) { + long i,t; + + player->clearFlag(P_AFK); + + if (!player->isCt() && player->getRace() != DUERGAR) { + *player << "You do not have the innate ability to turn invisible.\n"; + return(0); + } + + if(player->isEffected("invisibility")) { + *player << "You are already invisible!\n"; + return(0); + } + + // Check for daily limit here + if( !dec_daily(&player->daily[DL_INVISIBLE]) && !player->isCt()) { + *player << "You have used your innate invisibility enough times for today.\n"; + return(0); + } + + t = time(nullptr); + i = LT(player, LT_INNATE); + + if(!player->isStaff() && t < i) { + *player << "You are not able to call another innate ability yet.\n"; + player->pleaseWait(i-t); + return(0); + } + + if(player->getRoomParent()->flagIsSet(R_NO_MAGIC) && !player->checkStaff("Your innate abilities will not work here.\n")) + return(0); + + player->lasttime[LT_INNATE].ltime = t; + player->lasttime[LT_INNATE].interval = 60L; + + if(player->isStaff()) + player->lasttime[LT_INNATE].interval = 1; + + player->interruptDelayedActions(); + + *player << "You call upon your innate ability to to turn invisible.\n"; + if(!player->flagIsSet(P_DM_INVIS)) + broadcast(player->getSock(), player->getRoomParent(), "%M calls upon %s innate invisibility.", player.get(), player->hisHer()); + player->addEffect("invisibility", 900, player->getLevel(), player, true); return(0); diff --git a/magic/schools/abjuration.cpp b/magic/schools/abjuration.cpp index ceae8956..285b0c14 100644 --- a/magic/schools/abjuration.cpp +++ b/magic/schools/abjuration.cpp @@ -39,11 +39,13 @@ #include "mudObjects/monsters.hpp" // for Monster #include "mudObjects/players.hpp" // for Player #include "mudObjects/rooms.hpp" // for BaseRoom +#include "mudObjects/objects.hpp" // for Object, ObjectType, ObjectType... #include "proto.hpp" // for broadcast, bonus, up, broadcastG... #include "random.hpp" // for Random #include "server.hpp" // for Server, gServer #include "statistics.hpp" // for Statistics #include "stats.hpp" // for Stat +#include "commands.hpp" // for isPtester() //********************************************************************* // protection from room damage spells @@ -74,11 +76,99 @@ int splStaticField(const std::shared_ptr& player, cmd* cmnd, SpellData // splProtection //********************************************************************* // This function allows a spellcaster to cast a protection spell either -// on themself or on another player, improving the armor class by a -// score of 10. +// on themself or on another player, improving defense depending on the +// strength of the caster. Duration and strength of the "protection" +// effect scale with abjuration skill, level of the caster, and either +// piety or intelligence, depending on what class is casting it. int splProtection(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData) { - return(splGeneric(player, cmnd, spellData, "a", "protection", "protection")); + + + int mpNeeded=0; + double multiplier=1.0; + + int level = player->isMonster()?player->getLevel():player->getAsPlayer()->getSkillLevel("abjuration"); + + if(level <= 10) + mpNeeded = 10; + else if (level <= 20) + mpNeeded = 15; + else if (level <= 30) + mpNeeded = 20; + else if (level <= 40) + mpNeeded = 25; + + if(!player->isMonster() && player->spellFail(spellData->how)) { + if(spellData->how == CastType::CAST) + player->subMp(mpNeeded/2); + return(0); + } + + if(spellData->how == CastType::CAST && !player->checkMp(mpNeeded)) + return(0); + + int attrib = (player->piety.getCur()>player->intelligence.getCur()?player->piety.getCur():player->intelligence.getCur()); + + switch(player->getClass()) { + case CreatureClass::CLERIC: + case CreatureClass::DRUID: + multiplier += 0.15; + attrib = player->piety.getCur(); + break; + case CreatureClass::LICH: + case CreatureClass::MAGE: + multiplier += 0.25; + attrib = player->intelligence.getCur(); + break; + case CreatureClass::PALADIN: + case CreatureClass::DEATHKNIGHT: + multiplier += 0.1; + attrib = player->piety.getCur(); + break; + case CreatureClass::FIGHTER: + case CreatureClass::THIEF: + if(player->isPlayer() && player->getAsPlayer()->getSecondClass() == CreatureClass::MAGE) { + multiplier += 0.125; + attrib = player->intelligence.getCur(); + } + break; + default: + break; + } + + switch(player->getRace()) { + case GREYELF: + case ELF: + case SERAPH: + case TIEFLING: + case CAMBION: + multiplier += 0.05; + break; + default: + break; + } + + int abjuration = (player->isPlayer()?(int)player->getAsPlayer()->getSkillGained("abjuration"):(player->getLevel()*10)); + + int minStr = ((abjuration * 8) + attrib) / 160 + 10; + int maxStr = ((abjuration * 6) + attrib) / 90 + 10; + int strength = static_cast(Random::get(minStr,maxStr) * multiplier); + strength = std::max(10,strength); + + long minDur = (((level * 60) + (attrib * 3) + (abjuration * 2)) / 120) + 10; + long maxDur = (((level * 2) + (attrib / 7) + (abjuration / 7)) / 3) + 10; + long duration = static_cast(Random::get(minDur, maxDur) * multiplier); + duration = 60*std::min(duration, 75L); + + if(isPtester(player)) { + *player << ColorOn << "^Dabjuration = " << abjuration << ", attrib = " << attrib; + *player << ", multiplier = " << multiplier << "\n"; + *player << "minStr = " << minStr << ", maxStr = " << maxStr << "\n"; + *player << "minDur = " << minDur << ", maxDur = " << maxDur << "\n\n"; + *player << "Strength: " << strength << ", Duration: " << duration << "^x\n" << ColorOff; + } + + return(splGeneric(player, cmnd, spellData, "a", "protection", "protection", strength, duration, maxStr)); } //********************************************************************* @@ -133,10 +223,92 @@ int splUndeadWard(const std::shared_ptr& player, cmd* cmnd, SpellData* // splBless //********************************************************************* // This function allows a player to cast a bless spell on themself or -// on another player, reducing the target's thaco by 1. +// on another player, increasing their chance to hit based on "bless" +// effect strength. The MP needed scales with abjuration skill level for +// players, and it uses level for monsters int splBless(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData) { - return(splGeneric(player, cmnd, spellData, "a", "bless", "bless")); + + int mpNeeded=0; + double multiplier=1.0; + + int level = player->isMonster()?player->getLevel():player->getAsPlayer()->getSkillLevel("abjuration"); + + if(level <= 10) + mpNeeded = 10; + else if (level <= 20) + mpNeeded = 15; + else if (level <= 30) + mpNeeded = 20; + else if (level <= 40) + mpNeeded = 25; + + if(!player->isMonster() && player->spellFail(spellData->how)) { + if(spellData->how == CastType::CAST) + player->subMp(mpNeeded/2); + return(0); + } + + if(spellData->how == CastType::CAST && !player->checkMp(mpNeeded)) + return(0); + + + switch(player->getClass()) { + case CreatureClass::CLERIC: + case CreatureClass::DRUID: + multiplier += 0.2; + break; + case CreatureClass::LICH: + case CreatureClass::MAGE: + multiplier += 0.15; + break; + case CreatureClass::PALADIN: + case CreatureClass::DEATHKNIGHT: + multiplier += 0.1; + break; + case CreatureClass::FIGHTER: + case CreatureClass::THIEF: + if(player->isPlayer() && player->getAsPlayer()->getSecondClass() == CreatureClass::MAGE) + multiplier += 0.1; + break; + default: + break; + } + + switch(player->getRace()) { + case SERAPH: + case CAMBION: + case TIEFLING: + multiplier += 0.05; + break; + default: + break; + } + + int abjuration = (player->isPlayer()?(int)player->getAsPlayer()->getSkillGained("abjuration"):(player->getLevel()*10)); + int piety = player->piety.getCur(); + + int minStr = ((abjuration * 8) + piety) / 160 + 10; + int maxStr = ((abjuration * 6) + piety) / 90 + 10; + + int strength = static_cast(Random::get(minStr,maxStr) * multiplier); + + strength = std::max(10,strength); + + long minDur = (((level * 60) + (piety * 3) + (abjuration * 2)) / 120) + 10; + long maxDur = (((level * 2) + (piety / 7) + (abjuration / 7)) / 3) + 10; + long duration = static_cast(Random::get(minDur, maxDur) * multiplier); + duration = 60*std::min(duration, 75L); + + if(isPtester(player)) { + *player << ColorOn << "^Dabjuration = " << abjuration << ", piety = " << piety; + *player << ", multiplier = " << multiplier << "\n"; + *player << "minStr = " << minStr << ", maxStr = " << maxStr << "\n"; + *player << "minDur = " << minDur << ", maxDur = " << maxDur << "\n\n"; + *player << "Strength: " << strength << ", Duration: " << duration << "^x\n" << ColorOff; + } + + return(splGeneric(player, cmnd, spellData, "a", "bless", "bless", strength, duration, maxStr)); } //********************************************************************* @@ -526,9 +698,6 @@ int splArmor(const std::shared_ptr& player, cmd* cmnd, SpellData* spel mpNeeded = spellData->level; - if(spellData->how == CastType::CAST && !pPlayer->checkMp(mpNeeded)) - return(0); - if(!pPlayer->isCt()) { if(pPlayer->getClass() != CreatureClass::MAGE && pPlayer->getClass() != CreatureClass::LICH && pPlayer->getSecondClass() != CreatureClass::MAGE) { if(spellData->how == CastType::CAST) { @@ -541,6 +710,14 @@ int splArmor(const std::shared_ptr& player, cmd* cmnd, SpellData* spel } } + if(pPlayer->spellFail( spellData->how)) { + if(spellData->how == CastType::CAST) + pPlayer->subMp(mpNeeded); + return(0); + } + + if(spellData->how == CastType::CAST && !pPlayer->checkMp(mpNeeded)) + return(0); bool multi=false; @@ -548,23 +725,14 @@ int splArmor(const std::shared_ptr& player, cmd* cmnd, SpellData* spel (pPlayer->getClass() == CreatureClass::FIGHTER && pPlayer->getSecondClass() == CreatureClass::MAGE) || (pPlayer->getClass() == CreatureClass::THIEF && pPlayer->getSecondClass() == CreatureClass::MAGE) ); - - - if(pPlayer->spellFail( spellData->how)) { - if(spellData->how == CastType::CAST) - pPlayer->subMp(mpNeeded); - return(0); - } - // Cast armor on self int strength = 0; int duration = 0; if(spellData->how == CastType::CAST) { duration = 1800 + bonus(pPlayer->intelligence.getCur()); + pPlayer->subMp(mpNeeded); - if(spellData->how == CastType::CAST) - pPlayer->subMp(mpNeeded); if(pPlayer->getRoomParent()->magicBonus()) { player->print("The room's magical properties increase the power of your spell.\n"); duration += 600L; @@ -585,18 +753,86 @@ int splArmor(const std::shared_ptr& player, cmd* cmnd, SpellData* spel strength = pPlayer->hp.getCur(); } + if(spellData->how == CastType::CAST || spellData->how == CastType::SCROLL || spellData->how == CastType::WAND) { + player->print("Armor spell cast.\n"); + broadcast(pPlayer->getSock(), pPlayer->getRoomParent(),"%M casts an armor spell on %sself.", pPlayer.get(), pPlayer->himHer()); + } + pPlayer->addEffect("armor", duration, strength, player, true, player); pPlayer->computeAC(); pPlayer->printColor("^BYour magical armor will remain until it takes ^C%d^B damage.\n", strength); + return(1); +} + +//********************************************************************* +// splShield +//********************************************************************* +// This spell allows arcane casters and multi-class arcane casters to form a +// magical shield around them which improves defense. It also provides +// immunity to all magic missile attacks. + +int splShield(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData) { + std::shared_ptr pPlayer = player->getAsPlayer(); + int mpNeeded=0; + + if(!pPlayer) + return(0); + + mpNeeded = 15; + + + if(!pPlayer->isCt()) { + if(pPlayer->getClass() != CreatureClass::MAGE && pPlayer->getClass() != CreatureClass::LICH && pPlayer->getSecondClass() != CreatureClass::MAGE) { + if(spellData->how == CastType::CAST) { + player->print("Only mages, multi-class mages, and liches can cast the shield spell.\n"); + return(0); + } else { + player->print("Nothing happens.\n"); + return(0); + } + } + } + + if(pPlayer->spellFail(spellData->how)) { + if(spellData->how == CastType::CAST) + pPlayer->subMp(mpNeeded); + return(0); + } + + if(spellData->how == CastType::CAST && !pPlayer->checkMp(mpNeeded)) + return(0); + + // Cast shield on self + int strength = 10; + int duration = 0; + if(spellData->how == CastType::CAST) { + duration = 90L; + if(pPlayer->getRoomParent()->magicBonus()) { + player->print("The room's magical properties increase the power of your spell.\n"); + duration += 30L; + } + + strength = std::max(10,pPlayer->getSkillLevel("abjuration")); + + } else { + duration = 25L; + if (spellData->object) + strength = (spellData->object->getLevel()>0?spellData->object->getLevel():10); + } + if(spellData->how == CastType::CAST || spellData->how == CastType::SCROLL || spellData->how == CastType::WAND) { - player->print("Armor spell cast.\n"); - broadcast(pPlayer->getSock(), pPlayer->getRoomParent(),"%M casts an armor spell on %sself.", pPlayer.get(), pPlayer->himHer()); + player->print("Shield spell cast.\n"); + broadcast(pPlayer->getSock(), pPlayer->getRoomParent(),"%M casts a shield spell on %sself.", pPlayer.get(), pPlayer->himHer()); } + pPlayer->addEffect("shield", duration, strength, player, true, player); + pPlayer->computeAC(); + return(1); } + //********************************************************************* // splStoneskin //********************************************************************* diff --git a/magic/schools/conjuration.cpp b/magic/schools/conjuration.cpp index d7dcbc7c..e45ba423 100644 --- a/magic/schools/conjuration.cpp +++ b/magic/schools/conjuration.cpp @@ -48,17 +48,17 @@ char conjureTitles[][3][10][30] = { -// earth + // earth { // weak { "lesser earth elemental", "marble mongoose", "rock ape", "earth spider", "giant badger", - "steel snake", "golden fox", "giant rock worm", "glassteel golem", "earth demon" }, + "steel snake", "golden fox", "giant rock worm", "glassteel golem", "hulking earth demon" }, // normal { "giant mole", "obsidian worm", "mud sloth", "crystal wolf", "earth elemental", "granite elephant", "iron sentinel", "greater earth elemental", "steel tiger", "adamantium tiger" }, // buff { "pet rock", "sandman", "rock wolverine", "rock demon", "marble tiger", "earth devil", - "crystal sentinel", "galeb duhr", "steel hydra", "adamantium dragon" } }, + "crystal sentinel", "galeb duhr", "steel hydra", "adamantium pseudo-dragon" } }, // air { // weak @@ -80,40 +80,40 @@ char conjureTitles[][3][10][30] = { "fire angel", "greater fire elemental", "fire kraken", "phoenix" }, // buff { "burning bush", "fire asp", "flame sprite", "ruby serpent", "crimson iguana", "brimstone demon", - "efretti", "horned fire devil", "fire giant shaman", "venerable red dragon" } }, + "efretti", "horned fire devil", "fire giant shaman", "venerable red pseudo-dragon" } }, // water { // weak - { "lesser water elemental", "aquatic elf", "acidic blob", "vapor rat", "water weird", "fog beast", - "white crocodile", "steam jaguar", "water-logged troll", "bronze dragon" }, + { "lesser water elemental", "psychotic aquatic elf", "acidic blob", "vapor rat", "water weird", "fog beast", + "white crocodile", "steam jaguar", "water-logged troll", "menacing brine beast" }, // normal { "giant frog", "water imp", "mist devil", "water scorpion", "water elemental", "steam spider", "blood elemental", "greater water elemental", "acid devil", "water elemental lord" }, // buff - { "mist rat", "creeping fog", "water mephit", "mist spider", "carp dragon", "giant water slug", - "giant squid", "mist dragon", "kraken", "sea titan" } }, + { "mist rat", "creeping fog", "water mephit", "mist spider", "tidal wraith", "giant water slug", + "giant squid", "hulking mist beast", "adolescent kraken", "titanic merman" } }, // electricity { // weak { "lesser lightning elemental", "lightning ball", "storm cloud", "lightning imp", "crackling mephit", - "crackling orb", "storm mephit", "storm eagle", "storm sentinel", "greater lightning elemental" }, + "electric scarab", "storm mephit", "storm eagle", "storm sentinel", "greater lightning elemental" }, // normal - { "spark", "shocker lizard", "thunder hawk", "electric eel", "lightning demon", - "shocker imp", "shocking salamander", "thunderbolt", "storm giant", "storm giant shaman" }, + { "spark", "shocker lizard", "thunder hawk", "electric eel", "lesser lightning demon", + "shocker imp", "shocking salamander", "thunderbolt", "storm giant", "hulking lighting devil" }, // buff - { "static ball", "lightning serpent", "lightning devil", "thundercloud", "thunder cat", - "lightning djinn", "crackling roc", "young blue dragon", "blue dragon", "venerable blue dragon" } }, + { "static ball", "electric serpent", "lightning demon", "thundercloud", "thunder cat", + "electrogriffin", "crackling roc", "plasma wraith", "giant xag-yi", "storm revenant" } }, // cold { // weak - { "lesser ice elemental", "snow storm", "polar bear cub", "frost revenant", "swirling blizzard", - "frozen beast", "snow witch", "crystalline golem", "ice troll", "greater ice elemental" }, + { "lesser ice elemental", "snow storm", "polar bear", "frost revenant", "swirling blizzard", + "frozen ice beast", "snow witch", "ice golem", "iceblue troll", "greater ice elemental" }, // normal - { "ice rat", "snowman", "snow eagle", "winter mephit", "ice elemental", - "glacier wolf", "frozen guardian", "white remorhaz", "frost giant shaman", "frost giant lord" }, + { "ice rat", "snowman", "snow eagle", "frost mephit", "ice elemental", + "glacial tiger", "frozen guardian", "frost drake", "glacial wraith", "frost giant lord" }, // buff - { "winter fox", "winter wolf cub", "ice mephit", "polar bear", "frozen yeti", - "winter wolf", "abominable snowman", "young white dragon", "white dragon", "venerable white dragon" } } + { "winter fox", "winter wolf", "lesser ice devil", "dire polar bear", "iceshard swarm", + "frost basilisk", "abominable snowman", "white remorhaz", "snow stalker", "ice vortex" } } }; char bardConjureTitles[][10][35] = { @@ -385,7 +385,8 @@ int conjure(const std::shared_ptr&player, cmd *cmnd, SpellData *spellD std::shared_ptr target = nullptr; int title = 0, mp = 0, realm = 0, level = 0, spells = 0, chance = 0, sRealm = 0; int buff = 0, hp_percent = 0, mp_percent = 0, a = 0, rnum = 0, cClass = 0, skLevel = 0; - int interval = 0, n = 0, x = 0, hplow = 0, hphigh = 0, mplow = 0, mphigh = 0; + int n = 0, x = 0, hplow = 0, hphigh = 0, mplow = 0, mphigh = 0; + long interval = 0; time_t t, i, len = 0;; const char *delem; diff --git a/magic/schools/divination.cpp b/magic/schools/divination.cpp index 2916139d..4242bbe1 100644 --- a/magic/schools/divination.cpp +++ b/magic/schools/divination.cpp @@ -81,6 +81,30 @@ int splKnowAura(const std::shared_ptr& player, cmd* cmnd, SpellData* s return(splGeneric(player, cmnd, spellData, "a", "know-aura", "know-aura")); } +//********************************************************************* +// splEmpathy +//********************************************************************* +// This spell, combined with a know-aura effect, allows a cleric, druid, bard, +// or paladin to see the actual alignment number on the alignment scale that they are, +// or their target is. + +int splEmpathy(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData) { + + if(!player->isCt() && player->getClass() != CreatureClass::CLERIC && player->getClass() != CreatureClass::DRUID && + player->getClass() != CreatureClass::PALADIN && player->getClass() != CreatureClass::BARD) + { + + if(spellData->how != CastType::POTION) { + *player << (spellData->how==CastType::WAND?"Nothing happens":"You are unable to cast that spell") << ".\n"; + return(0); + } + + } + + return(splGeneric(player, cmnd, spellData, "an", "empathy", "empathy")); +} + + //********************************************************************* // splFortune //********************************************************************* diff --git a/magic/schools/enchantment.cpp b/magic/schools/enchantment.cpp index a96e12aa..460599a8 100644 --- a/magic/schools/enchantment.cpp +++ b/magic/schools/enchantment.cpp @@ -782,24 +782,38 @@ int splFear(const std::shared_ptr& player, cmd* cmnd, SpellData* spell int splSilence(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData) { std::shared_ptr target=nullptr; - int bns=0, canCast=0, mpCost=0; + int bns=0, mpCost=0; long dur=0; + bool canCast = false; if(player->getClass() == CreatureClass::BUILDER) { - player->print("You cannot cast this spell.\n"); + *player << "You cannot cast this spell.\n"; return(0); } if( player->getClass() == CreatureClass::LICH || - player->getClass() == CreatureClass::MAGE || - player->getClass() == CreatureClass::CLERIC || + player->getClass() == CreatureClass::MAGE || player->getAsPlayer()->getSecondClass() == CreatureClass::MAGE || player->isCt() ) - canCast = 1; + canCast = true; + + if(player->getClass() == CreatureClass::CLERIC) { + switch(player->getDeity()) { + case CERIS: + case ARACHNUS: + case ENOCH: + case ARAMON: + case MARA: + canCast = true; + break; + default: + break; + } + } if(!canCast && player->isPlayer() && spellData->how == CastType::CAST) { - player->print("You are unable to cast that spell.\n"); + *player << "You are unable to cast that spell.\n"; return(0); } @@ -820,8 +834,6 @@ int splSilence(const std::shared_ptr& player, cmd* cmnd, SpellData* sp else dur = Random::get(30,60); - - if(player->spellFail( spellData->how)) return(0); @@ -837,7 +849,7 @@ int splSilence(const std::shared_ptr& player, cmd* cmnd, SpellData* sp if(spellData->how == CastType::CAST || spellData->how == CastType::SCROLL || spellData->how == CastType::WAND) { broadcast(player->getSock(), player->getParent(), "%M casts silence on %sself.", player.get(), player->himHer()); } else if(spellData->how == CastType::POTION) - player->print("Your throat goes dry and you cannot speak.\n"); + *player << "Your throat goes dry and you cannot speak.\n"; // silence a monster or player } else { @@ -847,7 +859,7 @@ int splSilence(const std::shared_ptr& player, cmd* cmnd, SpellData* sp target = player->getParent()->findCreature(player, cmnd->str[2], cmnd->val[2], false); if(!target || target == player) { - player->print("That's not here.\n"); + *player << "That's not here.\n"; return(0); } @@ -856,7 +868,7 @@ int splSilence(const std::shared_ptr& player, cmd* cmnd, SpellData* sp if(player->isPlayer() && target->mFlagIsSet(M_PERMANENT_MONSTER)) { if(!dec_daily(&player->daily[DL_SILENCE]) && !player->isCt()) { - player->print("You have done that enough times for today.\n"); + *player << "You have done that enough times for today.\n"; return(0); } } @@ -882,28 +894,28 @@ int splSilence(const std::shared_ptr& player, cmd* cmnd, SpellData* sp target->wake("Terrible nightmares disturb your sleep!"); if(target->chkSave(SPL, player, bns) && !player->isCt()) { - target->print("%M tried to cast a silence spell on you!\n", player.get()); - broadcast(player->getSock(), target->getSock(), player->getParent(), "%M tried to cast a silence spell on %N!", player.get(), target.get()); - player->print("Your spell fizzles.\n"); + *target << ColorOn << "^c" << setf(CAP) << player << " tried to cast a silence spell on you!^x\n" << ColorOff; + broadcast(player->getSock(), target->getSock(), player->getParent(), "^c%M tried to cast a silence spell on %N!^x", player.get(), target.get()); + *player << ColorOn << "^c" << setf(CAP) << target << " resisted your spell!^x\n" << ColorOff; return(0); } if(player->isPlayer() && target->isPlayer()) { if(!dec_daily(&player->daily[DL_SILENCE]) && !player->isCt()) { - player->print("You have done that enough times for today.\n"); + *player << "You have done that enough times for today.\n"; return(0); } } if(spellData->how == CastType::CAST || spellData->how == CastType::SCROLL || spellData->how == CastType::WAND) { - player->print("Silence casted on %s.\n", target->getCName()); - broadcast(player->getSock(), target->getSock(), player->getParent(), "%M casts a silence spell on %N.", player.get(), target.get()); + *player << ColorOn << "^cSilence casted on " << target << ".^x\n" << ColorOff; + broadcast(player->getSock(), target->getSock(), player->getParent(), "^c%M casts a silence spell on %N.^x", player.get(), target.get()); logCast(player, target, "silence"); - target->print("%M casts a silence spell on you.\n", player.get()); + *target << ColorOn << "^c" << setf(CAP) << player << " casts a silence spell on you!^x\n" << ColorOff; } if(target->isMonster()) @@ -927,11 +939,11 @@ int splSilence(const std::shared_ptr& player, cmd* cmnd, SpellData* sp bool canEnchant(const std::shared_ptr& player, SpellData* spellData) { if(!player->isStaff()) { if(spellData->how == CastType::CAST && player->getClass() != CreatureClass::MAGE && player->getClass() != CreatureClass::LICH) { - player->print("Only mages may enchant objects.\n"); + player->print("Only mages and liches may enchant objects with the enchant spell.\n"); return(false); } if(spellData->how == CastType::CAST && player->getClass() == CreatureClass::MAGE && player->hasSecondClass()) { - player->print("Only pure mages may enchant objects.\n"); + player->print("Only pure mages may enchant objects with the enchant spell.\n"); return(false); } @@ -1047,7 +1059,7 @@ int cmdEnchant(const std::shared_ptr& player, cmd* cmnd) { if(player->isMagicallyHeld(true)) return(0); - if(!player->knowsSkill("enchant") || player->hasSecondClass()) { + if(!player->knowsSkill("enchant")) { player->print("You lack the training to enchant objects.\n"); return(0); } diff --git a/magic/schools/evocation.cpp b/magic/schools/evocation.cpp index acb522ec..6142cb74 100644 --- a/magic/schools/evocation.cpp +++ b/magic/schools/evocation.cpp @@ -110,6 +110,7 @@ int splMagicMissile(const std::shared_ptr& player, cmd* cmnd, SpellDat if(!player->canAttack(target)) return(0); + if(spellData->how == CastType::CAST) { maxMissiles = spellData->level / 2; } else { @@ -170,13 +171,15 @@ int splMagicMissile(const std::shared_ptr& player, cmd* cmnd, SpellDat while(a < num) { a++; missileDmg = Random::get(2,5) + bonus(player->intelligence.getCur())/2; - if(Random::get(1,100) <= 25 && target->isEffected("resist-magic")) { - player->print("Your magic-missile deflects off of %N.\n", target.get()); - broadcast(player->getSock(), target->getSock(), player->getParent(), "%M's magic-missile deflects off of %N.", player.get(), target.get()); - target->print("%M's magic-missile deflects off of you.\n", player.get()); + + colorCh = getRandColor(); + + if((Random::get(1,100) <= 25 && target->isEffected("resist-magic")) || target->isEffected("shield")) { + player->printColor("^@^%cYour magic-missile deflects off of %N%s.^x\n", colorCh,target.get(), target->isEffected("shield")?"'s magical shield":""); + broadcast(player->getSock(), target->getSock(), player->getParent(), "^@^%c%M's magic-missile deflects off of %N%s.^x", colorCh, player.get(), target.get(),target->isEffected("shield")?"'s magical shield":""); + target->print("^@^%c%M's magic-missile deflects off of you%s.^x\n", colorCh,player.get(),target->isEffected("shield")?"r magical shield":""); continue; } - colorCh = getRandColor(); player->printColor("^@^%cYour magic-missile strikes %N for %d damage.\n", colorCh, target.get(), missileDmg); target->printColor("^@^%c%M's magic-missile strikes you for %d damage!\n", colorCh, player.get(), missileDmg); @@ -396,6 +399,10 @@ int doOffensive(std::shared_ptrcaster, std::shared_ptr targe damage.set(osp->damage.roll() + bns); target->modifyDamage(caster, dmgType, damage, osp->realm); + + if(osp->realm == FIRE && target->getRace() == TROLL) + damage.set((damage.get())*120/100); + damage.set(std::max(0, damage.get())); m = std::min(target->hp.getCur(), damage.get()); @@ -713,6 +720,9 @@ int splDarkness(const std::shared_ptr& player, cmd* cmnd, SpellData* s if(spellData->how == CastType::CAST) player->subMp(15); + if (replaceCancelingEffects(player,target,"darkness")) + return(0); + } else { if(player->noPotion( spellData)) return(0); @@ -759,6 +769,7 @@ int splDarkness(const std::shared_ptr& player, cmd* cmnd, SpellData* s } } + player->print("You cast a darkness spell on %N.\n", target.get()); broadcast(player->getSock(), target->getSock(), player->getParent(), "%M casts a darkness spell on %N.", player.get(), target.get()); target->print("%M casts a darkness spell on you.\n", player.get()); @@ -766,6 +777,9 @@ int splDarkness(const std::shared_ptr& player, cmd* cmnd, SpellData* s if(spellData->how == CastType::CAST) player->subMp(20); + if (replaceCancelingEffects(player,target,"darkness")) + return(0); + if(!player->isStaff() || target->isStaff()) { if(target->isStaff() || target->chkSave(SPL, player, -25)) { player->printColor("^y%M resisted your darkness spell!\n", target.get()); @@ -826,17 +840,123 @@ int splDarkness(const std::shared_ptr& player, cmd* cmnd, SpellData* s } + long duration = 300L; + int strength = 0; + // final routines for creatures only if(target) { + + if(spellData->how == CastType::WAND || spellData->how == CastType::POTION) { + duration = std::max(10, spellData->object->getLevel()) * 30; + strength = std::max(10, spellData->object->getLevel()); + } + else + { + switch(player->getCastingType()) { + case MagicType::Divine: + strength = (int)player->getSkillLevel("day"); + duration = 600L + ((long)player->getSkillLevel("day") * 30); + break; + case MagicType::Arcane: + strength = (int)player->getSkillLevel("evocation"); + duration = 600L + ((long)player->getSkillLevel("evocation") * 30); + break; + default: + strength = (int)player->getSkillLevel("evocation"); + duration = 180L + ((long)player->getSkillLevel("evocation") * 5); + break; + } + } + + *player << "Strength: " << strength << "\nDuration: " << duration << "\n"; + if(spellData->how == CastType::CAST) { if(player->getRoomParent()->magicBonus()) { player->print("The room's magical properties increase the power of your spell.\n"); + duration += (duration*30)/100; } - target->addEffect("darkness", -2, -2, player, true, player); + target->addEffect("darkness", duration, strength, player, true, player); } else { - target->addEffect("darkness"); + target->addEffect("darkness", duration, strength); } } return(1); } + + +//********************************************************************* +// splLight +//********************************************************************* +// This spell allows a player to cast a light spell either on themselves +// or on a target creature. Only mages, druids, liches, bards, paladins, +// and clerics are able to cast this spell, as well as some specific races, +// regardless of class. + +int splLight(const std::shared_ptr& player, cmd* cmnd, SpellData* spellData) { + + + bool canCast=false; + + switch(player->getClass()) { + case CreatureClass::CLERIC: + case CreatureClass::PALADIN: + case CreatureClass::DRUID: + case CreatureClass::BARD: + case CreatureClass::LICH: + case CreatureClass::MAGE: + canCast=true; + break; + case CreatureClass::FIGHTER: + case CreatureClass::THIEF: + if(player->getAsPlayer()->getSecondClass() == CreatureClass::MAGE) + canCast=true; + break; + default: + break; + } + + switch (player->getRace()) { + case ELF: + case GREYELF: + case TIEFLING: + case CAMBION: + case SERAPH: + canCast=true; + break; + default: + break; + } + + if(!canCast && !player->isStaff()) { + *player << "You are unable to cast that spell.\n"; + return(0); + } + + long duration = 300L; + int strength = 0; + + if(spellData->how == CastType::WAND || spellData->how == CastType::POTION) { + duration = std::max(10, spellData->object->getLevel()) * 30; + strength = std::max(10, spellData->object->getLevel()); + } + else + { + switch(player->getCastingType()) { + case MagicType::Divine: + strength = (int)player->getSkillLevel("day"); + duration = 600L + ((long)player->getSkillLevel("day") * 30); + break; + case MagicType::Arcane: + strength = (int)player->getSkillLevel("evocation"); + duration = 600L + ((long)player->getSkillLevel("evocation") * 30); + break; + default: + strength = (int)player->getSkillLevel("evocation"); + duration = 180L + ((long)player->getSkillLevel("evocation") * 5); + break; + } + } + + return(splGeneric(player, cmnd, spellData, "a", "light", "light", strength, duration)); +} diff --git a/magic/schools/transmutation.cpp b/magic/schools/transmutation.cpp index 5921d1db..f3f0165d 100644 --- a/magic/schools/transmutation.cpp +++ b/magic/schools/transmutation.cpp @@ -768,7 +768,7 @@ int splDeafness(const std::shared_ptr& player, cmd* cmnd, SpellData* s canCast = 1; if(!canCast && player->isPlayer() && spellData->how == CastType::CAST) { - player->print("You are unable to cast that spell.\n"); + *player << "You are unable to cast that spell.\n"; return(0); } @@ -797,7 +797,7 @@ int splDeafness(const std::shared_ptr& player, cmd* cmnd, SpellData* s if(spellData->how == CastType::CAST || spellData->how == CastType::SCROLL || spellData->how == CastType::WAND) { broadcast(player->getSock(), player->getParent(), "%M casts deafness on %sself.", player.get(), player->himHer()); } else if(spellData->how == CastType::POTION) - player->print("Your throat goes dry and you cannot speak.\n"); + *player << "Everything goes suddenly deathly silent. You can't hear!\n"; // silence a monster or player } else { @@ -807,7 +807,7 @@ int splDeafness(const std::shared_ptr& player, cmd* cmnd, SpellData* s target = player->getParent()->findCreature(player, cmnd->str[2], cmnd->val[2], false); if(!target || target == player) { - player->print("That's not here.\n"); + *player << "That's not here.\n"; return(0); } @@ -815,7 +815,7 @@ int splDeafness(const std::shared_ptr& player, cmd* cmnd, SpellData* s return(0); if(player->isPlayer() && target->mFlagIsSet(M_PERMANENT_MONSTER)) { - if(!dec_daily(&player->daily[DL_SILENCE]) && !player->isCt()) { + if(!dec_daily(&player->daily[DL_DEAFNESS]) && !player->isCt()) { player->print("You have done that enough times for today.\n"); return(0); } @@ -839,28 +839,28 @@ int splDeafness(const std::shared_ptr& player, cmd* cmnd, SpellData* s target->wake("Terrible nightmares disturb your sleep!"); if(target->chkSave(SPL, player, bns) && !player->isCt()) { - target->print("%M tried to cast a deafness spell on you!\n", player.get()); - broadcast(player->getSock(), target->getSock(), player->getParent(), "%M tried to cast a deafness spell on %N!", player.get(), target.get()); - player->print("Your spell fizzles.\n"); + *target << ColorOn << "^c" << setf(CAP) << player << " tried to cast a deafness spell on you!^x\n" << ColorOff; + broadcast(player->getSock(), target->getSock(), player->getParent(), "^c%M tried to cast a deafness spell on %N!^x", player.get(), target.get()); + *player << "Your spell failed to take hold.\n"; return(0); } if(player->isPlayer() && target->isPlayer()) { - if(!dec_daily(&player->daily[DL_SILENCE]) && !player->isCt()) { - player->print("You have done that enough times for today.\n"); + if(!dec_daily(&player->daily[DL_DEAFNESS]) && !player->isCt()) { + *player << "You have done that enough times for today.\n"; return(0); } } if(spellData->how == CastType::CAST || spellData->how == CastType::SCROLL || spellData->how == CastType::WAND) { - player->print("Deafness casted on %s.\n", target->getCName()); - broadcast(player->getSock(), target->getSock(), player->getParent(), "%M casts a deafness spell on %N.", player.get(), target.get()); + *player << ColorOn << "^cDeafness casted on " << target << ".^x\n" << ColorOff; + broadcast(player->getSock(), target->getSock(), player->getParent(), "^c%M casts a deafness spell on %N.^x", player.get(), target.get()); logCast(player, target, "silence"); - target->print("%M casts a deafness spell on you.\n", player.get()); + *target << ColorOn << "^c" << setf(CAP) << player << " casts a deafness spell on you.^x\n" << ColorOff; } if(target->isMonster()) @@ -886,7 +886,7 @@ int splRegeneration(const std::shared_ptr& player, cmd* cmnd, SpellDat case CreatureClass::DRUID: break; default: - player->print("Only clerics and druids may cast this spell.\n"); + player->print("Only clerics and druids may cast that spell.\n"); return(0); } } diff --git a/magic/spells.cpp b/magic/spells.cpp index 8910a47d..c7f88524 100644 --- a/magic/spells.cpp +++ b/magic/spells.cpp @@ -124,6 +124,10 @@ std::string spellSkill(DomainOfMagic domain) { return("travel"); case CREATION: return("creation"); + case DAY: + return("day"); + case NIGHT: + return("night"); default: return(""); } diff --git a/movement/exits.cpp b/movement/exits.cpp index 6592490c..4f21ac2d 100644 --- a/movement/exits.cpp +++ b/movement/exits.cpp @@ -228,6 +228,10 @@ bool Exit::raceRestrict(const std::shared_ptr & creature) const !flagIsSet(X_SEL_KATARAN) && !flagIsSet(X_SEL_TIEFLING) && !flagIsSet(X_SEL_KENKU) && + !flagIsSet(X_SEL_GREYELF) && + !flagIsSet(X_SEL_WILDELF) && + !flagIsSet(X_SEL_DUERGAR) && + !flagIsSet(X_SEL_OROG) && !flagIsSet(X_RSEL_INVERT) ) return(false); @@ -253,7 +257,11 @@ bool Exit::raceRestrict(const std::shared_ptr & creature) const (flagIsSet(X_SEL_BARBARIAN) && creature->isRace(BARBARIAN)) || (flagIsSet(X_SEL_KATARAN) && creature->isRace(KATARAN)) || (flagIsSet(X_SEL_TIEFLING) && creature->isRace(TIEFLING)) || - (flagIsSet(X_SEL_KENKU) && creature->isRace(KENKU)) + (flagIsSet(X_SEL_KENKU) && creature->isRace(KENKU)) || + (flagIsSet(X_SEL_GREYELF) && creature->isRace(GREYELF)) || + (flagIsSet(X_SEL_WILDELF) && creature->isRace(WILDELF)) || + (flagIsSet(X_SEL_DUERGAR) && creature->isRace(DUERGAR)) || + (flagIsSet(X_SEL_OROG) && creature->isRace(OROG)) ); if(flagIsSet(X_RSEL_INVERT)) pass = !pass; diff --git a/movement/movement.cpp b/movement/movement.cpp index 259db71f..c159446e 100644 --- a/movement/movement.cpp +++ b/movement/movement.cpp @@ -409,9 +409,7 @@ bool Move::canEnter(const std::shared_ptr& player, const std::shared_ptr } } - if( (exit->flagIsSet(X_NEEDS_CLIMBING_GEAR) || exit->flagIsSet(X_CLIMBING_GEAR_TO_REPEL)) && - !player->isEffected("levitate") && - !player->isEffected("mist")) + if( (exit->flagIsSet(X_NEEDS_CLIMBING_GEAR) || exit->flagIsSet(X_CLIMBING_GEAR_TO_REPEL)) && !player->checkClimbing()) { int fall = (exit->flagIsSet(X_DIFFICULT_CLIMB) ? 50 : 0) + 50 - player->getFallBonus(); @@ -431,7 +429,7 @@ bool Move::canEnter(const std::shared_ptr& player, const std::shared_ptr return(false); } - if(exit->flagIsSet(X_NEEDS_CLIMBING_GEAR)) { + if((exit->flagIsSet(X_NEEDS_CLIMBING_GEAR) || X_CLIMBING_GEAR_TO_REPEL)) { player->print("You need climbing gear to go that way.\n"); return(false); } diff --git a/objects/object.cpp b/objects/object.cpp index 191af337..25864c3c 100644 --- a/objects/object.cpp +++ b/objects/object.cpp @@ -936,9 +936,11 @@ std::string Object::getDurabilityIndicator() const { return progressBar(5, getDurabilityPercent()); } short Object::getMagicpower() const { return(magicpower); } +unsigned short Object::getCastChance() const { return(castChance); } short Object::getLevel() const { return(level); } int Object::getRequiredSkill() const { return(requiredSkill); } short Object::getMinStrength() const { return(minStrength); } +unsigned short Object::getPermSpawnChance() const { return(permSpawnChance); } short Object::getClan() const { return(clan); } short Object::getSpecial() const { return(special); } short Object::getQuestnum() const { return(questnum); } @@ -967,6 +969,8 @@ std::string Object::getSizeStr() const{ return(getSizeName(size)); } + + //********************************************************************* // isQuestOwner //********************************************************************* diff --git a/objects/objectAttr.cpp b/objects/objectAttr.cpp index 9feaeb88..af80ab83 100644 --- a/objects/objectAttr.cpp +++ b/objects/objectAttr.cpp @@ -61,9 +61,11 @@ void Object::decChargesCur(short s) { chargesCur -= s; } void Object::incChargesCur(short s) { chargesCur += s; } void Object::setMagicpower(short m) { magicpower = m; } +void Object::setCastChance(unsigned short s) { castChance = std::max(0, std::min(1000, s)); } void Object::setLevel(short l) { level = l; } void Object::setRequiredSkill(int s) { requiredSkill = s; } void Object::setMinStrength(short s) { minStrength = s; } +void Object::setPermSpawnChance(unsigned short s) { permSpawnChance = std::max(0, std::min(1000, s)); } void Object::setClan(short c) { clan = c; } void Object::setSpecial(short s) { special = s; } void Object::setQuestnum(short q) { questnum = q; } diff --git a/objects/objects.cpp b/objects/objects.cpp index b9bca5e2..608a9f46 100644 --- a/objects/objects.cpp +++ b/objects/objects.cpp @@ -199,7 +199,7 @@ Object::Object() { lotteryNumbers[i] = 0; material = NO_MATERIAL; - keyVal = minStrength = 0; + keyVal = minStrength = permSpawnChance = castChance = 0; compass = nullptr; increase = nullptr; recipe = 0; @@ -289,6 +289,7 @@ void Object::objCopy(const Object& o) { armor = o.armor; wearflag = o.wearflag; magicpower = o.magicpower; + castChance = o.castChance; info = o.info; level = o.level; quality = o.quality; @@ -325,6 +326,7 @@ void Object::objCopy(const Object& o) { questOwner = o.questOwner; minStrength = o.minStrength; + permSpawnChance = o.permSpawnChance; material = o.material; for(i=0; i<6; i++) @@ -403,6 +405,7 @@ bool Object::operator==(const Object& o) const { armor != o.armor || wearflag != o.wearflag || magicpower != o.magicpower || + castChance != o.castChance || info != o.info || level != o.level || quality != o.quality || @@ -424,6 +427,7 @@ bool Object::operator==(const Object& o) const { extra != o.extra || questOwner != o.questOwner || minStrength != o.minStrength || + permSpawnChance != o.permSpawnChance || material != o.material || size != o.size || recipe != o.recipe || @@ -750,6 +754,10 @@ bool Object::raceRestrict(const std::shared_ptr & creature) cons !flagIsSet(O_SEL_KATARAN) && !flagIsSet(O_SEL_TIEFLING) && !flagIsSet(O_SEL_KENKU) && + !flagIsSet(O_SEL_GREYELF) && + !flagIsSet(O_SEL_WILDELF) && + !flagIsSet(O_SEL_DUERGAR) && + !flagIsSet(O_SEL_OROG) && !flagIsSet(O_RSEL_INVERT) ) return(false); @@ -776,7 +784,11 @@ bool Object::raceRestrict(const std::shared_ptr & creature) cons (flagIsSet(O_SEL_BARBARIAN) && creature->isRace(BARBARIAN)) || (flagIsSet(O_SEL_KATARAN) && creature->isRace(KATARAN)) || (flagIsSet(O_SEL_TIEFLING) && creature->isRace(TIEFLING)) || - (flagIsSet(O_SEL_KENKU) && creature->isRace(KENKU)) + (flagIsSet(O_SEL_KENKU) && creature->isRace(KENKU)) || + (flagIsSet(O_SEL_GREYELF) && creature->isRace(GREYELF)) || + (flagIsSet(O_SEL_WILDELF) && creature->isRace(WILDELF)) || + (flagIsSet(O_SEL_DUERGAR) && creature->isRace(DUERGAR)) || + (flagIsSet(O_SEL_OROG) && creature->isRace(OROG)) ); if(flagIsSet(O_RSEL_INVERT)) @@ -1319,6 +1331,72 @@ bool Object::isTrashAtPawn(Money value) const { } +bool Object::isMetal() const { + return(isFerrousMetal() || isNonFerrousMetal()); +} + +bool Object::isFerrousMetal() const { + + Material mat = getMaterial(); + + if(mat == IRON || mat == STEEL || mat == METEORIC_IRON || mat == SHADOW_IRON || mat == NEGATIVE_STEEL) + return(true); + + return(false); +} + +bool Object::isNonFerrousMetal() const { + Material mat = getMaterial(); + + if(isSilver()) + return(true); + + if (mat == MITHRIL || mat == ADAMANTIUM || mat == MCOPPER || mat == MGOLD || + mat == MPLATINUM || mat == MALANTHIUM || mat == MELECTRUM || mat == BRONZE || + mat == ARGENTINE || mat == ELECTRITE || mat == ORICHALCUM || mat == AMARANTHIUM || + mat == INFERNITE || mat == CELESTITE || mat == NEGATIVE_MITHRIL) + return(true); + + return(false); +} + +bool Object::isOrganic() const { + + Material mat = getMaterial(); + + if(mat == WOOD || mat == CLOTH || mat == PAPER || mat == ORGANIC || + mat == BONE || mat == LEATHER || mat == HARDLEATHER) + return(true); + + return(false); +} + +bool Object::isStone() const { + + Material mat = getMaterial(); + + if(isGemstone()) + return(true); + + if(mat == GLASS || mat == STONE || mat == CRYSTAL || mat == CERAMIC || + mat == CLAY || mat == SOFTSTONE) + return(true); + + return(false); +} + +bool Object::isFlammable() const { + + if(isOrganic()) + return(true); + + // Check for any other non-organic, if any, here in future + + return(false); +} + + + //********************************************************************* // spawnObjects //********************************************************************* diff --git a/players/levelGain.cpp b/players/levelGain.cpp index 8fbc0c72..97aaab28 100644 --- a/players/levelGain.cpp +++ b/players/levelGain.cpp @@ -593,7 +593,7 @@ int cmdTrain(const std::shared_ptr& player, cmd* cmnd) { if(!player->flagIsSet(P_CHOSEN_ALIGNMENT) && player->getLevel() == ALIGNMENT_LEVEL) { player->print("You must choose your alignment before you can train to level %d.\n", ALIGNMENT_LEVEL+1); - player->print("Use the 'alignment' command to do so. HELP ALIGNMENT.\n"); + player->print("Use the 'choosealignment' command to do so. HELP CHOOSEALIGNMENT.\n"); return(0); } @@ -614,9 +614,6 @@ int cmdTrain(const std::shared_ptr& player, cmd* cmnd) { goldneeded = std::min(maxgold, expneeded / 2L); - if(player->getRace() == HUMAN) - goldneeded += goldneeded/3/10; // Humans have +10% training costs. - // Level training cost for levels 1-3 is free if(player->getLevel() <= 3) // Free for levels 1-3 to train. goldneeded = 0; @@ -669,7 +666,7 @@ int cmdTrain(const std::shared_ptr& player, cmd* cmnd) { if(!player->flagIsSet(P_CHOSEN_ALIGNMENT) && player->getLevel() == ALIGNMENT_LEVEL) { player->print("You may now choose your alignment. You must do so before you can reach level %d.\n", ALIGNMENT_LEVEL+1); - player->print("Use the 'alignment' command to do so. HELP ALIGNMENT.\n"); + player->print("Use the 'choosealignment' command to do so. HELP CHOOSEALIGNMENT.\n"); } return(0); } diff --git a/players/player.cpp b/players/player.cpp index cf5da782..f4842866 100644 --- a/players/player.cpp +++ b/players/player.cpp @@ -163,12 +163,17 @@ void Player::init() { daily[DL_RESURRECT].max = 1; daily[DL_SILENCE].max = 3; + daily[DL_DEAFNESS].max = 3; daily[DL_HARM].max = 2; daily[DL_SCARES].max = 5; if (race == DARKELF) daily[DL_LEVITATE].max = 2; else daily[DL_LEVITATE].max = 1; + + if (race == DUERGAR) + daily[DL_INVISIBLE].max = 1; + } else { daily[DL_DEFEC].max = 100; } @@ -227,10 +232,18 @@ void Player::init() { if(level >=1 && (cClass == CreatureClass::LICH || cClass == CreatureClass::MAGE) ) { learnSpell(S_ARMOR); learnSpell(S_MAGIC_MISSILE); + learnSpell(S_PROTECTION); + learnSpell(S_LIGHT); } - if(level >= 1 && (cClass == CreatureClass::CLERIC || cClass == CreatureClass::DRUID) ) + if(level >= 1 && (cClass == CreatureClass::CLERIC || cClass == CreatureClass::DRUID) ) { learnSpell(S_VIGOR); + learnSpell(S_LIGHT); + learnSpell(S_BLESS); + } + + if(level >=1 && cClass == CreatureClass::PALADIN) + learnSpell(S_LIGHT); if(cClass == CreatureClass::CLERIC && deity == CERIS && level >= 13) learnSpell(S_REJUVENATE); @@ -812,6 +825,30 @@ int Player::getArmorWeight() const { return(weight); } +//********************************************************************* +// checkClimbing +//********************************************************************* +// This function determines if a player is wearing an object flagged as +// climbing equipment in the arms, legs, hands, wielded, or held. + +bool Player::checkClimbing() { + + if(isCt() || isEffected("fly") || isEffected("levitate") || isEffected("mist")) + return(true); + + //Do we have climbing gear object equppied in hands, arms, legs, held, or wielded? + for(auto & x : ready) { + if(x) { + if(x->flagIsSet(O_CLIMBING_GEAR) && + (x->getWearflag() == HANDS || x->getWearflag() == ARMS || x->getWearflag() == LEGS || + x->getWearflag() == HELD || x->getWearflag() == WIELD)) + return(true); + } + } + + return(false); +} + //********************************************************************* // getFallBonus @@ -824,7 +861,9 @@ int Player::getFallBonus() { for(auto & j : ready) if(j) - if(j->flagIsSet(O_CLIMBING_GEAR)) + if(j->flagIsSet(O_CLIMBING_GEAR) && + (j->getWearflag() == HANDS || j->getWearflag() == ARMS || j->getWearflag() == LEGS || + j->getWearflag() == HELD || j->getWearflag() == WIELD)) fall += j->damage.getPlus()*3; return(fall); } @@ -883,6 +922,9 @@ std::shared_ptr lowest_piety(const std::shared_ptr& room, bool int Player::getLight() const { int i=0, light=0; + if(isStaff()) + return(0); + for(i = 0; i < MAXWEAR; i++) { if (!ready[i]) continue; @@ -1143,11 +1185,35 @@ void Player::initLanguages() { learnLanguage(LGOBLINOID); learnLanguage(LORCISH); break; + case DUERGAR: + learnLanguage(LDUERGAR); + learnLanguage(LDWARVEN); + learnLanguage(LDARKELVEN); + learnLanguage(LKOBOLD); + learnLanguage(LGOBLINOID); + learnLanguage(LORCISH); + learnLanguage(LUNDERCOMMON); + learnLanguage(LSVIRFNEBLIN); + break; case ELF: learnLanguage(LELVEN); learnLanguage(LGOBLINOID); learnLanguage(LORCISH); break; + case GREYELF: + learnLanguage(LELVEN); + learnLanguage(LGOBLINOID); + learnLanguage(LORCISH); + learnLanguage(LGNOMISH); + learnLanguage(LHALFLING); + break; + case WILDELF: + learnLanguage(LELVEN); + learnLanguage(LGRUGACH); + learnLanguage(LFEY); + learnLanguage(LSYLVAN); + learnLanguage(LGOBLINOID); + break; case HALFELF: learnLanguage(LELVEN); break; @@ -1159,6 +1225,11 @@ void Player::initLanguages() { learnLanguage(LORCISH); learnLanguage(LGIANTKIN); break; + case OROG: + learnLanguage(LORCISH); + learnLanguage(LGIANTKIN); + learnLanguage(LOGRISH); + break; case HALFGIANT: learnLanguage(LGIANTKIN); learnLanguage(LOGRISH); @@ -1172,6 +1243,7 @@ void Player::initLanguages() { break; case TROLL: learnLanguage(LTROLL); + learnLanguage(LBUGBEAR); break; case HALFORC: learnLanguage(LORCISH); @@ -1179,6 +1251,7 @@ void Player::initLanguages() { case OGRE: learnLanguage(LOGRISH); learnLanguage(LGIANTKIN); + learnLanguage(LBUGBEAR); break; case DARKELF: learnLanguage(LDARKELVEN); @@ -1188,10 +1261,16 @@ void Player::initLanguages() { learnLanguage(LGNOMISH); learnLanguage(LKOBOLD); learnLanguage(LGOBLINOID); + learnLanguage(LUNDERCOMMON); + learnLanguage(LDUERGAR); + learnLanguage(LSVIRFNEBLIN); break; case GOBLIN: learnLanguage(LGOBLINOID); learnLanguage(LORCISH); + learnLanguage(LUNDERCOMMON); + learnLanguage(LHOBGOBLIN); + learnLanguage(LBUGBEAR); break; case MINOTAUR: learnLanguage(LMINOTAUR); @@ -1213,9 +1292,10 @@ void Player::initLanguages() { learnLanguage(LDARKELVEN); learnLanguage(LOGRISH); learnLanguage(LGIANTKIN); + learnLanguage(LUNDERCOMMON); break; case CAMBION: - learnLanguage(LINFERNAL); + learnLanguage(LABYSSAL); learnLanguage(LDARKELVEN); learnLanguage(LELVEN); learnLanguage(LCELESTIAL); @@ -1230,7 +1310,7 @@ void Player::initLanguages() { learnLanguage(LKATARAN); break; case TIEFLING: - learnLanguage(LINFERNAL); + learnLanguage(LABYSSAL); learnLanguage(LORCISH); learnLanguage(LGOBLINOID); learnLanguage(LTIEFLING); @@ -1496,12 +1576,21 @@ int Player::getSneakChance() { } //Racial bonuses ------------------------- - if(getRace() == ELF && getConstRoomParent()->isForest()) - chance += chance/4; - if((getRace() == DARKELF && getConstRoomParent()->flagIsSet(R_UNDERGROUND)) || getRace()==KOBOLD) - chance += chance/10; - if(getRace() == HALFLING || getRace() == KENKU) - chance += chance/5; + if((getRace() == ELF && getConstRoomParent()->isForest()) || + (getRace() == DARKELF && getConstRoomParent()->flagIsSet(R_UNDERGROUND))) + chance += (chance*25)/100; + else if((getRace() == WILDELF) && getConstRoomParent()->isForest()) + chance += (chance*35)/100; + else if(getRace()==KOBOLD || getRace() == KATARAN || getRace() == WILDELF || getRace() == ELF) + chance += (chance*10)/100; + else if (getRace() == DARKELF && getConstRoomParent()->flagIsSet(R_UNDERGROUND)) + chance += (chance*25)/100; + else if(getRace() == HALFLING) + chance += (chance*20)/100; + else if(getRace() == KENKU) + chance += (chance*15)/100; + + //---------------------------------------- if(isBlind()) diff --git a/players/playerClass.cpp b/players/playerClass.cpp index 84d0dbed..ed14be79 100644 --- a/players/playerClass.cpp +++ b/players/playerClass.cpp @@ -43,6 +43,7 @@ PlayerClass::PlayerClass(xmlNodePtr rootNode) { needDeity = false; numProf = 1; + xpAdjust = 0; hasAutomaticStats = false; baseStrength=-1; baseDexterity=-1; @@ -89,6 +90,7 @@ short PlayerClass::getBaseHp() { return(baseHp); } short PlayerClass::getBaseMp() { return(baseMp); } bool PlayerClass::needsDeity() { return(needDeity); } short PlayerClass::numProfs() { return(numProf); } +int PlayerClass::getXPAdjustment() { return(xpAdjust); } LevelGain* PlayerClass::getLevelGain(int lvl) { return(levels[lvl]); } bool PlayerClass::hasDefaultStats() { return(hasAutomaticStats); } bool PlayerClass::setDefaultStats(std::shared_ptr player) { diff --git a/players/playerInfo.cpp b/players/playerInfo.cpp index e18fa74a..a43548f4 100644 --- a/players/playerInfo.cpp +++ b/players/playerInfo.cpp @@ -308,6 +308,9 @@ int cmdDaily(const std::shared_ptr& player, cmd* cmnd) { if( target->isCt() || target->getRace() == DARKELF) { player->print("Levitate: %d of %d remaining.\n", target->daily[DL_LEVITATE].cur, target->daily[DL_LEVITATE].max); } + if( target->isCt() || target->getRace() == DUERGAR) { + player->print("Invisibility: %d of %d remaining.\n", target->daily[DL_INVISIBLE].cur, target->daily[DL_INVISIBLE].max); + } //General daily limits here player->print("\n"); diff --git a/players/players.cpp b/players/players.cpp index ffa42c7f..224c7bee 100644 --- a/players/players.cpp +++ b/players/players.cpp @@ -34,6 +34,9 @@ #include "random.hpp" // for Random #include "socket.hpp" // for Socket #include "stats.hpp" // for Stat +#include "playerClass.hpp" // for PlayerClass +#include "raceData.hpp" // for RaceData +#include "config.hpp" bool Player::operator <(const Player& t) const { @@ -102,6 +105,7 @@ void Player::pulseTick(long t) { hpTickAmt = std::max(1, 3 + bonus(constitution.getCur()) + (cClass == CreatureClass::BERSERKER ? 2:0)); if(!ill && !deathSickness) { if(!isEffected("bloodsac")) { + if(flagIsSet(P_SITTING)) hpTickAmt += 1; else if(flagIsSet(P_SLEEPING)) @@ -291,6 +295,16 @@ int Player::getHpTickBonus() const { bonus = 0; break; } + + // Trolls have natural regeneration so they get additional hp tick bonus + if(getRace() == TROLL && !isEffected("burning") && + !isEffected("magical-burning") && + !isEffected("death-sickness") && + !isEffected("bloodsac")) + bonus += Random::get(1,3); + + + return(bonus); } @@ -339,6 +353,26 @@ int Player::getMpTickBonus() const { return(bonus); } +int Player::getXPModifiers() const { + + PlayerClass* pClass = gConfig->classes[getClassString()]; + const RaceData* rData = gConfig->getRace(getRace()); + + if(!pClass || !rData) + return(0); + + int raceXPAdjust = rData->getXPAdjustment(); + int classXPAdjust = pClass->getXPAdjustment(); + + if (isCt()) { + if (raceXPAdjust) printColor("^DRace XP Mod........%s%d%% (%s)^x\n", (raceXPAdjust>0?"+":""), raceXPAdjust, rData->getName().c_str()); + if (classXPAdjust) printColor("^DClass XP Mod.......%s%d%% (%s)^x\n", (classXPAdjust>0?"+":""), classXPAdjust, getClassString().c_str()); + } + + return(raceXPAdjust + classXPAdjust); + +} + //********************************************************************* // doDoTs //********************************************************************* @@ -462,9 +496,14 @@ bool Player::doPlayerHarmRooms() { && !isEffected("heat-protection") && !isEffected("alwayswarm") ) { wake("You awaken suddenly"); - printColor("^rThe searing heat burns your flesh.\n"); + printColor("^rThe searing heat %sburns your flesh.\n", ((getRace()==TROLL && room->flagIsSet(R_FIRE_BONUS))?"severely ":"")); dt = BURNED; prot = false; + + //Trolls receive +20% from burn damage, but not from desert weather, only fire rooms + if(room->flagIsSet(R_FIRE_BONUS) && getRace() == TROLL) + dmg += (dmg*20)/100; + } else if( !isEffected("breathe-water") && !doesntBreathe() && ( room->flagIsSet(R_WATER_BONUS) ||( diff --git a/players/post.cpp b/players/post.cpp index 747728fa..0a2c846a 100644 --- a/players/post.cpp +++ b/players/post.cpp @@ -46,9 +46,10 @@ void Player::hasNewMudmail() const { return; const auto filename = (Path::Post / getName()).replace_extension("txt"); + const auto notification = (Path::Post / getName()).replace_extension("notify"); - if(fs::exists(filename)) - printColor("\n^W*** You have new mudmail in the post office.^x\n"); + if(fs::exists(filename) || fs::exists(notification)) + printColor("\n^W*** You have %s mudmail in the post office.^x\n", (fs::exists(notification)?"important":"new")); } @@ -200,7 +201,7 @@ void sendMail(const std::string &target, const std::string &message) { time(&t); strcpy(datestr, (char *) ctime(&t)); datestr[strlen(datestr) - 1] = 0; - auto header = fmt::format("\n--..__..--..__..--..__..--..__..--..__..--..__..--..__..--..__..--..__..--\n\nMail from System ({}):\n\n", datestr); + auto header = fmt::format("\n--..__..--..__..--..__..--..__..--..__..--..__..--..__..--..__..--..__..--\n\nMail from RoH-System ({}):\n\n", datestr); write(ff, header.c_str(), header.length()); write(ff, message.data(), message.length()); @@ -216,6 +217,49 @@ void sendMail(const std::string &target, const std::string &message) { } +void sendSystemNotice(const std::string &target, const std::string &message) { + char datestr[40]; + time_t t = time(nullptr); + int ff = 0; + + // Construct the file path + std::string filePath = (Path::Post / target).replace_extension("notify").c_str(); + + // Attempt to open the file + ff = open(filePath.c_str(), O_CREAT | O_APPEND | O_RDWR, ACC); + if (ff < 0) { + std::cerr << "Error: Unable to open notification file '" << filePath + << "' for " << target << " - " << strerror(errno) << std::endl; + return; // Exit gracefully without throwing an exception + } + + // Get current time string safely and strip newline + strncpy(datestr, ctime(&t), sizeof(datestr) - 1); + datestr[sizeof(datestr) - 1] = '\0'; // Ensure null termination + + // Remove trailing newline from ctime() + size_t len = strlen(datestr); + if (len > 0 && datestr[len - 1] == '\n') { + datestr[len - 1] = '\0'; + } + + // Format the header and footer + std::string header = fmt::format( + "\n^#!!!!********************************************************************!!!!^x\n" + "\n^cRoH System Notification ({}):^x\n\n", datestr); + + constexpr const char* footer = + "\n^#!!!!********************************************************************!!!!^x\n\n"; + + // Write to file + write(ff, header.c_str(), header.length()); + write(ff, message.data(), message.length()); + write(ff, footer, strlen(footer)); + + // Close file + close(ff); +} + //********************************************************************* // postedit @@ -306,15 +350,21 @@ int cmdReadMail(const std::shared_ptr& player, cmd* cmnd) { if(!canPost(player)) return(0); - player->clearFlag(P_UNREAD_MAIL); - const auto filename = (Path::Post / player->getName()).replace_extension("txt"); + + const auto notification = (Path::Post / player->getName()).replace_extension("notify"); + std::error_code ec; + if(fs::exists(notification)) { + player->getSock()->viewFile(notification, false); + fs::remove((Path::Post / player->getName()).replace_extension("notify"), ec); + } - if(!fs::exists(filename)) { + player->clearFlag(P_UNREAD_MAIL); + const auto mailfile = (Path::Post / player->getName()).replace_extension("txt"); + if(!fs::exists(mailfile)) { player->print("You have no mail.\n"); return(0); } - - player->getSock()->viewFile(filename, true); + player->getSock()->viewFile(mailfile, true); return(DOPROMPT); } diff --git a/python/mudEffect.cpp b/python/mudEffect.cpp index 2368a13e..234aa527 100644 --- a/python/mudEffect.cpp +++ b/python/mudEffect.cpp @@ -73,6 +73,8 @@ void init_module_effects(py::module &m) { .def("getName", &Effect::getName) .def("getPulseDelay", &Effect::getPulseDelay) .def("isPulsed", &Effect::isPulsed) + .def("usesStrength", &Effect::usesStrength) + .def("usesVariableStrength", &Effect::usesVariableStrength) ; } \ No newline at end of file diff --git a/python/mudObject.cpp b/python/mudObject.cpp index 64c025b6..210dfab9 100644 --- a/python/mudObject.cpp +++ b/python/mudObject.cpp @@ -84,7 +84,7 @@ void init_module_mudObject(py::module &m) { .def("setFlag", &BaseRoom::setFlag) .def("findCreature", &BaseRoom::findCreaturePython, py::return_value_policy::reference) .def("hasMagicBonus", &BaseRoom::magicBonus) - .def("killMortalObjects", &BaseRoom::killMortalObjects) + .def("killMortalObjects", &BaseRoom::killMortalObjects, "floor"_a=(bool)(true) ) .def("isForest", &BaseRoom::isForest) .def("setTempNoKillDarkmetal", &BaseRoom::setTempNoKillDarkmetal) .def("isSunlight", &BaseRoom::isSunlight) diff --git a/pythonLib/effectLib.py b/pythonLib/effectLib.py index bdc67f49..b2f6f228 100644 --- a/pythonLib/effectLib.py +++ b/pythonLib/effectLib.py @@ -110,6 +110,13 @@ def computeDetect(actor: MudObject, effect: EffectInfo, applier: Optional[MudObj duration += (applier.intelligence.getCur()-140) * 9 if applier.getRoom().hasMagicBonus(): duration += 400 + elif effect.getName() == "empathy": + duration = 300 + duration += (applier.piety.getCur()-140) * 9 + if applier.getClass() == mud.crtClasses.CLERIC: + duration += applier.getLevel() * 30 + if applier.getRoom().hasMagicBonus(): + duration += 400 else: duration = 1200 duration += ((applier.intelligence.getCur() - 140) * 18) diff --git a/questing/quests.cpp b/questing/quests.cpp index 7ad26874..f4dbd32b 100644 --- a/questing/quests.cpp +++ b/questing/quests.cpp @@ -1037,7 +1037,7 @@ bool QuestCompletion::complete(const std::shared_ptr& monster) { oStr << " (" << parentQuest->alignmentChange << ")"; *myPlayer << ColorOn << oStr.str() << ColorOff << "\n"; - myPlayer->setAlignment(std::max(-1000, std::min(1000,(myPlayer->getAlignment()+parentQuest->alignmentChange)))); + myPlayer->setAlignment(std::max(MIN_ALIGN, std::min(MAX_ALIGN,(myPlayer->getAlignment()+parentQuest->alignmentChange)))); myPlayer->alignAdjustAcThaco(); } if(!parentQuest->alignmentChange && parentQuest->alignmentShift) { diff --git a/roguelike/rogues.cpp b/roguelike/rogues.cpp index c88f6f63..ffb2c597 100644 --- a/roguelike/rogues.cpp +++ b/roguelike/rogues.cpp @@ -503,6 +503,10 @@ int cmdHide(const std::shared_ptr& player, cmd* cmnd) { long i = LT(player, LT_HIDE), t = time(nullptr); int chance=0; + const auto dexBns = bonus(player->dexterity.getCur()); + const auto pRace = player->getRace(); + const auto pRoom = player->getRoomParent(); + player->clearFlag(P_AFK); if(!player->ableToDoCommand()) @@ -532,112 +536,128 @@ int cmdHide(const std::shared_ptr& player, cmd* cmnd) { if( player->getClass() == CreatureClass::THIEF || player->getClass() == CreatureClass::ASSASSIN || player->getClass() == CreatureClass::ROGUE || - player->getClass() == CreatureClass::RANGER || + ((player->getClass() == CreatureClass::RANGER || player->getClass() == CreatureClass::DRUID) && !player->isIndoors()) || (player->getClass() == CreatureClass::CLERIC && player->getDeity() == MARA && !isDay()) || (player->getClass() == CreatureClass::CLERIC && player->getDeity() == LINOTHAN && !player->isIndoors()) || - (player->getRace() == DARKELF && player->getRoomParent()->flagIsSet(R_UNDERGROUND)) || - player->getRace() == HALFLING || - player->getRace() == KENKU || - player->getRace() == KOBOLD + (pRace == DARKELF && pRoom->flagIsSet(R_UNDERGROUND)) || + (pRace == ELF && pRoom->isForest()) || + (pRace == WILDELF && !player->isIndoors()) || + pRace == HALFLING || + pRace == KENKU || + pRace == KOBOLD || + pRace == KATARAN ) player->lasttime[LT_HIDE].interval = 5; else if(player->getSecondClass() == CreatureClass::THIEF || player->getSecondClass() == CreatureClass::ASSASSIN || (player->getClass() == CreatureClass::CLERIC && player->getDeity() == KAMIRA)) player->lasttime[LT_HIDE].interval = 6; else - player->lasttime[LT_HIDE].interval = 15; + player->lasttime[LT_HIDE].interval = 10; int level = (int)player->getSkillLevel("hide"); if(cmnd->num == 1) { switch(player->getClass()) { case CreatureClass::THIEF: if(player->getSecondClass() == CreatureClass::MAGE) { - chance = std::min(90, 5 + 4*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 4*level + 3*dexBns); player->lasttime[LT_HIDE].interval = 8; } else - chance = std::min(90, 5 + 6*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 6*level + 3*dexBns); break; case CreatureClass::ASSASSIN: - chance = std::min(90, 5 + 6*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 6*level + 3*dexBns); break; case CreatureClass::FIGHTER: if(player->getSecondClass() == CreatureClass::THIEF) - chance = std::min(90, 5 + 4*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 4*level + 3*dexBns); else - chance = std::min(90, 5 + 2*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 2*level + 3*dexBns); break; case CreatureClass::MAGE: if(player->getSecondClass() == CreatureClass::ASSASSIN || player->getSecondClass() == CreatureClass::THIEF) { - chance = std::min(90, 5 + 4*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 4*level + 3*dexBns); player->lasttime[LT_HIDE].interval = 8; } else chance = std::min(90, 5 + 2*level + - 3*bonus(player->dexterity.getCur())); + 3*dexBns); break; case CreatureClass::CLERIC: if(player->getSecondClass() == CreatureClass::ASSASSIN) - chance = std::min(90, 5 + 5*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 5*level + 3*dexBns); else if ((player->getDeity() == KAMIRA || player->getDeity() == ARACHNUS) && player->alignInOrder()) - chance = std::min(90, 5 + 4*level + 3*bonus(player->piety.getCur())); + chance = std::min(90, 5 + 4*level + 3*dexBns); else if (player->getDeity() == MARA && !isDay() && player->alignInOrder() && !player->isIndoors()) - chance = std::min(90, 5 + 6*level + 3*bonus(player->piety.getCur())); + chance = std::min(90, 5 + 6*level + 3*dexBns); else if (player->getDeity() == LINOTHAN && player->alignInOrder() && !player->isIndoors()) - chance = 5 + 10*level + 3*bonus(player->piety.getCur()); + chance = std::min(90,5 + 10*level + 3*dexBns); else - chance = std::min(90, 5 + 2*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 2*level + 3*dexBns); break; case CreatureClass::RANGER: case CreatureClass::DRUID: - chance = 5 + 10*level + 3*bonus(player->dexterity.getCur()); + chance = 5 + 10*level + 3*dexBns; break; case CreatureClass::ROGUE: - chance = std::min(90, 5 + 5*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 5*level + 3*dexBns); break; default: - chance = std::min(90, 5 + 2*level + 3*bonus(player->dexterity.getCur())); + chance = std::min(90, 5 + 2*level + 3*dexBns); break; } if(player->isStaff()) chance = 101; - if(player->isEffected("camouflage") && player->getRoomParent()->isOutdoors()) + if(player->isEffected("camouflage") && pRoom->isOutdoors()) chance += 20; - if( (player->getRoomParent()->flagIsSet(R_DARK_AT_NIGHT) && !isDay()) || - player->getRoomParent()->flagIsSet(R_DARK_ALWAYS) || - player->getRoomParent()->isEffected("dense-fog") + if( (pRoom->flagIsSet(R_DARK_AT_NIGHT) && !isDay()) || + pRoom->flagIsSet(R_DARK_ALWAYS) || + pRoom->isEffected("dense-fog") ) chance += 10; if(player->dexterity.getCur()/10 < 9 && !(player->getClass() == CreatureClass::CLERIC && (player->getDeity() == KAMIRA || player->getDeity() == MARA || player->getDeity() == LINOTHAN || player->getDeity() == ARACHNUS))) - chance -= 10*(9 - player->dexterity.getCur()/10); // Having less then average dex + chance -= 10*(9 - player->dexterity.getCur()/10); // Having less then average dex is a penalty + + // Racial hide bonuses + if (pRace == WILDELF) { + // Wild Elves get 35% in forests, 10% otherwise + chance += (player->getRoomParent()->isForest() ? (chance * 35) / 100 : (chance * 10) / 100); + } + else if (pRace == ELF) { + // Elves get 25% in forests, 10% otherwise + chance += (player->getRoomParent()->isForest() ? (chance * 25) / 100 : (chance * 10) / 100); + } + else if (pRace == DARKELF && player->getRoomParent()->flagIsSet(R_UNDERGROUND)) { + // Dark elves get +25% bonus when underground + chance += (chance * 25) / 100; + } + else if (pRace == HALFLING || player->getRace() == KENKU) { + // Halflings and Kenku always get a 20% bonus + chance += (chance * 20) / 100; + } + else if (pRace == KOBOLD || pRace == KATARAN) { + // Kobolds always get a 10% bonus + chance += (chance * 10) / 100; + } - *player << "You attempt to hide in the shadows.\n"; + if(player->inCombat()) + chance = 0; - //Racial hide bonuses - if(player->getRace() == ELF && player->getRoomParent()->isForest()) - chance += chance/4; - if(player->getRace() == HALFLING || player->getRace() == KENKU) - chance += chance/5; - if(player->getRace() == KOBOLD) - chance += chance/10; + if(player->isBlind()) + chance = std::min(chance, 20); + + *player << "You attempt to hide in the shadows.\n"; if(player->isIndoors() && (player->getClass() == CreatureClass::RANGER || player->getClass() == CreatureClass::DRUID || (player->getClass() == CreatureClass::CLERIC && player->getDeity() == LINOTHAN))) { - chance /= 2; - chance = std::max(25, chance); - *player << "You have trouble hiding while inside.\n"; + chance = std::max(chance / 2, 1); + *player << "You have trouble hiding while indoors.\n"; } - if(player->inCombat()) - chance = 0; - - if(player->isBlind()) - chance = std::min(chance, 20); - if(Random::get(1,100) <= chance || player->isEffected("mist")) { player->setFlag(P_HIDDEN); *player << "You slip into the shadows unnoticed.\n"; @@ -651,13 +671,14 @@ int cmdHide(const std::shared_ptr& player, cmd* cmnd) { return(0); } - object = player->getRoomParent()->findObject(player, cmnd, 1, true); + object = pRoom->findObject(player, cmnd, 1, true); if(!object) { *player << "You don't see that here.\n"; return(0); } + player->unhide(); if( object->flagIsSet(O_NO_TAKE) && @@ -665,17 +686,39 @@ int cmdHide(const std::shared_ptr& player, cmd* cmnd) { ) return(0); - if(isGuardLoot(player->getRoomParent(), player, "%M will not let you hide that.\n")) + if(isGuardLoot(pRoom, player, "%M will not let you hide that.\n")) return(0); - if(player->isDm()) + if(object->flagIsSet(O_HIDDEN)) { + *player << "It's already hidden!\n"; + return(0); + } + + if (player->isDm()) { chance = 100; - else if(player->getClass() == CreatureClass::THIEF || player->getClass() == CreatureClass::ASSASSIN || player->getClass() == CreatureClass::ROGUE) - chance = std::min(90, 10 + 5*level + 5*bonus(player->dexterity.getCur())); - else if(player->getClass() == CreatureClass::RANGER || player->getClass() == CreatureClass::DRUID) - chance = 5 + 9*level + 3*bonus(player->dexterity.getCur()); - else - chance = std::min(90, 5 + 3*level + 3*bonus(player->dexterity.getCur())); + } + // Thief-like classes get a high chance all the time, anywhere + else if (player->getClass() == CreatureClass::THIEF || + player->getClass() == CreatureClass::ASSASSIN || + player->getClass() == CreatureClass::ROGUE) { + chance = std::min(90, 10 + 5 * level + 5 * dexBns); + } + // Rangers, Druids, Wild Elves, and Elves get bonuses outdoors, but ONLY if in a forest + else if (pRoom->isForest() && + (player->getClass() == CreatureClass::RANGER || + player->getClass() == CreatureClass::DRUID || + pRace == WILDELF || + pRace == ELF)) { + chance = 5 + 9 * level + 3 * dexBns; + } + // Dark Elves get bonuses underground + else if (pRoom->flagIsSet(R_UNDERGROUND) && pRace == DARKELF) { + chance = 5 + 9 * level + 3 * dexBns; + } + // Default case for all other classes/races + else { + chance = std::min(90, 5 + 3 * level + 3 * dexBns); + } *player << "You attempt to hide it.\n"; broadcast(player->getSock(), player->getParent(), "%M attempts to hide %1P.", player.get(), object.get()); @@ -803,13 +846,23 @@ int cmdScout(const std::shared_ptr& player, cmd* cmnd) { return(0); } + std::string scoutExit = cmnd->str[1]; + if (scoutExit == "nw") + strcpy(cmnd->str[1], "northwest"); + else if (scoutExit == "ne") + strcpy(cmnd->str[1], "northeast"); + else if (scoutExit == "sw") + strcpy(cmnd->str[1], "southwest"); + else if (scoutExit == "se") + strcpy(cmnd->str[1], "southeast"); + exit = findExit(player, Move::formatFindExit(cmnd), cmnd->val[1], player->getRoomParent()); if(!exit) { *player << "You don't see that exit.\n"; player->lasttime[LT_SCOUT].ltime = t; - player->lasttime[LT_SCOUT].interval = 10L; + player->lasttime[LT_SCOUT].interval = 2L; return(0); } @@ -826,9 +879,23 @@ int cmdScout(const std::shared_ptr& player, cmd* cmnd) { return(0); if(!alwaysSucceed) { - player->lasttime[LT_SCOUT].ltime = t; - player->lasttime[LT_SCOUT].interval = 20L; + if( (exit->flagIsSet(X_LOCKED) || exit->flagIsSet(X_CLOSED) ) && + !player->checkStaff("The '%s' exit is closed. You cannot scout through a closed exit.\n", exit->getName().c_str()) + ) + return(0); + + if( exit->flagIsSet(X_NEEDS_FLY) && + !player->isEffected("fly") && + !player->checkStaff("You must be flying to scout there.\n") + ) + return(0); + + if((exit->flagIsSet(X_NEEDS_CLIMBING_GEAR) || exit->flagIsSet(X_CLIMBING_GEAR_TO_REPEL)) && !player->checkClimbing()) { + *player << "You need to equip climbing gear to scout that exit.\n"; + return(0); + } + chance = 40 + (int)player->getSkillLevel("scout") * 3; if (player->getClass() == CreatureClass::CLERIC && (player->getDeity() == MARA || player->getDeity() == LINOTHAN)) chance += ((player->piety.getCur()+player->dexterity.getCur())/20); @@ -847,21 +914,11 @@ int cmdScout(const std::shared_ptr& player, cmd* cmnd) { chance = std::min(85, chance); if(!player->isStaff() && Random::get(1, 100) > chance) { - player->print("You fail scout in that direction.\n"); + *player << ColorOn << "You failed scout the '^W" << exit->getName() << "^x' exit.\n" << ColorOff; player->checkImprove("scout", false); return(0); } - if( (exit->flagIsSet(X_LOCKED) || exit->flagIsSet(X_CLOSED) ) && - !player->checkStaff("You cannot scout through a closed exit.\n") - ) - return(0); - - if( exit->flagIsSet(X_NEEDS_FLY) && - !player->isEffected("fly") && - !player->checkStaff("You must fly to scout there.\n") - ) - return(0); } // can't scout where you can't go @@ -880,14 +937,18 @@ int cmdScout(const std::shared_ptr& player, cmd* cmnd) { if(!alwaysSucceed) player->checkImprove("scout", true); - player->printColor("You scout the %s^x exit.\n", exit->getCName()); + *player << ColorOn << "You scout the '^W" << exit->getName() << "^x' exit.\n" << ColorOff; if(player->isStaff() && player->flagIsSet(P_DM_INVIS)) - broadcast(isStaff, player->getSock(), player->getRoomParent(), "%M scouts the %s^x exit.", player.get(), exit->getCName()); + broadcast(isStaff, player->getSock(), player->getRoomParent(), "%M scouts the '^W%s^x' exit.", player.get(), exit->getCName()); else if(exit->flagIsSet(X_SECRET) || exit->isConcealed() || exit->flagIsSet(X_DESCRIPTION_ONLY)) broadcast(player->getSock(), player->getParent(), "%M scouts the area.", player.get()); else - broadcast(player->getSock(), player->getParent(), "%M scouts the %s^x exit.", player.get(), exit->getCName()); + broadcast(player->getSock(), player->getParent(), "%M scouts the '^W%s^x' exit.", player.get(), exit->getCName()); + + player->lasttime[LT_SCOUT].ltime = t; + player->lasttime[LT_SCOUT].interval = 15L; + doScout(player, exit); return(0); @@ -1344,17 +1405,31 @@ int cmdBackstab(const std::shared_ptr& player, cmd* cmnd) { *target << setf(CAP) << player << " attempts to backstab you!\n"; broadcast(player->getSock(), target->getSock(), player->getParent(), "%M attempts to backstab %N.", player.get(), target.get()); - if(target->isMonster()) + if(target->isMonster()) { target->getAsMonster()->addEnemy(player); + if(player->flagIsSet(P_LAG_PROTECTION_SET)) // Activates Lag protection. + player->setFlag(P_LAG_PROTECTION_ACTIVE); + } + if(player->breakObject(player->ready[WIELD-1], WIELD)) { broadcast(player->getSock(), player->getParent(), "%s backstab failed.", player->upHisHer()); player->setAttackDelay(player->getAttackDelay()*2); return(0); } + int skillLevel = (int)((player->getWeaponSkill(weapon) + (player->getSkillGained("backstab")*2)) / 3); + if(player->getRace() == KATARAN) + skillLevel += (skillLevel*10)/100; + + //Barbarians are naturally hyper aware - harder to backstab them. + if(target->getRace() == BARBARIAN) + skillLevel -= (skillLevel*30)/100; + + skillLevel = std::max(1,skillLevel); + AttackResult result = player->getAttackResult(target, weapon, DOUBLE_MISS, skillLevel); if(result == ATTACK_HIT || result == ATTACK_CRITICAL || result == ATTACK_BLOCK || result == ATTACK_GLANCING) { @@ -1575,7 +1650,7 @@ int cmdBackstab(const std::shared_ptr& player, cmd* cmnd) { } } else if(result == ATTACK_FUMBLE) { player->statistics.fumble(); - *player << ColorOn << "^gYou FUMBLED your weapon.\n" << ColorOff; + *player << ColorOn << "^gYou FUMBLED " << ((weapon && !weapon->flagIsSet(O_NO_PREFIX))?"your ":" ") << (weapon?weapon->getName():"your attack") << ".\n" << ColorOff; broadcast(player->getSock(), player->getParent(), "^g%M fumbled %s weapon.", player.get(), player->hisHer()); if(weapon->flagIsSet(O_ENVENOMED)) { diff --git a/roguelike/steal.cpp b/roguelike/steal.cpp index 27f5b6f8..c2a65040 100644 --- a/roguelike/steal.cpp +++ b/roguelike/steal.cpp @@ -551,7 +551,8 @@ int cmdSteal(const std::shared_ptr& player, cmd* cmnd) { // A roll of 100 always fails if(!cantSteal && ((roll <= chance && roll < 100) || player->isCt()) ) { - player->print("You succeeded.\n"); + //player->print("You succeeded.\n"); + *player << "Success! You stole " << object << " from " << target << "!\n"; player->checkImprove("steal", true); player->statistics.steal(); @@ -610,7 +611,7 @@ int cmdSteal(const std::shared_ptr& player, cmd* cmnd) { if(roll <= caught || isCt(bystander)) { // If roll is less than 10% of chance, bystander will see // what was trying to be stolen. - if(roll <= chance/10 || isCt(bystander)) + if(roll <= chance/10 || isCt(bystander)) bystander->printColor("%M tried to steal %1P from %N.\n", player.get(), object.get(), target.get()); else bystander->print("%M tried to steal something from %N.\n", player.get(), target.get()); @@ -693,7 +694,7 @@ int cmdSteal(const std::shared_ptr& player, cmd* cmnd) { broadcast(player->getSock(), target->getSock(), room, "%M tried to steal from %N.", player.get(), target.get()); if(!mTarget) { - if(!player->isEffected("blindness")) + if(!target->isEffected("blindness")) target->printColor("%M tried to steal %1P from you.\n", player.get(), object.get()); else target->print("Someone tried to steal something from you.\n"); diff --git a/roguelike/traps.cpp b/roguelike/traps.cpp index 8ababbb1..f382a981 100644 --- a/roguelike/traps.cpp +++ b/roguelike/traps.cpp @@ -992,48 +992,59 @@ void Player::loseAll(bool destroyAll, std::string lostTo) { //********************************************************************* // dissolveItem //********************************************************************* -// dissolve_item will randomly select one equipted (including held or -// wield) items on the given player and then delete it. The player +// dissolve_item will randomly select one equipped (including held or +// wielded) item on the given player and then delete it. The player // receives a message that the item was destroyed as well as who is // responsible for the deed. - void Player::dissolveItem(std::shared_ptr creature) { - char checklist[MAXWEAR]; - int numwear=0, i=0, n=0; + + if(!creature) + return; - for(i=0; i wearableSlots; - if(!numwear) - n = 0; - else { - i = Random::get(0, numwear-1); - n = (int) checklist[i]; - } - if(n) { - if(ready[n-1]) { - if(ready[n - 1]->flagIsSet(O_RESIST_DISOLVE)) { - printColor("%M tried to dissolve your %s.\n", creature.get(), ready[n-1]->getCName()); - n = 0; - return; - } + // Collect valid wearable slots + for (int i = 0; i < MAXWEAR; i++) { + if (!ready[i]) continue; // Skip empty slots + + if (creature->flagIsSet(M_DISSOLVES_ALL)) { + wearableSlots.push_back(i); + continue; // No need to check other dissolve flags } + + if ((creature->flagIsSet(M_DISSOLVES_ALL_METAL) && ready[i]->isMetal()) || + (creature->flagIsSet(M_DISSOLVES_FERROUS_METAL) && ready[i]->isFerrousMetal()) || + (creature->flagIsSet(M_DISSOLVES_NONFERROUS_METAL) && ready[i]->isNonFerrousMetal()) || + (creature->flagIsSet(M_DISSOLVES_ORGANIC) && ready[i]->isOrganic()) || + (creature->flagIsSet(M_DISSOLVES_STONE) && ready[i]->isStone())) { + wearableSlots.push_back(i); + } + } + + // If no items to dissolve, exit + if (wearableSlots.empty()) { + return; } + // Pick a random item from the list + int index = (wearableSlots.size() == 1) ? 0 : Random::get(0, static_cast(wearableSlots.size() - 1)); + int slot = wearableSlots[index]; - if(n) { - broadcast(getSock(), getRoomParent(),"%M destroys %N's %s.", creature.get(), this, ready[n-1]->getCName()); - printColor("%M destroys your %s.\n",creature.get(), ready[n-1]->getCName()); - logn("log.dissolve", "%s(L%d) lost %s to acid in room %s.\n", - getCName(), level, ready[n-1]->getCName(), getRoomParent()->fullName().c_str()); - // Unequip it and don't add it to the inventory, delete it - unequip(n, UNEQUIP_DELETE); - computeAC(); + // Check for dissolve resistance + we do not want to dissolve bags somebody might be holding + if (ready[slot] && (ready[slot]->flagIsSet(O_RESIST_DISOLVE) || ready[slot]->getType() == ObjectType::CONTAINER)) { + printColor("^g%M tried to dissolve your %s.^x\n", creature.get(), ready[slot]->getCName()); + return; } + + // Broadcast destruction + broadcast(getSock(), getRoomParent(), "^g%M dissolved %N's %s.^x", creature.get(), this, ready[slot]->getCName()); + printColor("^g%M dissolved your %s.^x\n", creature.get(), ready[slot]->getCName()); + logn("log.dissolve", "%s(L%d) lost %s to acid in room %s.\n", + getCName(), level, ready[slot]->getCName(), getRoomParent()->fullName().c_str()); + + // Unequip the item and remove it permanently + unequip(slot + 1, UNEQUIP_DELETE); + computeAC(); } //********************************************************************* diff --git a/server/access.cpp b/server/access.cpp index 34059d24..af2537b8 100644 --- a/server/access.cpp +++ b/server/access.cpp @@ -54,9 +54,9 @@ char shortClassAbbrev[][8] = { "A", "Be", "Cl", "F", "M", "P", "R", "T", "Va", " char mob_skill_str[][16] = { "Horrible", "Poor", "Fair", "Decent", "Average", "Talented", "Very Good", "Exceptional", "Master", "Grand Master", "Godlike" }; -int permAC[30] = { +int permAC[40] = { 25, 70, 110, 155, 200, 245, 290, 335, 380, 425, 470, 510, 535, 560, 580, 600, 620, 645, - 670, 690, 710, 730, 760, 780, 820, 870, 890, 910, 960, 1000 }; + 670, 690, 710, 730, 760, 780, 820, 870, 890, 910, 960, 1000, 1080, 1160, 1250, 1340, 1490, 1630, 1780, 1890, 1980, 2100}; // @@ -71,22 +71,22 @@ char lang_color[LANGUAGE_COUNT][3] = { "^c", // Common "^y", // Orcish "^y", // Giantkin - "^b", // Gnomish + "^C", // Gnomish "^g", // Trollish "^y", // Ogrish "^m", // Dark-Elven (Drow) "^g", // Goblinoid - "^y", // Minotaur + "^r", // Minotaur "^B", // Celestial "^c", // Kobold "^R", // Infernal - "^y", // Schnai + "^C", // Schnai "^r", // Kataran "^g", // Druidic "^y", // Wolfen "^m", // Thieves' Cant - "^m", // Arcanic (Mages/Lich) - "^m", // Abyssal + "^M", // Arcanic (Mages/Lich) + "^D", // Abyssal "^r", // Tiefling "^y", // Kenku "^M", // Fey @@ -100,13 +100,29 @@ char lang_color[LANGUAGE_COUNT][3] = { "^Y", // Firbolg "^r", // Satyr "^m", // Quickling + "^D", // Undercommon + "^c", // Svirfneblin + "^M", // Draconic + "^y", // Primordial + "^Y", // Thri-Kreen + "^y", // Sylvan + "^C", // Gith + "^Y", // Sphinx + "^G", // Pixie + "^g", // Leprechaun + "^D", // Necril + "^W", // Modron + "^D", // Noctis + "^G" // Grugach (Wild Elf) }; -char language_adj[][32] = { "an alien language", "dwarven", "elven", "halfling", "common", "orcish", "giantkin", - "gnomish", "trollish", "ogrish", "darkelf", "goblinoid", "minotaur", "celestial", "kobold", "infernal", - "barbarian", "kataran", "druidic", "wolfen", "thieves' cant", "arcanic", "abyssal", "tiefling", "kenku", - "fey", "lizardman", "centaur", "duergar", "gnoll", "bugbear", "hobgoblin", "brownie", "firbolg", "satyr", "quickling"}; +char language_adj[][32] = { "an alien language", "Dwarvish", "Elvish", "Halfling", "Common", "Orcish", "Giantkin", + "Gnomish", "Trollish", "Ogrish", "Darkelf", "Goblinoid", "Minotaur", "Celestial", "Kobold", "Infernal", + "Schnai", "Kataran", "Druidic", "Wolfen", "Thieves' Cant", "Arcanic", "Abyssal", "Tiefling", "Kenku", + "Fey", "Lizardman", "Centaur", "Duergar", "Gnoll", "Bugbear", "Hobgoblin", "Brownie", "Firbolg", "Satyr", "Quickling", + "Undercommon", "Svirfneblin", "Draconic", "Primordial", "Thri-kreen", "Sylvan", "Gith", "Sphinx", "Pixie", "Leprechaun", + "Necril", "Modron", "Noctis", "Grugach"}; char language_verb[][3][24] = { // Unknown (alien) @@ -114,37 +130,37 @@ char language_verb[][3][24] = { // Dwarven {"mutter", "utter", "grumble"}, // Elven - {"say", "speak", "lecture"}, + {"say", "lilt", "lecture"}, // Halfling {"say", "speak", "utter"}, // Common - {"say", "speak", "chat"}, + {"say", "drawl", "chat"}, // Orcish {"grunt", "squeal", "snort"}, // Giantkin - {"boom", "speak", "bellow"}, + {"boom", "rumble", "bellow"}, // Gnomish {"say", "speak", "utter"}, // Trollish*/ - {"snarl", "spit", "gutterly cough"}, + {"snarl", "spit", "rasp"}, // Ogrish {"grunt", "snarl", "boom"}, // Dark elven {"scoff", "sneer", "preen"}, // Goblinoid - {"sputter", "snort", "cough"}, + {"sputter", "chitter", "cough"}, // Minotaur {"snort", "grunt", "speak"}, // Celestial - {"speak", "sing", "eloquently speak"}, + {"speak", "sing", "orate"}, // Kobold {"bark", "snort", "growl"}, // Infernal - {"snarl", "growl", "gutterly speak"}, + {"snarl", "growl", "seethe"}, // Schnai {"growl", "snarl", "sneer"}, // Kataran - {"purr", "growl", "spit"}, + {"hiss", "growl", "spit"}, // Druidic {"mutter", "say", "sound"}, // Wolfen @@ -152,13 +168,13 @@ char language_verb[][3][24] = { // Thieves' Cant {"gesture", "allude", "mumble"}, // Arcanic - {"gibber", "quickly speak", "chatter"}, + {"jabber", "rattle", "chatter"}, // Abyssal - {"growl", "snarl", "cough"}, + {"shriek", "howl", "gibber"}, // Tiefling - {"jabber", "spit", "snarl"}, + {"hiss", "spit", "snarl"}, //Kenku - {"squawk", "cluck", "screech"}, + {"squawk", "mimic", "screech"}, //Fey {"sing", "whistle", "buzz"}, //Lizardman @@ -176,11 +192,39 @@ char language_verb[][3][24] = { //Brownie {"squeak", "scritch", "shrill"}, //Firbolg - {"grunt", "bellow", "heavily cough"}, + {"grunt", "bellow", "murmur"}, //Satyr - {"groan", "gutterly growl", "grunt"}, + {"groan", "grate", "rasp"}, //Quickling - {"sputter", "rapidly squeak", "jabber"} + {"sputter", "rapidly squeak", "jabber"}, + //Undercommon + {"grunt", "growl", "rasp"}, + //Svirfneblin + {"whisper", "murmur", "mutter"}, + //Draconic + {"hiss", "growl", "roar"}, + //Primordial + {"vibrate", "rumble", "resonate"}, + //Thri-Kreen + {"click", "drone", "rasp"}, + //Sylvan + {"chime", "lilt", "whisper"}, + //Gith + {"intone", "utter", "growl"}, + //Sphinx + {"pronounce", "proclaim", "riddle"}, + //Pixie + {"squeak", "chatter", "trill"}, + //Leprechaun + {"jest", "babble", "cackle"}, + //Necril + {"rasp", "moan", "lament"}, + //Modron + {"state", "drone", "calculate"}, + //Noctis + {"hiss", "rasp", "enthrall"}, + //Grugach + {"chatter", "grunt", "snap"} }; @@ -269,7 +313,7 @@ std::string getOrdinal(int num) { last = getLastDigit(num, 2); - if(last > 10&& last < 14) + if(last > 10 && last < 14) ordinal += "th"; else { last = getLastDigit(num, 1); @@ -293,7 +337,7 @@ std::string getOrdinal(int num) { } int get_perm_ac(int nIndex) { - nIndex = std::max( 0, std::min(nIndex, 29 ) ); + nIndex = std::max( 0, std::min(nIndex, 39 ) ); return(permAC[nIndex]); } diff --git a/server/login.cpp b/server/login.cpp index e9429d4b..680b29ba 100644 --- a/server/login.cpp +++ b/server/login.cpp @@ -566,7 +566,7 @@ void createPlayer(std::shared_ptr sock, const std::string& str) { if(!Create::getSubRace(sock, str, Create::doWork)) return; - Create::getClass(sock, str, Create::doPrint); + Create::getSex(sock, str, Create::doPrint); return; case CREATE_GET_SEX: @@ -608,6 +608,13 @@ void createPlayer(std::shared_ptr sock, const std::string& str) { Create::startCustom(sock, str, Create::doPrint); return; + case CREATE_GET_VERIFY_RACE: + if(!Create::getVerifyRace(sock, str, Create::doWork)) + return; + Create::getSex(sock, str, Create::doPrint); + Create::getClass(sock, str, Create::doPrint); + return; + case CREATE_GET_STATS: if(!Create::getStats(sock, str, Create::doWork)) @@ -976,8 +983,19 @@ bool Create::getRace(const std::shared_ptr& sock, std::string str, int m } if(!sock->getPlayer()->getRace()) return(false); - if(!gConfig->getRace(sock->getPlayer()->getRace())->isParent()) + if(!gConfig->getRace(sock->getPlayer()->getRace())->isParent()) { + + int xpadjust = gConfig->getRace(sock->getPlayer()->getRace())->getXPAdjustment(); + if(xpadjust < 0) { + sock->printColor("Race ^W%s^x currently has an earned experience penalty of %s%d%%.\n", gConfig->getRace(sock->getPlayer()->getRace())->getName().c_str(), (xpadjust<0?"":"+"), xpadjust); + sock->printColor("This will make leveling up more difficult. Are you sure you want %s as your race? (Y/N):", gConfig->getRace(sock->getPlayer()->getRace())->getName().c_str()); + Create::getVerifyRace(sock, "", Create::doPrint); + sock->setState(CREATE_GET_VERIFY_RACE); + return(false); + } + Create::finishRace(sock); + } } return(true); } @@ -1042,10 +1060,30 @@ bool Create::getSubRace(const std::shared_ptr& sock, std::string str, in if(choices.find(k) != choices.end()) { sock->getPlayer()->setRace(choices[k]->getId()); + int xpadjust = gConfig->getRace(sock->getPlayer()->getRace())->getXPAdjustment(); + if(xpadjust < 0) { + sock->printColor("Race ^W%s^x currently has an earned experience penalty of %s%d%%.\n", gConfig->getRace(sock->getPlayer()->getRace())->getName().c_str(), (xpadjust<0?"":"+"), xpadjust); + sock->printColor("This will make leveling up more difficult. Are you sure you want %s as your race? (Y/N):", gConfig->getRace(sock->getPlayer()->getRace())->getName().c_str()); + Create::getVerifyRace(sock, "", Create::doPrint); + sock->setState(CREATE_GET_VERIFY_RACE); + return(false); + } + + Create::finishRace(sock); return(true); } - return(false); + else { + + Create::getSubRace(sock, "", Create::doPrint); + sock->setState(CREATE_GET_SUBRACE); + return(false); + } + + //return(false); + + if(!sock->getPlayer()->getRace()) + return(false); } return(true); } @@ -1058,6 +1096,7 @@ void Create::finishRace(const std::shared_ptr& sock) { const RaceData* race = gConfig->getRace(sock->getPlayer()->getRace()); sock->printColor("\nYour chosen race: ^W%s^x\n\n", race->getName().c_str()); + sock->getPlayer()->setSize(race->getSize()); sock->getPlayer()->initLanguages(); @@ -1281,6 +1320,38 @@ bool Create::startCustom(const std::shared_ptr& sock, std::string str, i } return(true); } + + +//********************************************************************* +// getVerifyRace +//********************************************************************* +bool Create::getVerifyRace(const std::shared_ptr& sock, std::string str, int mode) { + if(mode == Create::doPrint) { + sock->setState(CREATE_GET_VERIFY_RACE); + return(false); + } + else if (mode == Create::doWork) { + boost::trim(str); + if (str.length() > 0) { + if(tolower(str.at(0)) == 'y') { + Create::finishRace(sock); + return(true); + } else if(tolower(str.at(0)) == 'n') { + Create::getRace(sock, "", Create::doPrint); + sock->setState(CREATE_GET_RACE); + return(false); + } + } + } + + Create::getVerifyRace(sock, "", Create::doPrint); + sock->print("\nPlease choose Y or N:\n: "); + sock->setState(CREATE_GET_VERIFY_RACE); + return(false); + + return(true); +} + //********************************************************************* // getStatsChoice //********************************************************************* @@ -1293,13 +1364,19 @@ bool Create::getStatsChoice(const std::shared_ptr& sock, std::string str return(false); } - sock->print("\nFor character stats, you may:\n"); + sock->print("\nYour character is defined by five core attributes, each shaping how they navigate the world:"); + sock->printColor("\n^cStrength^x.........Your sheer physical might. More strength means harder hits and greater feats of power."); + sock->printColor("\n^cDexterity^x........Your agility and reflexes. Speed, precision, and finesse can turn the tide of battle."); + sock->printColor("\n^cConstitution^x.....Your toughness and stamina. The greater your endurance, the longer you can survive punishment."); + sock->printColor("\n^cIntelligence^x.....Your reasoning and knowledge. A sharp mind is key to mastering magic, tactics, and learning."); + sock->printColor("\n^cPiety^x............Your willpower and spiritual strength. Vital for those who seek divine favor or unwavering resolve."); - sock->printColor("\n[^WC^x]hoose your own stats"); - sock->printColor("\n[^WU^x]se predefined stats provided by the mud"); - - sock->print("\n\nNote: For beginners that are unfamiliar with game mechanics, it is highly recommended to use predefined stats to reduce the learning curve.\n"); + sock->printColor("\n\nSet your character's attributes:"); + sock->printColor("\n[^WC^x]hoose - You'll manually assign points to each attribute, customizing your strengths and weaknesses."); + sock->printColor("\n[^WU^x]se prefefined - We will select predefined attributes optimized for your chosen class."); + sock->print("\n\nNote: If you are a beginner here or are generally new to MUDs, we highly recommended choosing the predefined option.\n"); + sock->askFor(": "); sock->setState(CREATE_GET_STATS_CHOICE); @@ -1308,11 +1385,19 @@ bool Create::getStatsChoice(const std::shared_ptr& sock, std::string str boost::trim(str); if (str.length() > 0) { if(tolower(str.at(0)) == 'c') { - sock->print("You have chosen to select your own stats.\n"); + sock->print("You have chosen to select your own attributes.\n"); Create::getStats(sock, "", Create::doPrint); // We've set the next state so don't change it after we return return(false); } else if(tolower(str.at(0)) == 'u') { + + if(usePredefinedStatsUnavailable(sock->getPlayer()->getRace())) { + sock->printColor("^yDue to large attribute adjustments for race ^c%s^y, the predefined option is not available.^x\n", gConfig->getRace(sock->getPlayer()->getRace())->getName().c_str()); + sock->printColor("^yYou will need to choose your own initial attributes.^x\n"); + Create::getStats(sock, "", Create::doPrint); + return(false); + } + PlayerClass *pClass = gConfig->classes[sock->getPlayer()->getClassString()]; if(!pClass) { Create::getStats(sock, str, Create::doPrint); @@ -1325,11 +1410,66 @@ bool Create::getStatsChoice(const std::shared_ptr& sock, std::string str } } + sock->print("\nPlease choose [C] or [U]"); sock->askFor(": "); sock->setState(CREATE_GET_STATS_CHOICE); } return(false); } +std::string getRacialBonusesString(short race, bool full) { + short num = 0; + const std::string abbrevStat[7] = { "none", "STR", "DEX", "CON", "INT", "PIE", "CHA" }; + std::ostringstream oStr; + + auto playerRace = gConfig->getRace(race); + + if(playerRace == nullptr) + return(nullptr); + + if(race == HUMAN) + return("You'll choose one attribute to raise and one attribute to lower"); + + for (int i = 0; i < 5; ++i) { + num = playerRace->getStatAdj(i + 1) / 10; + + oStr << (full?getFullStatName(i+1,true):abbrevStat[i + 1]) << ": "; + + if (num == 0) { + oStr << "-"; + } else { + oStr << (num > 0 ? "+" : "") << num; + } + + oStr << " "; + } + + return oStr.str(); +} + +//********************************************************************* +// usePredefinedStatusUnavailable +//********************************************************************* +// The predefined stats are all stored in the classes.xml file. Some +// races might have racial stat adjustments that knock a newly created +// character's stats to 0. For example, if a race has a -30 adjustment. +// We do not want that. This function is a safeguard against that. It +// checks the gConfig for the chosen race, and if it finds any initial +// stat adjustment that is lower than -20, it will return true. The +// player will then be forced to choose their own stats, where safeguards +// to keep a stat from going below 10 are in place. +bool usePredefinedStatsUnavailable(short race) { + + short num = 0; + auto playerRace = gConfig->getRace(race); + + for (int i = 0; i < 5; ++i) { + num = playerRace->getStatAdj(i+1); + if (num < -20) + return(true); + } + + return(false); +} //********************************************************************* @@ -1337,13 +1477,19 @@ bool Create::getStatsChoice(const std::shared_ptr& sock, std::string str //********************************************************************* bool Create::getStats(const std::shared_ptr& sock, std::string str, int mode) { - if(mode == Create::doPrint) { - sock->print("\nYou have 56 points to distribute among your 5 stats. Please enter your 5"); - sock->print("\nnumbers in the following order: Strength, Dexterity, Constitution,"); - sock->print("\nIntelligence, Piety. No stat may be smaller than 3 or larger than 18."); - sock->print("\nUse the following format: ## ## ## ## ##\n\n"); + std::string raceHelpfile = stripSpaces(gConfig->getRace(sock->getPlayer()->getRace())->getName()); + lowercize(raceHelpfile,0); + if(mode == Create::doPrint) { + sock->printColor("\nYou have 56 points to distribute across your 5 core attributes: ^cStrength, Dexterity, Constitution, Intelligence, Piety^x."); + sock->printColor("\n\nEach attribute value you choose will be adjusted due to your chosen race: ^c%s^x.", gConfig->getRace(sock->getPlayer()->getRace())->getName().c_str()); + sock->printColor("\nThe adjustments for ^c%s^x are as follows: ^c%s^x\n", + gConfig->getRace(sock->getPlayer()->getRace())->getName().c_str(),getRacialBonusesString(sock->getPlayer()->getRace(), true).c_str()); + + sock->print("\n\nPlease enter your 5 attribute numbers in order, using the following format: ## ## ## ## ##"); + sock->printColor("\nNOTE: Each value you choose must be at least 3 and no higher than 18. Also, none must be adjusted to below 1.\n"); + sock->askFor(": "); sock->setState(CREATE_GET_STATS); @@ -1369,9 +1515,22 @@ bool Create::getStats(const std::shared_ptr& sock, std::string str, int return(false); } + short statAdjustment=0; + for(i=0; i<5; i++) { if(num[i] < 3 || num[i] > 18) { - sock->print("No stats < 3 or > 18 please.\n"); + sock->print("No values may be < 3 or > 18. Please try again.\n"); + sock->print(": "); + sock->setState(CREATE_GET_STATS); + return(false); + } + statAdjustment = gConfig->getRace(sock->getPlayer()->getRace())->getStatAdj(i+1); + if(((num[i]*10)+statAdjustment) < 10) { + sock->printColor("For race ^c%s^x, the initially chosen value for %s cannot be less than %d.", + gConfig->getRace(sock->getPlayer()->getRace())->getName().c_str(), getFullStatName(i+1).c_str(), abs(statAdjustment/10)+1); + sock->printColor("\n\nThe initial adjustments for race ^c%s^x are as follows: \n^c%s^x", + gConfig->getRace(sock->getPlayer()->getRace())->getName().c_str(),getRacialBonusesString(sock->getPlayer()->getRace(),true).c_str()); + sock->print("\n\nPlease choose five numbers again.\n"); sock->print(": "); sock->setState(CREATE_GET_STATS); return(false); @@ -1380,7 +1539,7 @@ bool Create::getStats(const std::shared_ptr& sock, std::string str, int } if(sum != 56) { - sock->print("Stat total must equal 56 points, yours totaled %d.\n", sum); + sock->print("Attribute total must equal 56 points, yours totaled %d.\n", sum); sock->print(": "); sock->setState(CREATE_GET_STATS); return(false); @@ -1429,7 +1588,7 @@ void Create::finishStats(const std::shared_ptr& sock) { bool Create::getBonusStat(const std::shared_ptr& sock, std::string str, int mode) { if(mode == Create::doPrint) { - sock->print("\nRaise which stat?\n[A] Strength, [B] Dexterity, [C] Constitution, [D] Intelligence, or [E] Piety.\n : "); + sock->print("\nRaise which attribute?\n[A] Strength, [B] Dexterity, [C] Constitution, [D] Intelligence, or [E] Piety.\n : "); sock->setState(CREATE_BONUS_STAT); } else if(mode == Create::doWork) { @@ -1467,7 +1626,7 @@ bool Create::getBonusStat(const std::shared_ptr& sock, std::string str, bool Create::getPenaltyStat(const std::shared_ptr& sock, std::string str, int mode) { if(mode == Create::doPrint) { - sock->printColor("\nChoose your stat to lower:\n[^WA^x] Strength, [^WB^x] Dexterity, [^WC^x] Constitution, [^WD^x] Intelligence, or [^WE^x] Piety.\n : "); + sock->printColor("\nChoose an attribute to lower:\n[^WA^x] Strength, [^WB^x] Dexterity, [^WC^x] Constitution, [^WD^x] Intelligence, or [^WE^x] Piety.\n : "); sock->setState(CREATE_PENALTY_STAT); } else if(mode == Create::doWork) { @@ -1919,6 +2078,9 @@ void Create::done(const std::shared_ptr& sock, const std::string &str, i Create::addStartingItem(player, "tut", 40); Create::addStartingItem(player, "tut", 42, false, true, 3); + if(player->knowsSkill("bash")) + Create::addStartingItem(player, "tut", 48); + player->fd = sock->getFd(); player->setSock(sock); diff --git a/server/update.cpp b/server/update.cpp index 48f68451..466a6d8f 100644 --- a/server/update.cpp +++ b/server/update.cpp @@ -749,8 +749,7 @@ void Server::updateAction(long t) { resp = act->response; if(isdigit(*(resp))) { - - num = 10*(toNum(resp)); + num = 10*std::stoi(resp); ++resp; num = (num == 0) ? 100:num; @@ -772,26 +771,12 @@ void Server::updateAction(long t) { broadcast((std::shared_ptr )nullptr, monster->getRoomParent(), "%M says, \"%s\"", monster.get(), resp); break; case 'T': // Mob Trash-talk - if(Random::get(1,100) <= 10) { - - if(countTotalEnemies(monster) > 0 && !monster->getAsMonster()->nearEnemy()) { - if(monster->daily[DL_BROAD].cur > 0) { - broadcast("### %M broadcasted, \"%s\"", monster.get(), resp); - subtractMobBroadcast(monster, 0); - } - } - } + if(countTotalEnemies(monster) > 0 && !monster->getAsMonster()->nearEnemy() && thresh <= num) + broadcast("### %M broadcasted, \"%s\"", monster.get(), resp); break; - case 'B': // Mob general random broadcasts - if(Random::get(1,100) <= 10) { - - if(countTotalEnemies(monster) < 1 && thresh <= num) { - if(monster->daily[DL_BROAD].cur > 0) { - broadcast("### %M broadcasted, \"%s\"", monster.get(), resp); - subtractMobBroadcast(monster, 0); - } - } - } + case 'B': // Mob general random broadcasts - if has no enemies + if(countTotalEnemies(monster) < 1 && thresh <= num) + broadcast("### %M broadcasted, \"%s\"", monster.get(), resp); break; case 'A': // attack monster in target string if(monster->first_tlk->target && !monster->getAsMonster()->hasEnemy()) { diff --git a/skills/skillLoader.cpp b/skills/skillLoader.cpp index 02ec258f..4fa0950a 100644 --- a/skills/skillLoader.cpp +++ b/skills/skillLoader.cpp @@ -116,6 +116,7 @@ bool Config::loadSkills() { addToMap(SkillInfoBuilder() .name("charm").displayName("Charm") .group("general") + .gainType(SkillGainType::HARD) , skills); addToMap(SkillInfoBuilder() .name("sing").displayName("Sing") @@ -145,6 +146,7 @@ bool Config::loadSkills() { addToMap(SkillInfoBuilder() .name("steal").displayName("Steal") .group("general") + .gainType(SkillGainType::HARD) , skills); addToMap(SkillInfoBuilder() .name("search").displayName("Search") @@ -311,6 +313,16 @@ bool Config::loadSkills() { .group("divine") .gainType(SkillGainType::MEDIUM) , skills); + addToMap(SkillInfoBuilder() + .name("day").displayName("Day") + .group("divine") + .gainType(SkillGainType::HARD) + , skills); + addToMap(SkillInfoBuilder() + .name("night").displayName("Night") + .group("divine") + .gainType(SkillGainType::HARD) + , skills); addToMap(SkillInfoBuilder() .name("protection").displayName("Protection") .group("divine") @@ -361,6 +373,14 @@ bool Config::loadSkills() { .name("gore").displayName("Gore") .group("offensive") , skills); + addToMap(SkillInfoBuilder() + .name("smash").displayName("Smash") + .group("offensive") + , skills); + addToMap(SkillInfoBuilder() + .name("slam").displayName("Slam") + .group("offensive") + , skills); addToMap(SkillInfoBuilder() .name("disarm").displayName("Disarm") .group("offensive") diff --git a/skills/skills.cpp b/skills/skills.cpp index a23b823e..6414b8f5 100644 --- a/skills/skills.cpp +++ b/skills/skills.cpp @@ -221,6 +221,14 @@ void Creature::checkImprove(const std::string& skillName, bool success, int att chance += crSkill->getGainBonus(); } + //Humans get +20% bonus chance on all skill raises + if (getRace()==HUMAN) + chance += (chance*20)/100; + + //Half-Elf and Half-Orc get +10% bonus chance on all skill raises + if (getRace()==HALFELF || getRace()==HALFORC) + chance += (chance*10)/100; + // Unless max for level, 3% chance minimum chance = std::max(chance, 3); diff --git a/staff/dm.cpp b/staff/dm.cpp index 0f73715a..5663b4ab 100644 --- a/staff/dm.cpp +++ b/staff/dm.cpp @@ -1748,11 +1748,24 @@ bool dmGlobalSpells(const std::shared_ptr& player, int splno, bool check player->hp.restore(); player->mp.restore(); player->removeEffect("death-sickness"); + player->curePoison(); + player->cureDisease(); + player->removeCurse(); + player->removeEffect("blindness"); + player->removeEffect("deafness"); + player->removeEffect("silence"); + player->removeEffect("petrification"); break; case S_HEAL: if(check) return(true); - if(player->getClass() != CreatureClass::LICH) + if(player->getClass() != CreatureClass::LICH) { player->hp.restore(); + player->curePoison(); + player->cureDisease(); + player->removeCurse(); + player->removeEffect("blindness"); + player->removeEffect("deafness"); + } break; case S_BLESS: if(check) return(true); diff --git a/staff/dmcrt.cpp b/staff/dmcrt.cpp index d1490d00..6c27236e 100644 --- a/staff/dmcrt.cpp +++ b/staff/dmcrt.cpp @@ -316,6 +316,9 @@ std::string Creature::statCrt(int statFlags) { if(size) crtStr << "Size: ^Y" << getSizeName(size) << "^x\n"; + if(mTarget && mTarget->getPermSpawnChance()) + crtStr << "^cPerm spawn chance: " << mTarget->getPermSpawnChance() << "/1000^x\n"; + if(clan) crtStr << "Clan: " << gConfig->getClan(clan)->getName() << "(" << clan << ")\n"; if(pTarget && pTarget->getGuild()) @@ -824,7 +827,7 @@ int dmSetCrt(const std::shared_ptr& player, cmd* cmnd) { target->setAlignment((short)(target->isPlayer()?((P_TOP_ROYALBLUE+P_BOTTOM_ROYALBLUE)/2):((M_TOP_ROYALBLUE+M_BOTTOM_ROYALBLUE)/2))); else { al_match=false; - *player << ColorOn << "^yInvalid alignment: '" << align_txt << "'\nEnter in alignment name or number. i.e. name: bloodred, red, royalblue, etc.. or number (-1000 to 1000)\nNOTE: 'royal blue' or 'blood red' will not work. Use no spaces.\n" << ColorOff; + *player << ColorOn << "^yInvalid alignment: '" << align_txt << "'\nEnter in alignment name or number. i.e. name: bloodred, red, royalblue, etc.. or number (" << MIN_ALIGN << " to " << MAX_ALIGN << ")\nNOTE: 'royal blue' or 'blood red' will not work. Use no spaces.\n" << ColorOff; break; } if (al_match) @@ -1673,15 +1676,32 @@ int dmSetCrt(const std::shared_ptr& player, cmd* cmnd) { "Poison damage/tick", mTarget->getPoisonDamage()); break; } - if(!strcmp(cmnd->str[3], "pty")) { - target->piety.setMax(std::max(1, std::min(cmnd->val[3], MAX_STAT_NUM))); - target->piety.restore(); - player->print("Piety set.\n"); - log_immort(true, player, "%s set %s %s's %s to %d.\n", - player->getCName(), PLYCRT(target), target->getCName(), - "Peity", target->piety.getCur()); - break; + if(!strcmp(cmnd->str[3], "pschance") && mTarget) { + if(cmnd->val[3] < 0 || cmnd->val[3] > 1000) { + *player << "PermCrt spawn chance must be between 0 and 1000."; + return(0); + } + mTarget->setPermSpawnChance((short)cmnd->val[3]); + *player << "PermCrt spawn chance set to " << (short)cmnd->val[3] << "/1000.\n"; + log_immort(true, player, "%s set %s %s's %s to %d/1000.\n", + player->getCName(), PLYCRT(mTarget), mTarget->getCName(), + "PermCrt spawn chance", mTarget->getPermSpawnChance()); + if (!mTarget->flagIsSet(M_PERMANENT_MONSTER)) { + *player << "NOTE: PermCrt spawn chance only works on permed mobs.\n"; + *player << "Please remember to use *perm on " << mTarget << ".\n"; + } + break; + + } + if(!strcmp(cmnd->str[3], "pty") || !strcmp(cmnd->str[3], "pie")) { + target->piety.setMax(std::max(1, std::min(cmnd->val[3], MAX_STAT_NUM))); + target->piety.restore(); + player->print("Piety set.\n"); + log_immort(true, player, "%s set %s %s's %s to %d.\n", + player->getCName(), PLYCRT(target), target->getCName(), + "Peity", target->piety.getCur()); + break; } /* @@ -2983,14 +3003,14 @@ int dmAlignment(const std::shared_ptr& player, cmd* cmnd) { else if (alignName == "royalblue") creature->setAlignment((short)(creature->isPlayer()?((P_TOP_ROYALBLUE+P_BOTTOM_ROYALBLUE)/2):((M_TOP_ROYALBLUE+M_BOTTOM_ROYALBLUE)/2))); else { - *player << ColorOn << "^yInvalid alignment: '" << alignName << "'\nEnter in alignment name or a number. i.e. name: bloodred, red, royalblue, etc.. or number (-1000 to 1000)\nNOTE: 'royal blue' or 'blood red' will not work. Use no spaces.\n" << ColorOff; + *player << ColorOn << "^yInvalid alignment: '" << alignName << "'\nEnter in alignment name or a number. i.e. name: bloodred, red, royalblue, etc.. or number (" << MIN_ALIGN << " to " << MAX_ALIGN << ")\nNOTE: 'royal blue' or 'blood red' will not work. Use no spaces.\n" << ColorOff; return(0); } *player << ColorOn << setf(CAP) << creature << "'s alignment value is now set to median " << creature->alignColor() << creature->alignString() << "^x. (" << creature->getAlignment() << ") [" << (creature->isPlayer()?"Player":"Monster") << "]\n" << ColorOff; break; } alignValue = (short)cmnd->val[2]; - alignValue = std::max(-1000,std::min(1000,alignValue)); + alignValue = std::max(MIN_ALIGN,std::min(MAX_ALIGN,alignValue)); creature->setAlignment((short)alignValue); diff --git a/staff/dmobj.cpp b/staff/dmobj.cpp index d19908b6..c465a04f 100644 --- a/staff/dmobj.cpp +++ b/staff/dmobj.cpp @@ -68,6 +68,7 @@ #include "xml.hpp" // for loadObject #include "toNum.hpp" #include "join.hpp" +#include "color.hpp" //********************************************************************* // dmCreateObj @@ -117,6 +118,7 @@ std::string Object::statObj(int statFlags) { std::string str = ""; std::string objName = getName(); std::string objPlural = plural; + std::string castChanceString; boost::replace_all(objName, "^", "^^"); boost::replace_all(objPlural, "^", "^^"); @@ -215,11 +217,17 @@ std::string Object::statObj(int statFlags) { if(minStrength > 0) objStr << "^WMinimum " << minStrength << " strength required to use.^x\n"; + if(permSpawnChance > 0) + objStr << "^cPerm spawn chance: " << permSpawnChance << "/1000^x\n"; + objStr << "Type: " << getTypeName() << " "; if(type == ObjectType::WEAPON) { - if(magicpower > 0 && magicpower < MAXSPELL && flagIsSet(O_WEAPON_CASTS)) - objStr << "Casts: " << magicpower << "(" << get_spell_name(magicpower - 1) << ")\n"; + if(magicpower > 0 && magicpower < MAXSPELL && flagIsSet(O_WEAPON_CASTS)) { + if(castChance>0) + castChanceString = " - CastChance: " + std::to_string(castChance) + "/1000"; + objStr << "^yCasts: " << magicpower << " (" << get_spell_name(magicpower - 1) << (castChance>0?castChanceString:"") << ")^x\n"; + } } else { switch (type) { case ObjectType::ARMOR: @@ -266,6 +274,8 @@ std::string Object::statObj(int statFlags) { if(magicpower > 0 && magicpower < gConfig->getMaxSong()) objStr << "Song #" << magicpower << "(Song of " << get_song_name(magicpower-1) << ")\n"; break; + default: + break; } } @@ -511,6 +521,8 @@ int dmSetObj(const std::shared_ptr& player, cmd* cmnd) { !strcmp(cmnd->str[3], "material") || !strcmp(cmnd->str[3], "effect") || !strcmp(cmnd->str[3], "eff") || + !strcmp(cmnd->str[3], "efa") || + !strcmp(cmnd->str[3], "efr") || !strcmp(cmnd->str[3], "wear") ) ) ) { @@ -710,8 +722,8 @@ int dmSetObj(const std::shared_ptr& player, cmd* cmnd) { } } else if(flags[1] == 'h') { // Charges - if(num > 100000 || num < 0) { - player->print("How about a realistic number of charges.\n"); + if(num > 1000 || num < 0) { + player->print("Charges must be between 0 and 1000.\n"); return(PROMPT); } @@ -733,9 +745,31 @@ int dmSetObj(const std::shared_ptr& player, cmd* cmnd) { setType = "Charges (Cur)"; } result = num; + } else if(flags[1] == 'c') { + if(object->getType() != ObjectType::WEAPON || !object->flagIsSet(O_WEAPON_CASTS) || + !object->getMagicpower() || !object->getChargesCur() || !object->getChargesMax()) { + + *player << ColorOn << "^yFor weapon cast chance to work, the object:\n"; + *player << "1) Must be of object type weapon. " << (object->getType() == ObjectType::WEAPON?"(Valid)":"^R<--^y") << "\n"; + *player << "2) Must have oflag " << (O_WEAPON_CASTS+1) << " set. " << (object->flagIsSet(O_WEAPON_CASTS)?"(Valid)":"^R<--^y") << "\n"; + *player << "3) Must have a magic power (spell) set. " << (object->getMagicpower()?"(Valid)":"^R<--^y") << "\n"; + *player << "4) Must have current charges set. " << (object->getChargesCur()?"(Valid)":"^R<--^y") << "\n"; + *player << "5) Must have max charges set, and max charges >= current charges. " + << ((object->getChargesMax() && (object->getChargesMax()>=object->getChargesCur()))?"(Valid)":"^R<--^y") << "\n"; + *player << "^x\n" << ColorOff; + + return(0); + } + else if(cmnd->val[3] < 0 || cmnd->val[3] > 1000) { + *player << ColorOn << "^yWeapon cast chance must be between 1 and 1000 (out of 1000).^x\n" << ColorOff; + return(0); + } + + object->setCastChance((short)cmnd->val[3]); + *player << "Weapon cast chance set to: " << cmnd->val[3] << "/1000.\n"; } else { - return(setWhich(player, "coin cost, compass, ch(arges), ch(arges)m(ax), ch(arges)a(ll)")); + return(setWhich(player, "cast chance(cc), coin cost(coi), compass(com), charges(ch), charges max(chm), charges all(cha)")); } break; @@ -793,8 +827,19 @@ int dmSetObj(const std::shared_ptr& player, cmd* cmnd) { break; case 'f': { + + bool addToEffList=false, remFromEffList=false; + + if(flags[2] == 'a') + addToEffList=true; + else if (flags[2] == 'r') + remFromEffList=true; + if(cmnd->num < 5) { - player->print("Set what effect to what?\n"); + if(addToEffList || remFromEffList) + *player << ColorOn << (addToEffList?"Add":"Remove") << " what effect " << (addToEffList?"to ":"from ") << stripColor(objname) << "'s effect list?\n" << ColorOff; + else + *player << ColorOn << "Set " << stripColor(objname) << "'s bestowed effect to what?\n" << ColorOff; return(0); } @@ -803,34 +848,55 @@ int dmSetObj(const std::shared_ptr& player, cmd* cmnd) { std::string txt = getFullstrText(cmnd->fullstr, 5); if(!txt.empty()) - duration = toNum(txt); + duration = (long)std::stoi(txt); txt = getFullstrText(cmnd->fullstr, 6); if(!txt.empty()) - strength = toNum(txt); + strength = std::stoi(txt); if(duration > EFFECT_MAX_DURATION || duration < -1) { - player->print("Duration must be between -1 and %d.\n", EFFECT_MAX_DURATION); + *player << "Duration must be either -1 (permanent), or up to " << EFFECT_MAX_DURATION << " seconds.\n"; return(0); } if(strength < 0 || strength > EFFECT_MAX_STRENGTH) { - player->print("Strength must be between 0 and %d.\n", EFFECT_MAX_STRENGTH); + *player << "Strength must be between 0 and " << EFFECT_MAX_STRENGTH << ".\n"; return(0); } std::string effectStr = cmnd->str[4]; - if(duration == 0) { - player->print("Effect '%s' removed.\n", object->getEffect().c_str()); - object->clearEffect(); - log_immort(2, player, "%s cleared %s's effect.\n", - player->getCName(), objname); - } else { + if(duration == 0 || remFromEffList) { + if(remFromEffList && !object->isEffected(effectStr)) { + *player << ColorOn << "Effect '^W" << effectStr << "^x' not present or not found on " << stripColor(objname) << ".\n" << ColorOff; + return(0); + } + if(remFromEffList) { + *player << ColorOn << "Effect '^W" << effectStr << "^x' removed from " << stripColor(objname) << "'s effect list.\n" << ColorOff; + object->removeEffect(effectStr); + log_immort(2, player, "%s removed effect '%s' from %s's effect list.\n",player->getCName(), effectStr.c_str(), objname); + } + else { + *player << ColorOn << "Bestowed effect '^W" << object->getEffect() << "^x' removed from " << stripColor(objname) << ".\n" << ColorOff; + log_immort(2, player, "%s cleared %s's bestowed effect (%s).\n",player->getCName(), objname, object->getEffect().c_str()); + object->clearEffect(); + } + + } else if (addToEffList) { + object->addEffect(effectStr,duration,strength); + if(!object->isEffected(effectStr)) { + *player << ColorOn << "The efffect '^W" << effectStr << "^x' is not a valid effect to be added to an object's effect list.\n" << ColorOff; + return(0); + } + *player << ColorOn << "Effect '^W" << effectStr << "^x' added to " << stripColor(objname) << "'s effect list. (Duration: " << duration << " Strength: " << strength << ")\n" << ColorOff; + log_immort(2, player, "%s added effect '%s' to %s's effect list.\n", player->getCName(), effectStr.c_str(), objname); + + } + else { object->setEffect(effectStr); object->setEffectDuration(duration); object->setEffectStrength(strength); - player->print("Effect '%s' added with duration %d and strength %d.\n", effectStr.c_str(), duration, strength); - log_immort(2, player, "%s set %s's effect to %s with duration %d and strength %d.\n", + *player << ColorOn << "Bestowed effect '^W" << effectStr << "^x' added with duration " << duration << ", strength " << strength << ".\n" << ColorOff; + log_immort(2, player, "%s set %s's bestowed effect to '%s' with duration %d, strength %d.\n", player->getCName(), objname, effectStr.c_str(), duration, strength); } } @@ -1030,7 +1096,23 @@ int dmSetObj(const std::shared_ptr& player, cmd* cmnd) { return(setWhich(player, "objects")); } break; - + case 'p': + if(!strcmp(cmnd->str[3], "pschance")) { + if(cmnd->val[3] < 0 || cmnd->val[3] > 1000) { + *player << "PermObj spawn chance must be between 0 and 1000."; + return(0); + } + object->setPermSpawnChance((short)cmnd->val[3]); + *player << "PermObj spawn chance set to " << (short)cmnd->val[3] << "/1000.\n"; + log_immort(true, player, "%s set object %s's %s to %d/1000.\n", + player->getCName(), object->getCName(), + "PermObj spawn chance", object->getPermSpawnChance()); + if (!object->flagIsSet(O_PERM_ITEM)) { + *player << "NOTE: PermObj spawn chance only works on permed objects.\n"; + *player << ColorOn << "Please remember to use *perm on " << object << ".\n" << ColorOff; + } + } + break; case 'q': if(flags[1] == 'u' && flags[2] == 'a') { if(object->getType() == ObjectType::ARMOR || object->getType() == ObjectType::WEAPON) { diff --git a/staff/dmply.cpp b/staff/dmply.cpp index 4261d5ab..e0ddcecc 100644 --- a/staff/dmply.cpp +++ b/staff/dmply.cpp @@ -2650,28 +2650,30 @@ int dmChangeStats(const std::shared_ptr& player, cmd* cmnd) { if(cmnd->num < 2) { - player->print("Allow whom to change their stats?\n"); + *player << "Allow whom to change their stats?\n"; return(0); } cmnd->str[1][0]=up(cmnd->str[1][0]); target = gServer->findPlayer(cmnd->str[1]); if(!target) { - player->print("Player not online.\n"); + *player << "Player not online.\n"; return(0); } if(target->flagIsSet(P_DM_INVIS) && !isDm(player)) { - player->print("Player not online.\n"); + *player << "Player not online.\n"; return(0); } if(!target->flagIsSet(P_CAN_CHANGE_STATS)) { target->setFlag(P_CAN_CHANGE_STATS); - player->print("%s can now choose new stats.\n", target->getCName()); + *player << target->getCName() << " can now choose new stats with changestats.\n"; + *target << ColorOn << "^yYou may now use the changestats command.^x\n" << ColorOff; } else { target->clearFlag(P_CAN_CHANGE_STATS); - player->print("%s can no longer choose new stats.\n", target->getCName()); + *player << target->getCName() << " can no longer choose new stats with changestats.\n"; + *target << ColorOn << "^yYou may no longer use the changestats command.^x\n" << ColorOff; } log_immort(false, player, "%s set %s to choose new stats\n", player->getCName(), target->getCName()); diff --git a/staff/dmroom.cpp b/staff/dmroom.cpp index 2047edb2..b6d917bc 100644 --- a/staff/dmroom.cpp +++ b/staff/dmroom.cpp @@ -801,6 +801,8 @@ int stat_rom(const std::shared_ptr& player, const std::shared_ptr monster=nullptr; std::shared_ptr object=nullptr; std::shared_ptr shop=nullptr; + double mSpawnChance = 0.0, oSpawnChance = 0.0; + std::string mSpawnChanceString, oSpawnChanceString; time_t t = time(nullptr); if(!player->checkBuilder(room)) @@ -851,8 +853,17 @@ int stat_rom(const std::shared_ptr& player, const std::shared_ptrprintColor("^y%2d) ^x%14s ^y::^x %-30s ^yInterval:^x %-5d ^yTime Until Spawn:^x %-5d", (*it).first+1, - crtm->cr.displayStr("", 'y').c_str(), object ? object->getCName() : "", crtm->interval, std::max(0, crtm->ltime + crtm->interval - t)); + if(object && object->getPermSpawnChance()) { + oSpawnChance = (object->getPermSpawnChance()/1000.0)*100; + std::ostringstream oss; + oss << std::fixed << std::setprecision(2) << oSpawnChance; + + oSpawnChanceString = "Spawn Attempt (" + oss.str() + "%)"; + } + + player->printColor("^y%2d) ^x%14s ^y::^x %-30s ^yInterval:^x %-5d ^yTime Until %s:^x %-5d", (*it).first+1, + crtm->cr.displayStr("", 'y').c_str(), object ? object->getCName() : "", crtm->interval, + ((object && object->getPermSpawnChance())?oSpawnChanceString.c_str():"Spawn"), std::max(0, crtm->ltime + crtm->interval - t)); if(room->flagIsSet(R_SHOP_STORAGE) && object) player->printColor(" ^yCost:^x %s", object->value.str().c_str()); @@ -873,9 +884,17 @@ int stat_rom(const std::shared_ptr& player, const std::shared_ptrpermMonsters.begin(); it != room->permMonsters.end() ; it++) { crtm = &(*it).second; loadMonster((*it).second.cr, monster); + if(monster && monster->getPermSpawnChance()) { + mSpawnChance = (monster->getPermSpawnChance()/1000.0)*100; + std::ostringstream mss; + mss << std::fixed << std::setprecision(2) << mSpawnChance; + + mSpawnChanceString = "Spawn Attempt (" + mss.str() + "%)"; + } - player->printColor("^m%2d) ^x%14s ^m::^x %-30s ^mInterval:^x %d ^yTime until Spawn:^x %-5d\n", (*it).first+1, - crtm->cr.displayStr("", 'm').c_str(), monster ? monster->getCName() : "", crtm->interval, std::max(0, crtm->ltime + crtm->interval - t)); + player->printColor("^m%2d) ^x%14s ^m::^x %-30s ^mInterval:^x %d ^yTime Until %s:^x %-5d\n", (*it).first+1, + crtm->cr.displayStr("", 'm').c_str(), monster ? monster->getCName() : "", crtm->interval, + ((monster && monster->getPermSpawnChance())?mSpawnChanceString.c_str():"Spawn"), std::max(0, crtm->ltime + crtm->interval - t)); if(monster) { monster = nullptr; diff --git a/staff/watchers.cpp b/staff/watchers.cpp index d74a58c7..632f6bd8 100644 --- a/staff/watchers.cpp +++ b/staff/watchers.cpp @@ -119,7 +119,7 @@ int dmWatcherBroad(const std::shared_ptr& admin, cmd* cmnd) { text = getFullstrText(cmnd->fullstr, 1); if(text.empty()) { - admin->print("Broadcast what?\n"); + admin->print("Force a watcher to gossip what?\n"); return(0); } @@ -139,17 +139,17 @@ int dmWatcherBroad(const std::shared_ptr& admin, cmd* cmnd) { } if(!found) { - admin->print("No watchers were found to broadcast your message.\n"); + admin->print("No watchers were found to gossip your message.\n"); return(0); } - broadcast(isDm, "^g*** %s forced %s to broadcast", admin->getCName(), watcher->getCName()); + broadcast(isDm, "^g*** %s forced %s to gossip", admin->getCName(), watcher->getCName()); - text = "broadcast " + text; - strcpy(cmnd->str[0], "broadcast"); + text = "gossip " + text; + strcpy(cmnd->str[0], "gossip"); cmnd->fullstr = text; cmdProcess(watcher, cmnd); - log_immort(true, admin, "%s made %s broadcast \"%s\"\n", admin->getCName(), watcher->getCName(), text.c_str()); + log_immort(true, admin, "%s made %s gossip \"%s\"\n", admin->getCName(), watcher->getCName(), text.c_str()); return(0); } diff --git a/util/misc.cpp b/util/misc.cpp index 97b0c84e..f8e8ec19 100644 --- a/util/misc.cpp +++ b/util/misc.cpp @@ -810,6 +810,8 @@ bool isUseableFilterString(std::shared_ptr searcher, std::string fs, b } + + //********************************************************************* // findTarget //********************************************************************* @@ -1014,3 +1016,22 @@ std::string stripNonDigits(std::string someString) { return(digitString); } + +std::string stripSpaces(std::string someString) { + + someString.erase( + std::remove_if(someString.begin(), someString.end(), [](unsigned char c) { + return std::isspace(c); + }), + someString.end() + ); + return someString; + +} + +std::string toLower(const std::string& str) { + std::string lowerStr = str; + std::transform(lowerStr.begin(), lowerStr.end(), lowerStr.begin(), + [](unsigned char c) { return std::tolower(c); }); + return lowerStr; +} diff --git a/xml/creatures-xml.cpp b/xml/creatures-xml.cpp index 14b1b5a6..21ca6814 100644 --- a/xml/creatures-xml.cpp +++ b/xml/creatures-xml.cpp @@ -56,6 +56,7 @@ #include "stats.hpp" // for Stat #include "structs.hpp" // for daily, saves #include "xml.hpp" // for newStringChild +#include "paths.hpp" // for Post notifications on changes class Object; @@ -256,28 +257,53 @@ int Creature::readFromXml(xmlNodePtr rootNode, bool offline) { setRace(getRace()+10); } + if(getVersion() < "2.62c") { + #define OLD_GREYELF 37 + #define OLD_WILDELF 38 + #define OLD_DUERGAR 40 + + if(getRace() == OLD_GREYELF) + setRace(GREYELF); + if(getRace() == OLD_WILDELF) + setRace(WILDELF); + if(getRace() == OLD_DUERGAR) + setRace(DUERGAR); + + // Lanugage correction + if(getRace() == CAMBION || getRace() == TIEFLING) { + forgetLanguage(LINFERNAL); + learnLanguage(LABYSSAL); + } + + } + + // Here and above - all creatures (including players) + if(isPlayer()) { + if(getVersion() < "2.47b") { pThis->recordLevelInfo(); } - } - if(isPlayer()) { + if(getVersion() < "2.47b") { -#define P_OLD_MISTED 55 // Player is in mist form + #define P_OLD_MISTED 55 // Player is in mist form if(flagIsSet(P_OLD_MISTED)) { addEffect("mist", -1); clearFlag(P_OLD_MISTED); } -#define P_OLD_INCOGNITO 104 // DM/CT is incognito + #define P_OLD_INCOGNITO 104 // DM/CT is incognito if(flagIsSet(P_OLD_INCOGNITO)) { addEffect("incognito", -1); clearFlag(P_OLD_INCOGNITO); } } + if(getVersion() < "2.54h") { -#define P_OLD_NO_AUTO_WEAR 79 // Player won't wear all when they log on + #define P_OLD_NO_AUTO_WEAR 79 // Player won't wear all when they log on clearFlag(P_OLD_NO_AUTO_WEAR); - } + } + + if(getVersion() < "2.47a") { // Update weapon skills for (auto const& [skillId, skill] : skills) { @@ -303,9 +329,9 @@ int Creature::readFromXml(xmlNodePtr rootNode, bool offline) { if(getVersion() < "2.46k" && knowsSkill("endurance")) { remSkill("endurance"); -#define P_RUNNING_OLD 56 + #define P_RUNNING_OLD 56 clearFlag(P_RUNNING_OLD); - } + } if(getVersion() < "2.43" && getClass() != CreatureClass::BERSERKER) { int skill = level; @@ -326,7 +352,6 @@ int Creature::readFromXml(xmlNodePtr rootNode, bool offline) { addSkill("necromancy", skill); addSkill("translocation", skill); addSkill("transmutation", skill); - addSkill("fire", convertProf(cThis, FIRE)); addSkill("water", convertProf(cThis, WATER)); addSkill("earth", convertProf(cThis, EARTH)); @@ -334,6 +359,8 @@ int Creature::readFromXml(xmlNodePtr rootNode, bool offline) { addSkill("cold", convertProf(cThis, COLD)); addSkill("electric", convertProf(cThis, ELEC)); } + + if(getVersion() < "2.45c" && getCastingType() == Divine) { int skill = level; if(isPureCaster() || isHybridCaster() || isStaff()) { @@ -384,78 +411,132 @@ int Creature::readFromXml(xmlNodePtr rootNode, bool offline) { } } - if(isPlayer()) { + + + if(getVersion() < "2.52") { + #define P_NO_LONG_DESCRIPTION_OLD 4 + #define P_NO_SHORT_DESCRIPTION_OLD 5 + clearFlag(P_NO_LONG_DESCRIPTION_OLD); + clearFlag(P_NO_SHORT_DESCRIPTION_OLD); + } + if(getVersion() < "2.52b") { + #define P_CAN_PROXY_OLD 156 + clearFlag(P_CAN_PROXY_OLD); + } + + if(getVersion() < "2.52d") { + #define P_CAN_MUDMAIL_STAFF_OLD 161 + clearFlag(P_CAN_MUDMAIL_STAFF_OLD); + } + + //reset all saves to max of 99 + if(getVersion() < "2.54c") { + for(int a=POI; a<=SPL;a++) + saves[a].chance = std::min(99,saves[a].chance); + } + + if(getVersion() < "2.54f") { + if (level < 15) + daily[DL_TELEP].cur = 1; + else + daily[DL_TELEP].cur = 3; + if (getClass() == CreatureClass::MAGE || getClass() == CreatureClass::LICH) + daily[DL_TELEP].cur = std::max(3,std::min(10, (int)getSkillLevel("translocation")/5)); - if(getVersion() < "2.52") { - #define P_NO_LONG_DESCRIPTION_OLD 4 - #define P_NO_SHORT_DESCRIPTION_OLD 5 - clearFlag(P_NO_LONG_DESCRIPTION_OLD); - clearFlag(P_NO_SHORT_DESCRIPTION_OLD); } - if(getVersion() < "2.52b") { - #define P_CAN_PROXY_OLD 156 - clearFlag(P_CAN_PROXY_OLD); - } - - if(getVersion() < "2.52d") { - #define P_CAN_MUDMAIL_STAFF_OLD 161 - clearFlag(P_CAN_MUDMAIL_STAFF_OLD); - } - - //reset all saves to max of 99 - if(getVersion() < "2.54c") { - for(int a=POI; a<=SPL;a++) - saves[a].chance = std::min(99,saves[a].chance); - } - - if(getVersion() < "2.54f") { - if (level < 15) - daily[DL_TELEP].cur = 1; - else - daily[DL_TELEP].cur = 3; - if (getClass() == CreatureClass::MAGE || getClass() == CreatureClass::LICH) - daily[DL_TELEP].cur = std::max(3,std::min(10, (int)getSkillLevel("translocation")/5)); - - } - if(getVersion() < "2.56c") { - if (getClass() == CreatureClass::PALADIN && deity == LINOTHAN && level >=13) { - addSkill("hands",(level*9)); - } - } + + if(getVersion() < "2.56c") { + if (getClass() == CreatureClass::PALADIN && deity == LINOTHAN && level >=13) { + addSkill("hands",(level*9)); + } + } + + if (getVersion() < "2.59") { + // Free-action was turned into an effect - clean up old flag + #define P_FREE_ACTION_OLD 180 + clearFlag(P_FREE_ACTION_OLD); + // Gore skill added for Minotaurs - give existing Minotaurs 75% of max gained skill + if (getRace() == MINOTAUR) + addSkill("gore",std::max(1,(level*30)/4)); + } + + if (getVersion() < "2.61") { + if (getClass() == CreatureClass::CLERIC && getDeity() == LINOTHAN) + addSkill("parry",std::max(1,(level*10))); + } + + if (getVersion() < "2.61b") { + if (getClass() == CreatureClass::CLERIC && getDeity() == ARAMON && level >= 10 && getAsPlayer()->getSecondClass() == CreatureClass::NONE) + addSkill("unholyword",std::max(1,(level*30)/4)); + } + + //Multi-class clerics (like cleric/assassins) weren't supposed to get unholyword. Ooops....This fixes that. + if(getVersion() < "2.61c") { + if (getClass() == CreatureClass::CLERIC && getAsPlayer()->getSecondClass() != CreatureClass::NONE && knowsSkill("unholyword")) + remSkill("unholyword"); + // Bring Enoch clerics up to snuff with holyword, since they've been suffering with it being broken for so long + if (getClass() == CreatureClass::CLERIC && getAsPlayer()->getSecondClass() == CreatureClass::NONE && knowsSkill("holyword")) { + if (getSkillGained("holyword") < ((level*30)/4)) + setSkill("holyword", ((level*30)/4)); + } + } - if (getVersion() < "2.59") { - // Free-action was turned into an effect - clean up old flag - #define P_FREE_ACTION_OLD 180 - clearFlag(P_FREE_ACTION_OLD); - // Gore skill added for Minotaurs - give existing Minotaurs 75% of max gained skill - if (getRace() == MINOTAUR) - addSkill("gore",std::max(1,(level*30)/4)); + if(getVersion() < "2.63") { + //Added a lot of additional languages for the various races + //Going to just reinit the languages for players + getAsPlayer()->initLanguages(); + //Add transmute and enchant to existing level 10+ Mage/Thief or Lich at level*9 gained + if (level >=10 && (getClass() == CreatureClass::LICH || (getClass() == CreatureClass::MAGE && getAsPlayer()->getSecondClass() == CreatureClass::THIEF))) { + addSkill("transmute",(level*9)); + addSkill("enchant",(level*9)); } - if (getVersion() < "2.61") { - if (getClass() == CreatureClass::CLERIC && getDeity() == LINOTHAN) - addSkill("parry",std::max(1,(level*10))); + + // Smash skill added for existing Ogres and Half-Giants - 75% of max gained for current level + if (getRace() == OGRE || getRace() == HALFGIANT) + addSkill("smash",std::max(1,(level*9)/10)); + + // Existing Paladins receiving kick/slam/bash - 75% of max gained for current level + if (getClass() == CreatureClass::PALADIN) { + addSkill("kick",std::max(1,(level*30)/4)); } - if (getVersion() < "2.61b") { - if (getClass() == CreatureClass::CLERIC && getDeity() == ARAMON && level >= 10 && getAsPlayer()->getSecondClass() == CreatureClass::NONE) - addSkill("unholyword",std::max(1,(level*30)/4)); + // Existing Dknights and paladins get bash at 75% of max gained for current level + if (getClass() == CreatureClass::DEATHKNIGHT || getClass() == CreatureClass::PALADIN) { + addSkill("bash",std::max(1,(level*30)/4)); } - //Multi-class clerics (like cleric/assassins) weren't supposed to get unholyword. Ooops....This fixes that. - if(getVersion() < "2.61c") { - if (getClass() == CreatureClass::CLERIC && getAsPlayer()->getSecondClass() != CreatureClass::NONE && knowsSkill("unholyword")) - remSkill("unholyword"); + //Existing Berserkers, Fighters, Assassins, Rogues, Thieves, and Thief/Mages get slam at 75% of max gained per level + if (getClass() == CreatureClass::ASSASSIN || getClass() == CreatureClass::THIEF || getClass() == CreatureClass::ROGUE || + getClass() == CreatureClass::FIGHTER || getClass() == CreatureClass::BERSERKER || getClass() == CreatureClass::PALADIN) { + addSkill("slam",std::max(1,(level*30)/4)); + } - // Bring Enoch clerics up to snuff with holyword, since they've been suffering with it being broken for so long - if (getClass() == CreatureClass::CLERIC && getAsPlayer()->getSecondClass() == CreatureClass::NONE && knowsSkill("holyword")) { - if (getSkillGained("holyword") < ((level*30)/4)) - setSkill("holyword", ((level*30)/4)); - } + //Existing clerics of Ares and Linothan get slam at 75% max gained for level + if (getClass() == CreatureClass::CLERIC && (getDeity() == ARES || getDeity() == LINOTHAN)) { + addSkill("slam",std::max(1,(level*30)/4)); } - + + const auto notification = (Path::Post / getName()).replace_extension("notify"); + std::string msg = "^yThe racial stat adjustments for race ^YOgre^y and ^WTroll^x were recently changed.\n" + "Since your ogre or troll is currently one of their allowed classes, the 'changestats'\n" + "command is now active for you.\n" + "Please see: ^Rhelp changelog^y, ^Rhelp ogre^y, ^Rhelp troll^x, ^Rhelp changestats^y.^x\n"; + + + if((getRace() == OGRE && (getClass() == CreatureClass::BERSERKER || getClass()==CreatureClass::DEATHKNIGHT || getClass()==CreatureClass::FIGHTER)) || + (getRace() == TROLL && (getClass() == CreatureClass::ASSASSIN || getClass()==CreatureClass::BERSERKER || + getClass()==CreatureClass::DEATHKNIGHT || getClass()==CreatureClass::FIGHTER || getClass()==CreatureClass::MONK))) { + if (!fs::exists(notification)) + sendSystemNotice(getName(), msg); + setFlag(P_CAN_CHANGE_STATS); + setFlag(P_UNREAD_MAIL); + + } + } - } + + } // end if(isPlayer()) setVersion(); diff --git a/xml/monsters-xml.cpp b/xml/monsters-xml.cpp index 8ebe0e03..08e5e7eb 100644 --- a/xml/monsters-xml.cpp +++ b/xml/monsters-xml.cpp @@ -173,6 +173,7 @@ void Monster::readXml(xmlNodePtr curNode, bool offline) { } else if(NODE_NAME(curNode, "TradeTalk")) xml::copyToCString(ttalk, curNode); else if(NODE_NAME(curNode, "NumWander")) setNumWander(xml::toNum(curNode)); + else if(NODE_NAME(curNode, "PermSpawnChance")) setPermSpawnChance(xml::toNum(curNode)); else if(NODE_NAME(curNode, "MagicResistance")) setMagicResistance(xml::toNum(curNode)); else if(NODE_NAME(curNode, "MobTrade")) setMobTrade(xml::toNum(curNode)); else if(NODE_NAME(curNode, "AssistMobs")) { @@ -294,6 +295,7 @@ void Monster::saveXml(xmlNodePtr curNode) const { xml::saveNonNullString(curNode, "TradeTalk", ttalk); xml::saveNonZeroNum(curNode, "NumWander", numwander); + xml::saveNonZeroNum(curNode, "PermSpawnChance", permSpawnChance); xml::saveNonZeroNum(curNode, "MagicResistance", magicResistance); xml::saveNonZeroNum(curNode, "DefenseSkill", defenseSkill); xml::saveNonZeroNum(curNode, "AttackPower", attackPower); diff --git a/xml/objects-xml.cpp b/xml/objects-xml.cpp index 18dbb936..e1e2dabb 100644 --- a/xml/objects-xml.cpp +++ b/xml/objects-xml.cpp @@ -193,6 +193,7 @@ int Object::readFromXml(xmlNodePtr rootNode, std::list *idList, boo else if(NODE_NAME(curNode, "Armor")) xml::copyToNum(armor, curNode); else if(NODE_NAME(curNode, "WearFlag")) xml::copyToNum(wearflag, curNode); else if(NODE_NAME(curNode, "MagicPower")) xml::copyToNum(magicpower, curNode); + else if(NODE_NAME(curNode, "CastChance")) xml::copyToNum(castChance, curNode); else if(NODE_NAME(curNode, "Effect")) xml::copyToString(effect, curNode); else if(NODE_NAME(curNode, "EffectDuration")) xml::copyToNum(effectDuration, curNode); else if(NODE_NAME(curNode, "EffectStrength")) xml::copyToNum(effectStrength, curNode); @@ -215,6 +216,7 @@ int Object::readFromXml(xmlNodePtr rootNode, std::list *idList, boo else if(NODE_NAME(curNode, "KeyVal")) xml::copyToNum(keyVal, curNode); else if(NODE_NAME(curNode, "Material")) material = (Material)xml::toNum(curNode); else if(NODE_NAME(curNode, "MinStrength")) xml::copyToNum(minStrength, curNode); + else if(NODE_NAME(curNode, "PermSpawnChance")) xml::copyToNum(permSpawnChance, curNode); else if(NODE_NAME(curNode, "NumAttacks")) xml::copyToNum(numAttacks, curNode); else if(NODE_NAME(curNode, "Delay")) xml::copyToNum(delay, curNode); else if(NODE_NAME(curNode, "Extra")) xml::copyToNum(extra, curNode); @@ -607,6 +609,7 @@ int Object::saveToXml(xmlNodePtr rootNode, int permOnly, LoadType saveType, int xml::saveNonZeroNum(rootNode, "WearFlag", wearflag); xml::saveNonZeroNum(rootNode, "MagicPower", magicpower); + xml::saveNonZeroNum(rootNode, "CastChance", castChance); xml::saveNonZeroNum(rootNode, "Level", level); xml::saveNonZeroNum(rootNode, "Quality", quality); @@ -626,6 +629,8 @@ int Object::saveToXml(xmlNodePtr rootNode, int permOnly, LoadType saveType, int xml::saveNonZeroNum(rootNode, "KeyVal", keyVal); xml::saveNonZeroNum(rootNode, "Material", (int)material); xml::saveNonZeroNum(rootNode, "MinStrength", minStrength); + xml::saveNonZeroNum(rootNode, "PermSpawnChance", permSpawnChance); + saveCatRefArray(rootNode, "InBag", "Obj", in_bag, 3); diff --git a/xml/players-xml.cpp b/xml/players-xml.cpp index fcf8bc14..c59b9a40 100644 --- a/xml/players-xml.cpp +++ b/xml/players-xml.cpp @@ -551,6 +551,7 @@ void PlayerClass::load(xmlNodePtr rootNode) { } else if(NODE_NAME(curNode, "UnarmedWeaponSkill")) xml::copyToString(unarmedWeaponSkill, curNode); else if(NODE_NAME(curNode, "NumProfs")) xml::copyToNum(numProf, curNode); + else if(NODE_NAME(curNode, "XPAdjustment")) xml::copyToNum(xpAdjust, curNode); else if(NODE_NAME(curNode, "NeedsDeity")) { int i=0; xml::copyToNum(i, curNode);