You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3495 lines
101 KiB

/**
* @file player.cpp
*
* Implementation of player functionality, leveling, actions, creation, loading, etc.
*/
#include <algorithm>
#include <cmath>
#include <cstdint>
#ifdef USE_SDL3
#include <SDL3/SDL_events.h>
#include <SDL3/SDL_timer.h>
#else
#include <SDL.h>
#endif
#include <fmt/core.h>
#include "control.h"
#include "controls/control_mode.hpp"
#include "controls/plrctrls.h"
#include "cursor.h"
#include "dead.h"
#ifdef _DEBUG
#include "debug.h"
#endif
#include "engine/backbuffer_state.hpp"
#include "engine/load_cl2.hpp"
#include "engine/load_file.hpp"
#include "engine/points_in_rectangle_range.hpp"
#include "engine/random.hpp"
#include "engine/render/clx_render.hpp"
#include "engine/trn.hpp"
#include "engine/world_tile.hpp"
#include "game_mode.hpp"
#include "gamemenu.h"
#include "headless_mode.hpp"
#include "help.h"
#include "inv_iterators.hpp"
#include "levels/tile_properties.hpp"
#include "levels/trigs.h"
#include "lighting.h"
#include "loadsave.h"
#include "minitext.h"
#include "missiles.h"
#include "monster.h"
#include "nthread.h"
#include "objects.h"
#include "options.h"
#include "player.h"
#include "qol/autopickup.h"
#include "qol/floatingnumbers.h"
4 years ago
#include "qol/stash.h"
#include "spells.h"
#include "stores.h"
#include "towners.h"
#include "utils/is_of.hpp"
#include "utils/language.h"
#include "utils/log.hpp"
#include "utils/str_cat.hpp"
#include "utils/utf8.hpp"
namespace devilution {
uint8_t MyPlayerId;
Player *MyPlayer;
std::vector<Player> Players;
Player *InspectPlayer;
bool MyPlayerIsDead;
namespace {
struct DirectionSettings {
Direction dir;
PLR_MODE walkMode;
};
void UpdatePlayerLightOffset(Player &player)
{
if (player.lightId == NO_LIGHT)
return;
const WorldTileDisplacement offset = player.position.CalculateWalkingOffset(player._pdir, player.AnimInfo);
ChangeLightOffset(player.lightId, offset.screenToLight());
}
void WalkInDirection(Player &player, const DirectionSettings &walkParams)
{
player.occupyTile(player.position.future, true);
player.position.temp = player.position.tile + walkParams.dir;
}
constexpr std::array<const DirectionSettings, 8> WalkSettings { {
// clang-format off
{ Direction::South, PM_WALK_SOUTHWARDS },
{ Direction::SouthWest, PM_WALK_SOUTHWARDS },
{ Direction::West, PM_WALK_SIDEWAYS },
{ Direction::NorthWest, PM_WALK_NORTHWARDS },
{ Direction::North, PM_WALK_NORTHWARDS },
{ Direction::NorthEast, PM_WALK_NORTHWARDS },
{ Direction::East, PM_WALK_SIDEWAYS },
{ Direction::SouthEast, PM_WALK_SOUTHWARDS }
// clang-format on
} };
bool PlrDirOK(const Player &player, Direction dir)
{
const Point position = player.position.tile;
const Point futurePosition = position + dir;
if (futurePosition.x < 0 || !PosOkPlayer(player, futurePosition)) {
return false;
}
if (dir == Direction::East) {
return !IsTileSolid(position + Direction::SouthEast);
}
if (dir == Direction::West) {
return !IsTileSolid(position + Direction::SouthWest);
}
return true;
}
void HandleWalkMode(Player &player, Direction dir)
{
const auto &dirModeParams = WalkSettings[static_cast<size_t>(dir)];
SetPlayerOld(player);
if (!PlrDirOK(player, dir)) {
return;
}
player._pdir = dir;
// The player's tile position after finishing this movement action
player.position.future = player.position.tile + dirModeParams.dir;
WalkInDirection(player, dirModeParams);
player.tempDirection = dirModeParams.dir;
player._pmode = dirModeParams.walkMode;
}
void StartWalkAnimation(Player &player, Direction dir, bool pmWillBeCalled)
{
int8_t skippedFrames = -2;
if (leveltype == DTYPE_TOWN && sgGameInitInfo.bRunInTown != 0)
skippedFrames = 2;
if (pmWillBeCalled)
skippedFrames += 1;
NewPlrAnim(player, player_graphic::Walk, dir, AnimationDistributionFlags::ProcessAnimationPending, skippedFrames);
}
/**
* @brief Start moving a player to a new tile
*/
void StartWalk(Player &player, Direction dir, bool pmWillBeCalled)
{
if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) {
SyncPlrKill(player, DeathReason::Unknown);
return;
}
StartWalkAnimation(player, dir, pmWillBeCalled);
HandleWalkMode(player, dir);
}
void ClearStateVariables(Player &player)
{
player.position.temp = { 0, 0 };
player.tempDirection = Direction::South;
player.queuedSpell.spellLevel = 0;
}
void StartAttack(Player &player, Direction d, bool includesFirstFrame)
{
if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) {
SyncPlrKill(player, DeathReason::Unknown);
return;
}
int8_t skippedAnimationFrames = 0;
const auto flags = player._pIFlags;
// If the first frame is not included in vanilla, the skip logic for the first frame will not be executed.
// This will result in a different and slower attack speed.
if (HasAnyOf(flags, ItemSpecialEffect::FastestAttack)) {
// If the fastest attack logic is trigger frames in vanilla two frames are skipped, so missing the first frame reduces the skip logic by two frames.
skippedAnimationFrames = includesFirstFrame ? 4 : 2;
} else if (HasAnyOf(flags, ItemSpecialEffect::FasterAttack)) {
skippedAnimationFrames = includesFirstFrame ? 3 : 2;
} else if (HasAnyOf(flags, ItemSpecialEffect::FastAttack)) {
skippedAnimationFrames = includesFirstFrame ? 2 : 1;
} else if (HasAnyOf(flags, ItemSpecialEffect::QuickAttack)) {
skippedAnimationFrames = includesFirstFrame ? 1 : 0;
}
auto animationFlags = AnimationDistributionFlags::ProcessAnimationPending;
if (player._pmode == PM_ATTACK)
animationFlags = static_cast<AnimationDistributionFlags>(animationFlags | AnimationDistributionFlags::RepeatedAction);
NewPlrAnim(player, player_graphic::Attack, d, animationFlags, skippedAnimationFrames, player._pAFNum);
player._pmode = PM_ATTACK;
FixPlayerLocation(player, d);
SetPlayerOld(player);
}
void StartRangeAttack(Player &player, Direction d, WorldTileCoord cx, WorldTileCoord cy, bool includesFirstFrame)
{
if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) {
SyncPlrKill(player, DeathReason::Unknown);
return;
}
int8_t skippedAnimationFrames = 0;
const auto flags = player._pIFlags;
if (!gbIsHellfire) {
if (includesFirstFrame && HasAnyOf(flags, ItemSpecialEffect::QuickAttack | ItemSpecialEffect::FastAttack)) {
skippedAnimationFrames += 1;
}
if (HasAnyOf(flags, ItemSpecialEffect::FastAttack)) {
skippedAnimationFrames += 1;
}
}
auto animationFlags = AnimationDistributionFlags::ProcessAnimationPending;
if (player._pmode == PM_RATTACK)
animationFlags = static_cast<AnimationDistributionFlags>(animationFlags | AnimationDistributionFlags::RepeatedAction);
NewPlrAnim(player, player_graphic::Attack, d, animationFlags, skippedAnimationFrames, player._pAFNum);
player._pmode = PM_RATTACK;
FixPlayerLocation(player, d);
SetPlayerOld(player);
player.position.temp = WorldTilePosition { cx, cy };
}
player_graphic GetPlayerGraphicForSpell(SpellID spellId)
{
switch (GetSpellData(spellId).type()) {
case MagicType::Fire:
return player_graphic::Fire;
case MagicType::Lightning:
return player_graphic::Lightning;
default:
return player_graphic::Magic;
}
}
void StartSpell(Player &player, Direction d, WorldTileCoord cx, WorldTileCoord cy)
{
if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) {
SyncPlrKill(player, DeathReason::Unknown);
return;
}
// Checks conditions for spell again, because initial check was done when spell was queued and the parameters could be changed meanwhile
bool isValid = false;
switch (player.queuedSpell.spellType) {
case SpellType::Skill:
case SpellType::Spell:
isValid = CheckSpell(player, player.queuedSpell.spellId, player.queuedSpell.spellType, true) == SpellCheckResult::Success;
break;
case SpellType::Scroll:
isValid = CanUseScroll(player, player.queuedSpell.spellId);
break;
case SpellType::Charges:
isValid = CanUseStaff(player, player.queuedSpell.spellId);
break;
default:
break;
}
if (!isValid)
return;
auto animationFlags = AnimationDistributionFlags::ProcessAnimationPending;
if (player._pmode == PM_SPELL)
animationFlags = static_cast<AnimationDistributionFlags>(animationFlags | AnimationDistributionFlags::RepeatedAction);
NewPlrAnim(player, GetPlayerGraphicForSpell(player.queuedSpell.spellId), d, animationFlags, 0, player._pSFNum);
PlaySfxLoc(GetSpellData(player.queuedSpell.spellId).sSFX, player.position.tile);
player._pmode = PM_SPELL;
FixPlayerLocation(player, d);
SetPlayerOld(player);
player.position.temp = WorldTilePosition { cx, cy };
player.queuedSpell.spellLevel = player.GetSpellLevel(player.queuedSpell.spellId);
player.executedSpell = player.queuedSpell;
}
void RespawnDeadItem(Item &&itm, Point target)
{
if (ActiveItemCount >= MAXITEMS)
return;
const int ii = AllocateItem();
Item &item = Items[ii];
dItem[target.x][target.y] = ii + 1;
item = itm;
item.position = target;
RespawnItem(item, true);
NetSendCmdPItem(false, CMD_SPAWNITEM, target, item);
}
void DeadItem(Player &player, Item &&item, Displacement direction)
{
if (item.isEmpty())
return;
const Point playerTile = player.position.tile;
if (direction != Displacement { 0, 0 }) {
const Point target = playerTile + direction;
if (ItemSpaceOk(target)) {
RespawnDeadItem(std::move(item), target);
return;
}
}
std::optional<Point> dropPoint = FindClosestValidPosition(ItemSpaceOk, playerTile, 1, 50);
if (dropPoint) {
RespawnDeadItem(std::move(item), *dropPoint);
}
}
int DropGold(Player &player, int amount, bool skipFullStacks)
{
for (int i = 0; i < player._pNumInv && amount > 0; i++) {
Item &item = player.InvList[i];
if (item._itype != ItemType::Gold || (skipFullStacks && item._ivalue == MaxGold))
continue;
if (amount < item._ivalue) {
Item goldItem;
MakeGoldStack(goldItem, amount);
DeadItem(player, std::move(goldItem), { 0, 0 });
item._ivalue -= amount;
return 0;
}
amount -= item._ivalue;
DeadItem(player, std::move(item), { 0, 0 });
player.RemoveInvItem(i);
i = -1;
}
return amount;
}
void DropHalfPlayersGold(Player &player)
{
const int remainingGold = DropGold(player, player._pGold / 2, true);
if (remainingGold > 0) {
DropGold(player, remainingGold, false);
}
player._pGold /= 2;
}
void InitLevelChange(Player &player)
{
const Player &myPlayer = *MyPlayer;
RemoveEnemyReferences(player);
RemovePlrMissiles(player);
player.pManaShield = false;
player.wReflections = 0;
if (&player != MyPlayer) {
// share info about your manashield when another player joins the level
if (myPlayer.pManaShield)
NetSendCmd(true, CMD_SETSHIELD);
// share info about your reflect charges when another player joins the level
NetSendCmdParam1(true, CMD_SETREFLECT, myPlayer.wReflections);
} else if (qtextflag) {
qtextflag = false;
stream_stop();
}
FixPlrWalkTags(player);
SetPlayerOld(player);
if (&player == MyPlayer) {
player.occupyTile(player.position.tile, false);
} else {
player._pLvlVisited[player.plrlevel] = true;
}
ClrPlrPath(player);
player.destAction = ACTION_NONE;
player._pLvlChanging = true;
if (&player == MyPlayer) {
player.pLvlLoad = 10;
}
}
/**
* @brief Continue movement towards new tile
*/
bool DoWalk(Player &player)
{
// Play walking sound effect on certain animation frames
if (*GetOptions().Audio.walkingSound && (leveltype != DTYPE_TOWN || sgGameInitInfo.bRunInTown == 0)) {
if (player.AnimInfo.currentFrame == 0
|| player.AnimInfo.currentFrame == 4) {
PlaySfxLoc(SfxID::Walk, player.position.tile);
}
}
if (!player.AnimInfo.isLastFrame()) {
// We didn't reach new tile so update player's "sub-tile" position
UpdatePlayerLightOffset(player);
return false;
}
// We reached the new tile -> update the player's tile position
dPlayer[player.position.tile.x][player.position.tile.y] = 0;
player.position.tile = player.position.temp;
// dPlayer is set here for backwards compatibility; without it, the player would be invisible if loaded from a vanilla save.
player.occupyTile(player.position.tile, false);
// Update the coordinates for lighting and vision entries for the player
if (leveltype != DTYPE_TOWN) {
ChangeLightXY(player.lightId, player.position.tile);
ChangeVisionXY(player.getId(), player.position.tile);
}
StartStand(player, player.tempDirection);
ClearStateVariables(player);
// Reset the "sub-tile" position of the player's light entry to 0
if (leveltype != DTYPE_TOWN) {
ChangeLightOffset(player.lightId, { 0, 0 });
}
AutoPickup(player);
return true;
}
bool WeaponDecay(Player &player, int ii)
{
if (!player.InvBody[ii].isEmpty() && player.InvBody[ii]._iClass == ICLASS_WEAPON && HasAnyOf(player.InvBody[ii]._iDamAcFlags, ItemSpecialEffectHf::Decay)) {
player.InvBody[ii]._iPLDam -= 5;
if (player.InvBody[ii]._iPLDam <= -100) {
RemoveEquipment(player, static_cast<inv_body_loc>(ii), true);
CalcPlrInv(player, true);
return true;
}
CalcPlrInv(player, true);
}
return false;
}
bool DamageWeapon(Player &player, unsigned damageFrequency)
{
if (&player != MyPlayer) {
return false;
}
if (WeaponDecay(player, INVLOC_HAND_LEFT))
return true;
if (WeaponDecay(player, INVLOC_HAND_RIGHT))
return true;
if (!FlipCoin(damageFrequency)) {
return false;
}
if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() && player.InvBody[INVLOC_HAND_LEFT]._iClass == ICLASS_WEAPON) {
if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == DUR_INDESTRUCTIBLE) {
return false;
}
player.InvBody[INVLOC_HAND_LEFT]._iDurability--;
if (player.InvBody[INVLOC_HAND_LEFT]._iDurability <= 0) {
RemoveEquipment(player, INVLOC_HAND_LEFT, true);
CalcPlrInv(player, true);
return true;
}
}
if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvBody[INVLOC_HAND_RIGHT]._iClass == ICLASS_WEAPON) {
if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == DUR_INDESTRUCTIBLE) {
return false;
}
player.InvBody[INVLOC_HAND_RIGHT]._iDurability--;
if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == 0) {
RemoveEquipment(player, INVLOC_HAND_RIGHT, true);
CalcPlrInv(player, true);
return true;
}
}
if (player.InvBody[INVLOC_HAND_LEFT].isEmpty() && player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) {
if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == DUR_INDESTRUCTIBLE) {
return false;
}
player.InvBody[INVLOC_HAND_RIGHT]._iDurability--;
if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == 0) {
RemoveEquipment(player, INVLOC_HAND_RIGHT, true);
CalcPlrInv(player, true);
return true;
}
}
if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Shield) {
if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == DUR_INDESTRUCTIBLE) {
return false;
}
player.InvBody[INVLOC_HAND_LEFT]._iDurability--;
if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == 0) {
RemoveEquipment(player, INVLOC_HAND_LEFT, true);
CalcPlrInv(player, true);
return true;
}
}
return false;
}
bool PlrHitMonst(Player &player, Monster &monster, bool adjacentDamage = false)
{
int hper = 0;
if (!monster.isPossibleToHit())
return false;
if (adjacentDamage) {
if (player.getCharacterLevel() > 20)
hper -= 30;
else
hper -= (35 - player.getCharacterLevel()) * 2;
}
int hit = GenerateRnd(100);
if (monster.mode == MonsterMode::Petrified) {
hit = 0;
}
hper += player.GetMeleePiercingToHit() - player.CalculateArmorPierce(monster.armorClass, true);
hper = std::clamp(hper, 5, 95);
if (monster.tryLiftGargoyle())
return true;
if (hit >= hper) {
#ifdef _DEBUG
if (!DebugGodMode)
#endif
return false;
}
if (gbIsHellfire && HasAllOf(player._pIFlags, ItemSpecialEffect::FireDamage | ItemSpecialEffect::LightningDamage)) {
// Fixed off by 1 error from Hellfire
const int midam = RandomIntBetween(player._pIFMinDam, player._pIFMaxDam);
AddMissile(player.position.tile, player.position.temp, player._pdir, MissileID::SpectralArrow, TARGET_MONSTERS, player, midam, 0);
}
const int mind = player._pIMinDam;
const int maxd = player._pIMaxDam;
int dam = RandomIntBetween(mind, maxd);
dam += dam * player._pIBonusDam / 100;
dam += player._pIBonusDamMod;
int dam2 = dam << 6;
dam += player._pDamageMod;
const ClassAttributes &classAttributes = GetClassAttributes(player._pClass);
if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::CriticalStrike)) {
if (GenerateRnd(100) < player.getCharacterLevel()) {
dam *= 2;
}
}
ItemType phanditype = ItemType::None;
if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Sword || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Sword) {
phanditype = ItemType::Sword;
}
if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Mace || player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Mace) {
phanditype = ItemType::Mace;
}
switch (monster.data().monsterClass) {
case MonsterClass::Undead:
if (phanditype == ItemType::Sword) {
dam -= dam / 2;
} else if (phanditype == ItemType::Mace) {
dam += dam / 2;
}
break;
case MonsterClass::Animal:
if (phanditype == ItemType::Mace) {
dam -= dam / 2;
} else if (phanditype == ItemType::Sword) {
dam += dam / 2;
}
break;
case MonsterClass::Demon:
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::TripleDemonDamage)) {
dam *= 3;
}
break;
}
if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::Devastation) && GenerateRnd(100) < 5) {
dam *= 3;
}
if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::Doppelganger) && monster.type().type != MT_DIABLO && !monster.isUnique() && GenerateRnd(100) < 10) {
AddDoppelganger(monster);
}
dam <<= 6;
if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::Jesters)) {
int r = GenerateRnd(201);
if (r >= 100)
r = 100 + (r - 100) * 5;
dam = dam * r / 100;
}
if (adjacentDamage)
dam >>= 2;
if (&player == MyPlayer) {
if (HasAnyOf(player.pDamAcFlags, ItemSpecialEffectHf::Peril)) {
dam2 += player._pIGetHit << 6;
if (dam2 >= 0) {
ApplyPlrDamage(DamageType::Physical, player, 0, 1, dam2);
}
dam *= 2;
}
#ifdef _DEBUG
if (DebugGodMode) {
dam = monster.hitPoints; /* ensure monster is killed with one hit */
}
#endif
ApplyMonsterDamage(DamageType::Physical, monster, dam);
}
int skdam = 0;
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::RandomStealLife)) {
skdam = GenerateRnd(dam / 8);
player._pHitPoints += skdam;
if (player._pHitPoints > player._pMaxHP) {
player._pHitPoints = player._pMaxHP;
}
player._pHPBase += skdam;
if (player._pHPBase > player._pMaxHPBase) {
player._pHPBase = player._pMaxHPBase;
}
RedrawComponent(PanelDrawComponent::Health);
}
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealMana3 | ItemSpecialEffect::StealMana5) && HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) {
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealMana3)) {
skdam = 3 * dam / 100;
}
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealMana5)) {
skdam = 5 * dam / 100;
}
player._pMana += skdam;
if (player._pMana > player._pMaxMana) {
player._pMana = player._pMaxMana;
}
player._pManaBase += skdam;
if (player._pManaBase > player._pMaxManaBase) {
player._pManaBase = player._pMaxManaBase;
}
RedrawComponent(PanelDrawComponent::Mana);
}
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealLife3 | ItemSpecialEffect::StealLife5)) {
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealLife3)) {
skdam = 3 * dam / 100;
}
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::StealLife5)) {
skdam = 5 * dam / 100;
}
player._pHitPoints += skdam;
if (player._pHitPoints > player._pMaxHP) {
player._pHitPoints = player._pMaxHP;
}
player._pHPBase += skdam;
if (player._pHPBase > player._pMaxHPBase) {
player._pHPBase = player._pMaxHPBase;
}
RedrawComponent(PanelDrawComponent::Health);
}
if (monster.hasNoLife()) {
M_StartKill(monster, player);
} else {
if (monster.mode != MonsterMode::Petrified && HasAnyOf(player._pIFlags, ItemSpecialEffect::Knockback))
M_GetKnockback(monster, player.position.tile);
M_StartHit(monster, player, dam);
}
return true;
}
bool PlrHitPlr(Player &attacker, Player &target)
{
if (target._pInvincible) {
return false;
}
if (HasAnyOf(target._pSpellFlags, SpellFlag::Etherealize)) {
return false;
}
const int hit = GenerateRnd(100);
int hper = attacker.GetMeleeToHit() - target.GetArmor();
hper = std::clamp(hper, 5, 95);
int blk = 100;
if ((target._pmode == PM_STAND || target._pmode == PM_ATTACK) && target._pBlockFlag) {
blk = GenerateRnd(100);
}
int blkper = target.GetBlockChance() - (attacker.getCharacterLevel() * 2);
blkper = std::clamp(blkper, 0, 100);
if (hit >= hper) {
return false;
}
if (blk < blkper) {
const Direction dir = GetDirection(target.position.tile, attacker.position.tile);
StartPlrBlock(target, dir);
return true;
}
const int mind = attacker._pIMinDam;
const int maxd = attacker._pIMaxDam;
int dam = RandomIntBetween(mind, maxd);
dam += (dam * attacker._pIBonusDam) / 100;
dam += attacker._pIBonusDamMod + attacker._pDamageMod;
const ClassAttributes &classAttributes = GetClassAttributes(attacker._pClass);
if (HasAnyOf(classAttributes.classFlags, PlayerClassFlag::CriticalStrike)) {
if (GenerateRnd(100) < attacker.getCharacterLevel()) {
dam *= 2;
}
}
const int skdam = dam << 6;
if (HasAnyOf(attacker._pIFlags, ItemSpecialEffect::RandomStealLife)) {
const int tac = GenerateRnd(skdam / 8);
attacker._pHitPoints += tac;
if (attacker._pHitPoints > attacker._pMaxHP) {
attacker._pHitPoints = attacker._pMaxHP;
}
attacker._pHPBase += tac;
if (attacker._pHPBase > attacker._pMaxHPBase) {
attacker._pHPBase = attacker._pMaxHPBase;
}
RedrawComponent(PanelDrawComponent::Health);
}
if (&attacker == MyPlayer) {
NetSendCmdDamage(true, target, skdam, DamageType::Physical);
}
StartPlrHit(target, skdam, false);
return true;
}
bool PlrHitObj(const Player &player, Object &targetObject)
{
if (targetObject.IsBreakable()) {
BreakObject(player, targetObject);
return true;
}
return false;
}
bool DoAttack(Player &player)
{
if (player.AnimInfo.currentFrame == player._pAFNum - 2) {
PlaySfxLoc(SfxID::Swing, player.position.tile);
}
bool didhit = false;
if (player.AnimInfo.currentFrame == player._pAFNum - 1) {
Point position = player.position.tile + player._pdir;
Monster *monster = FindMonsterAtPosition(position);
if (monster != nullptr) {
if (CanTalkToMonst(*monster)) {
player.position.temp.x = 0; /** @todo Looks to be irrelevant, probably just remove it */
return false;
}
}
if (!gbIsHellfire || !HasAllOf(player._pIFlags, ItemSpecialEffect::FireDamage | ItemSpecialEffect::LightningDamage)) {
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FireDamage)) {
AddMissile(position, { 1, 0 }, Direction::South, MissileID::WeaponExplosion, TARGET_MONSTERS, player, 0, 0);
}
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::LightningDamage)) {
AddMissile(position, { 2, 0 }, Direction::South, MissileID::WeaponExplosion, TARGET_MONSTERS, player, 0, 0);
}
}
if (monster != nullptr) {
didhit = PlrHitMonst(player, *monster);
} else if (PlayerAtPosition(position) != nullptr && !player.friendlyMode) {
didhit = PlrHitPlr(player, *PlayerAtPosition(position));
} else {
Object *object = FindObjectAtPosition(position, false);
if (object != nullptr) {
didhit = PlrHitObj(player, *object);
}
}
if (player.CanCleave()) {
// playing as a class/weapon with cleave
position = player.position.tile + Right(player._pdir);
monster = FindMonsterAtPosition(position);
if (monster != nullptr) {
if (!CanTalkToMonst(*monster) && monster->position.old == position) {
if (PlrHitMonst(player, *monster, true))
didhit = true;
}
}
position = player.position.tile + Left(player._pdir);
monster = FindMonsterAtPosition(position);
if (monster != nullptr) {
if (!CanTalkToMonst(*monster) && monster->position.old == position) {
if (PlrHitMonst(player, *monster, true))
didhit = true;
}
}
}
if (didhit && DamageWeapon(player, 30)) {
StartStand(player, player._pdir);
ClearStateVariables(player);
return true;
}
}
if (player.AnimInfo.isLastFrame()) {
StartStand(player, player._pdir);
ClearStateVariables(player);
return true;
}
return false;
}
bool DoRangeAttack(Player &player)
{
int arrows = 0;
if (player.AnimInfo.currentFrame == player._pAFNum - 1) {
arrows = 1;
}
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::MultipleArrows) && player.AnimInfo.currentFrame == player._pAFNum + 1) {
arrows = 2;
}
for (int arrow = 0; arrow < arrows; arrow++) {
int xoff = 0;
int yoff = 0;
if (arrows != 1) {
const int angle = arrow == 0 ? -1 : 1;
const int x = player.position.temp.x - player.position.tile.x;
if (x != 0)
yoff = x < 0 ? angle : -angle;
const int y = player.position.temp.y - player.position.tile.y;
if (y != 0)
xoff = y < 0 ? -angle : angle;
}
int dmg = 4;
MissileID mistype = MissileID::Arrow;
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FireArrows)) {
mistype = MissileID::FireArrow;
}
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::LightningArrows)) {
mistype = MissileID::LightningArrow;
}
if (HasAllOf(player._pIFlags, ItemSpecialEffect::FireArrows | ItemSpecialEffect::LightningArrows)) {
// Fixed off by 1 error from Hellfire
dmg = RandomIntBetween(player._pIFMinDam, player._pIFMaxDam);
mistype = MissileID::SpectralArrow;
}
AddMissile(
player.position.tile,
player.position.temp + Displacement { xoff, yoff },
player._pdir,
mistype,
TARGET_MONSTERS,
player,
dmg,
0);
if (arrow == 0 && mistype != MissileID::SpectralArrow) {
PlaySfxLoc(arrows != 1 ? SfxID::ShootBow2 : SfxID::ShootBow, player.position.tile);
}
if (DamageWeapon(player, 40)) {
StartStand(player, player._pdir);
ClearStateVariables(player);
return true;
}
}
if (player.AnimInfo.isLastFrame()) {
StartStand(player, player._pdir);
ClearStateVariables(player);
return true;
}
return false;
}
void DamageParryItem(Player &player)
{
if (&player != MyPlayer) {
return;
}
if (player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Shield || player.InvBody[INVLOC_HAND_LEFT]._itype == ItemType::Staff) {
if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == DUR_INDESTRUCTIBLE) {
return;
}
player.InvBody[INVLOC_HAND_LEFT]._iDurability--;
if (player.InvBody[INVLOC_HAND_LEFT]._iDurability == 0) {
RemoveEquipment(player, INVLOC_HAND_LEFT, true);
CalcPlrInv(player, true);
}
}
if (player.InvBody[INVLOC_HAND_RIGHT]._itype == ItemType::Shield) {
if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability != DUR_INDESTRUCTIBLE) {
player.InvBody[INVLOC_HAND_RIGHT]._iDurability--;
if (player.InvBody[INVLOC_HAND_RIGHT]._iDurability == 0) {
RemoveEquipment(player, INVLOC_HAND_RIGHT, true);
CalcPlrInv(player, true);
}
}
}
}
bool DoBlock(Player &player)
{
if (player.AnimInfo.isLastFrame()) {
StartStand(player, player._pdir);
ClearStateVariables(player);
if (FlipCoin(10)) {
DamageParryItem(player);
}
return true;
}
return false;
}
void DamageArmor(Player &player)
{
if (&player != MyPlayer) {
return;
}
if (player.InvBody[INVLOC_CHEST].isEmpty() && player.InvBody[INVLOC_HEAD].isEmpty()) {
return;
}
bool targetHead = FlipCoin(3);
if (!player.InvBody[INVLOC_CHEST].isEmpty() && player.InvBody[INVLOC_HEAD].isEmpty()) {
targetHead = false;
}
if (player.InvBody[INVLOC_CHEST].isEmpty() && !player.InvBody[INVLOC_HEAD].isEmpty()) {
targetHead = true;
}
Item *pi;
if (targetHead) {
pi = &player.InvBody[INVLOC_HEAD];
} else {
pi = &player.InvBody[INVLOC_CHEST];
}
if (pi->_iDurability == DUR_INDESTRUCTIBLE) {
return;
}
pi->_iDurability--;
if (pi->_iDurability != 0) {
return;
}
if (targetHead) {
RemoveEquipment(player, INVLOC_HEAD, true);
} else {
RemoveEquipment(player, INVLOC_CHEST, true);
}
CalcPlrInv(player, true);
}
bool DoSpell(Player &player)
{
if (player.AnimInfo.currentFrame == player._pSFNum) {
CastSpell(
player,
player.executedSpell.spellId,
player.position.tile,
player.position.temp,
player.executedSpell.spellLevel);
if (IsAnyOf(player.executedSpell.spellType, SpellType::Scroll, SpellType::Charges)) {
EnsureValidReadiedSpell(player);
}
}
if (player.AnimInfo.isLastFrame()) {
StartStand(player, player._pdir);
ClearStateVariables(player);
return true;
}
return false;
}
bool DoGotHit(Player &player)
{
if (player.AnimInfo.isLastFrame()) {
StartStand(player, player._pdir);
ClearStateVariables(player);
if (!FlipCoin(4)) {
DamageArmor(player);
}
return true;
}
return false;
}
bool DoDeath(Player &player)
{
if (player.AnimInfo.isLastFrame()) {
if (player.AnimInfo.tickCounterOfCurrentFrame == 0) {
player.AnimInfo.ticksPerFrame = 100;
dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer;
} else if (&player == MyPlayer && player.AnimInfo.tickCounterOfCurrentFrame == 30) {
MyPlayerIsDead = true;
}
}
return false;
}
bool IsPlayerAdjacentToObject(Player &player, Object &object)
{
const int x = std::abs(player.position.tile.x - object.position.x);
int y = std::abs(player.position.tile.y - object.position.y);
if (y > 1 && object.position.y >= 1 && FindObjectAtPosition(object.position + Direction::NorthEast) == &object) {
// special case for activating a large object from the north-east side
y = std::abs(player.position.tile.y - object.position.y + 1);
}
return x <= 1 && y <= 1;
}
void TryDisarm(const Player &player, Object &object)
{
if (&player == MyPlayer)
NewCursor(CURSOR_HAND);
if (!object._oTrapFlag) {
return;
}
const int trapdisper = 2 * player._pDexterity - 5 * currlevel;
if (GenerateRnd(100) > trapdisper) {
return;
}
for (int j = 0; j < ActiveObjectCount; j++) {
Object &trap = Objects[ActiveObjects[j]];
if (trap.IsTrap() && FindObjectAtPosition({ trap._oVar1, trap._oVar2 }) == &object) {
trap._oVar4 = 1;
object._oTrapFlag = false;
}
}
if (object.IsTrappedChest()) {
object._oTrapFlag = false;
}
}
void CheckNewPath(Player &player, bool pmWillBeCalled)
{
int x = 0;
int y = 0;
Monster *monster;
Player *target;
Object *object;
Item *item;
const int targetId = player.destParam1;
switch (player.destAction) {
case ACTION_ATTACKMON:
case ACTION_RATTACKMON:
case ACTION_SPELLMON:
monster = &Monsters[targetId];
if (monster->hasNoLife()) {
player.Stop();
return;
}
if (player.destAction == ACTION_ATTACKMON)
MakePlrPath(player, monster->position.future, false);
break;
case ACTION_ATTACKPLR:
case ACTION_RATTACKPLR:
case ACTION_SPELLPLR:
target = &Players[targetId];
if (target->hasNoLife()) {
player.Stop();
return;
}
if (player.destAction == ACTION_ATTACKPLR)
MakePlrPath(player, target->position.future, false);
break;
case ACTION_OPERATE:
case ACTION_DISARM:
case ACTION_OPERATETK:
object = &Objects[targetId];
break;
case ACTION_PICKUPITEM:
case ACTION_PICKUPAITEM:
item = &Items[targetId];
break;
default:
break;
}
Direction d;
if (player.walkpath[0] != WALK_NONE) {
if (player._pmode == PM_STAND) {
if (&player == MyPlayer) {
if (player.destAction == ACTION_ATTACKMON || player.destAction == ACTION_ATTACKPLR) {
if (player.destAction == ACTION_ATTACKMON) {
x = std::abs(player.position.future.x - monster->position.future.x);
y = std::abs(player.position.future.y - monster->position.future.y);
d = GetDirection(player.position.future, monster->position.future);
} else {
x = std::abs(player.position.future.x - target->position.future.x);
y = std::abs(player.position.future.y - target->position.future.y);
d = GetDirection(player.position.future, target->position.future);
}
if (x < 2 && y < 2) {
ClrPlrPath(player);
if (player.destAction == ACTION_ATTACKMON && monster->talkMsg != TEXT_NONE && monster->talkMsg != TEXT_VILE14) {
TalktoMonster(player, *monster);
} else {
StartAttack(player, d, pmWillBeCalled);
}
player.destAction = ACTION_NONE;
}
}
}
switch (player.walkpath[0]) {
case WALK_N:
StartWalk(player, Direction::North, pmWillBeCalled);
break;
case WALK_NE:
StartWalk(player, Direction::NorthEast, pmWillBeCalled);
break;
case WALK_E:
StartWalk(player, Direction::East, pmWillBeCalled);
break;
case WALK_SE:
StartWalk(player, Direction::SouthEast, pmWillBeCalled);
break;
case WALK_S:
StartWalk(player, Direction::South, pmWillBeCalled);
break;
case WALK_SW:
StartWalk(player, Direction::SouthWest, pmWillBeCalled);
break;
case WALK_W:
StartWalk(player, Direction::West, pmWillBeCalled);
break;
case WALK_NW:
StartWalk(player, Direction::NorthWest, pmWillBeCalled);
break;
}
for (size_t j = 1; j < MaxPathLengthPlayer; j++) {
player.walkpath[j - 1] = player.walkpath[j];
}
player.walkpath[MaxPathLengthPlayer - 1] = WALK_NONE;
if (player._pmode == PM_STAND) {
StartStand(player, player._pdir);
player.destAction = ACTION_NONE;
}
}
return;
}
if (player.destAction == ACTION_NONE) {
return;
}
if (player._pmode == PM_STAND) {
switch (player.destAction) {
case ACTION_ATTACK:
d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 });
StartAttack(player, d, pmWillBeCalled);
break;
case ACTION_ATTACKMON:
x = std::abs(player.position.tile.x - monster->position.future.x);
y = std::abs(player.position.tile.y - monster->position.future.y);
if (x <= 1 && y <= 1) {
d = GetDirection(player.position.future, monster->position.future);
if (monster->talkMsg != TEXT_NONE && monster->talkMsg != TEXT_VILE14) {
TalktoMonster(player, *monster);
} else {
StartAttack(player, d, pmWillBeCalled);
}
}
break;
case ACTION_ATTACKPLR:
x = std::abs(player.position.tile.x - target->position.future.x);
y = std::abs(player.position.tile.y - target->position.future.y);
if (x <= 1 && y <= 1) {
d = GetDirection(player.position.future, target->position.future);
StartAttack(player, d, pmWillBeCalled);
}
break;
case ACTION_RATTACK:
d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 });
StartRangeAttack(player, d, player.destParam1, player.destParam2, pmWillBeCalled);
break;
case ACTION_RATTACKMON:
d = GetDirection(player.position.future, monster->position.future);
if (monster->talkMsg != TEXT_NONE && monster->talkMsg != TEXT_VILE14) {
TalktoMonster(player, *monster);
} else {
StartRangeAttack(player, d, monster->position.future.x, monster->position.future.y, pmWillBeCalled);
}
break;
case ACTION_RATTACKPLR:
d = GetDirection(player.position.future, target->position.future);
StartRangeAttack(player, d, target->position.future.x, target->position.future.y, pmWillBeCalled);
break;
case ACTION_SPELL:
d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 });
StartSpell(player, d, player.destParam1, player.destParam2);
break;
case ACTION_SPELLWALL:
StartSpell(player, static_cast<Direction>(player.destParam3), player.destParam1, player.destParam2);
player.tempDirection = static_cast<Direction>(player.destParam3);
break;
case ACTION_SPELLMON:
d = GetDirection(player.position.tile, monster->position.future);
StartSpell(player, d, monster->position.future.x, monster->position.future.y);
break;
case ACTION_SPELLPLR:
d = GetDirection(player.position.tile, target->position.future);
StartSpell(player, d, target->position.future.x, target->position.future.y);
break;
case ACTION_OPERATE:
if (IsPlayerAdjacentToObject(player, *object)) {
if (object->_oBreak == 1) {
d = GetDirection(player.position.tile, object->position);
StartAttack(player, d, pmWillBeCalled);
} else {
OperateObject(player, *object);
}
}
break;
case ACTION_DISARM:
if (IsPlayerAdjacentToObject(player, *object)) {
if (object->_oBreak == 1) {
d = GetDirection(player.position.tile, object->position);
StartAttack(player, d, pmWillBeCalled);
} else {
TryDisarm(player, *object);
OperateObject(player, *object);
}
}
break;
case ACTION_OPERATETK:
if (object->_oBreak != 1) {
OperateObject(player, *object);
}
break;
case ACTION_PICKUPITEM:
if (&player == MyPlayer) {
x = std::abs(player.position.tile.x - item->position.x);
y = std::abs(player.position.tile.y - item->position.y);
if (x <= 1 && y <= 1 && pcurs == CURSOR_HAND && !item->_iRequest) {
NetSendCmdGItem(true, CMD_REQUESTGITEM, player, targetId);
item->_iRequest = true;
}
}
break;
case ACTION_PICKUPAITEM:
if (&player == MyPlayer) {
x = std::abs(player.position.tile.x - item->position.x);
y = std::abs(player.position.tile.y - item->position.y);
if (x <= 1 && y <= 1 && pcurs == CURSOR_HAND) {
NetSendCmdGItem(true, CMD_REQUESTAGITEM, player, targetId);
}
}
break;
case ACTION_TALK:
if (&player == MyPlayer) {
HelpFlag = false;
TalkToTowner(player, player.destParam1);
}
break;
default:
break;
}
FixPlayerLocation(player, player._pdir);
player.destAction = ACTION_NONE;
return;
}
if (player._pmode == PM_ATTACK && player.AnimInfo.currentFrame >= player._pAFNum) {
if (player.destAction == ACTION_ATTACK) {
d = GetDirection(player.position.future, { player.destParam1, player.destParam2 });
StartAttack(player, d, pmWillBeCalled);
player.destAction = ACTION_NONE;
} else if (player.destAction == ACTION_ATTACKMON) {
x = std::abs(player.position.tile.x - monster->position.future.x);
y = std::abs(player.position.tile.y - monster->position.future.y);
if (x <= 1 && y <= 1) {
d = GetDirection(player.position.future, monster->position.future);
StartAttack(player, d, pmWillBeCalled);
}
player.destAction = ACTION_NONE;
} else if (player.destAction == ACTION_ATTACKPLR) {
x = std::abs(player.position.tile.x - target->position.future.x);
y = std::abs(player.position.tile.y - target->position.future.y);
if (x <= 1 && y <= 1) {
d = GetDirection(player.position.future, target->position.future);
StartAttack(player, d, pmWillBeCalled);
}
player.destAction = ACTION_NONE;
} else if (player.destAction == ACTION_OPERATE) {
if (IsPlayerAdjacentToObject(player, *object)) {
if (object->_oBreak == 1) {
d = GetDirection(player.position.tile, object->position);
StartAttack(player, d, pmWillBeCalled);
}
}
}
}
if (player._pmode == PM_RATTACK && player.AnimInfo.currentFrame >= player._pAFNum) {
if (player.destAction == ACTION_RATTACK) {
d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 });
StartRangeAttack(player, d, player.destParam1, player.destParam2, pmWillBeCalled);
player.destAction = ACTION_NONE;
} else if (player.destAction == ACTION_RATTACKMON) {
d = GetDirection(player.position.tile, monster->position.future);
StartRangeAttack(player, d, monster->position.future.x, monster->position.future.y, pmWillBeCalled);
player.destAction = ACTION_NONE;
} else if (player.destAction == ACTION_RATTACKPLR) {
d = GetDirection(player.position.tile, target->position.future);
StartRangeAttack(player, d, target->position.future.x, target->position.future.y, pmWillBeCalled);
player.destAction = ACTION_NONE;
}
}
if (player._pmode == PM_SPELL && player.AnimInfo.currentFrame >= player._pSFNum) {
if (player.destAction == ACTION_SPELL) {
d = GetDirection(player.position.tile, { player.destParam1, player.destParam2 });
StartSpell(player, d, player.destParam1, player.destParam2);
player.destAction = ACTION_NONE;
} else if (player.destAction == ACTION_SPELLMON) {
d = GetDirection(player.position.tile, monster->position.future);
StartSpell(player, d, monster->position.future.x, monster->position.future.y);
player.destAction = ACTION_NONE;
} else if (player.destAction == ACTION_SPELLPLR) {
d = GetDirection(player.position.tile, target->position.future);
StartSpell(player, d, target->position.future.x, target->position.future.y);
player.destAction = ACTION_NONE;
}
}
}
bool PlrDeathModeOK(Player &player)
{
if (&player != MyPlayer) {
return true;
}
if (player._pmode == PM_DEATH) {
return true;
}
if (player._pmode == PM_QUIT) {
return true;
}
if (player._pmode == PM_NEWLVL) {
return true;
}
return false;
}
void ValidatePlayer()
{
assert(MyPlayer != nullptr);
Player &myPlayer = *MyPlayer;
// Player::setCharacterLevel ensures that the player level is within the expected range in case someone has edited their character level in memory
myPlayer.setCharacterLevel(myPlayer.getCharacterLevel());
// This lets us catch cases where someone is editing experience directly through memory modification and reset their experience back to the expected cap.
if (myPlayer._pExperience > myPlayer.getNextExperienceThreshold()) {
myPlayer._pExperience = myPlayer.getNextExperienceThreshold();
if (*GetOptions().Gameplay.experienceBar) {
RedrawEverything();
}
}
int gt = 0;
for (int i = 0; i < myPlayer._pNumInv; i++) {
if (myPlayer.InvList[i]._itype == ItemType::Gold) {
int maxGold = GOLD_MAX_LIMIT;
if (gbIsHellfire) {
maxGold *= 2;
}
if (myPlayer.InvList[i]._ivalue > maxGold) {
myPlayer.InvList[i]._ivalue = maxGold;
}
gt += myPlayer.InvList[i]._ivalue;
}
}
if (gt != myPlayer._pGold)
myPlayer._pGold = gt;
if (myPlayer._pBaseStr > myPlayer.GetMaximumAttributeValue(CharacterAttribute::Strength)) {
myPlayer._pBaseStr = myPlayer.GetMaximumAttributeValue(CharacterAttribute::Strength);
}
if (myPlayer._pBaseMag > myPlayer.GetMaximumAttributeValue(CharacterAttribute::Magic)) {
myPlayer._pBaseMag = myPlayer.GetMaximumAttributeValue(CharacterAttribute::Magic);
}
if (myPlayer._pBaseDex > myPlayer.GetMaximumAttributeValue(CharacterAttribute::Dexterity)) {
myPlayer._pBaseDex = myPlayer.GetMaximumAttributeValue(CharacterAttribute::Dexterity);
}
if (myPlayer._pBaseVit > myPlayer.GetMaximumAttributeValue(CharacterAttribute::Vitality)) {
myPlayer._pBaseVit = myPlayer.GetMaximumAttributeValue(CharacterAttribute::Vitality);
}
uint64_t msk = 0;
for (size_t b = static_cast<size_t>(SpellID::Firebolt); b < SpellsData.size(); b++) {
if (GetSpellBookLevel((SpellID)b) != -1) {
msk |= GetSpellBitmask(static_cast<SpellID>(b));
if (myPlayer._pSplLvl[b] > MaxSpellLevel)
myPlayer._pSplLvl[b] = MaxSpellLevel;
}
}
myPlayer._pMemSpells &= msk;
myPlayer._pInfraFlag = false;
}
HeroClass GetPlayerSpriteClass(HeroClass cls)
{
if (cls == HeroClass::Bard && !HaveBardAssets())
return HeroClass::Rogue;
if (cls == HeroClass::Barbarian && !HaveBarbarianAssets())
return HeroClass::Warrior;
return cls;
}
PlayerWeaponGraphic GetPlayerWeaponGraphic(player_graphic graphic, PlayerWeaponGraphic weaponGraphic)
{
if (leveltype == DTYPE_TOWN && IsAnyOf(graphic, player_graphic::Lightning, player_graphic::Fire, player_graphic::Magic)) {
// If the hero doesn't hold the weapon in town then we should use the unarmed animation for casting
switch (weaponGraphic) {
case PlayerWeaponGraphic::Mace:
case PlayerWeaponGraphic::Sword:
return PlayerWeaponGraphic::Unarmed;
case PlayerWeaponGraphic::SwordShield:
case PlayerWeaponGraphic::MaceShield:
return PlayerWeaponGraphic::UnarmedShield;
default:
break;
}
}
return weaponGraphic;
}
uint16_t GetPlayerSpriteWidth(HeroClass cls, player_graphic graphic, PlayerWeaponGraphic weaponGraphic)
{
const PlayerSpriteData spriteData = GetPlayerSpriteDataForClass(cls);
switch (graphic) {
case player_graphic::Stand:
return spriteData.stand;
case player_graphic::Walk:
return spriteData.walk;
case player_graphic::Attack:
if (weaponGraphic == PlayerWeaponGraphic::Bow)
return spriteData.bow;
return spriteData.attack;
case player_graphic::Hit:
return spriteData.swHit;
case player_graphic::Block:
return spriteData.block;
case player_graphic::Lightning:
return spriteData.lightning;
case player_graphic::Fire:
return spriteData.fire;
case player_graphic::Magic:
return spriteData.magic;
case player_graphic::Death:
return spriteData.death;
}
app_fatal("Invalid player_graphic");
}
void GetPlayerGraphicsPath(std::string_view path, std::string_view prefix, std::string_view type, char out[256])
{
*BufCopy(out, "plrgfx\\", path, "\\", prefix, "\\", prefix, type) = '\0';
}
} // namespace
void Player::CalcScrolls()
{
_pScrlSpells = 0;
for (const Item &item : InventoryAndBeltPlayerItemsRange { *this }) {
if (item.isScroll() && item._iStatFlag) {
_pScrlSpells |= GetSpellBitmask(item._iSpell);
}
}
EnsureValidReadiedSpell(*this);
}
bool Player::CanUseItem(const Item &item) const
{
if (!IsItemValid(*this, item))
return false;
return _pStrength >= item._iMinStr
&& _pMagic >= item._iMinMag
&& _pDexterity >= item._iMinDex;
}
void Player::RemoveInvItem(int iv, bool calcScrolls)
{
if (this == MyPlayer) {
// Locate the first grid index containing this item and notify remote clients
for (size_t i = 0; i < InventoryGridCells; i++) {
const int8_t itemIndex = InvGrid[i];
if (std::abs(itemIndex) - 1 == iv) {
NetSendCmdParam1(false, CMD_DELINVITEMS, static_cast<uint16_t>(i));
break;
}
}
}
// Iterate through invGrid and remove every reference to item
for (int8_t &itemIndex : InvGrid) {
if (std::abs(itemIndex) - 1 == iv) {
itemIndex = 0;
}
}
InvList[iv].clear();
_pNumInv--;
// If the item at the end of inventory array isn't the one removed, shift all following items back one index to retain inventory order.
if (_pNumInv > 0 && _pNumInv != iv) {
for (int newIndex = iv; newIndex < _pNumInv; newIndex++) {
InvList[newIndex] = InvList[newIndex + 1].pop();
}
for (int8_t &itemIndex : InvGrid) {
if (itemIndex > iv + 1) { // if item was shifted, decrease the index so it's paired with the correct item.
itemIndex--;
}
if (itemIndex < -(iv + 1)) {
itemIndex++; // since occupied cells are negative, increment the index to keep it same as as top-left cell for item, only negative.
}
}
}
if (calcScrolls) {
CalcScrolls();
}
}
void Player::RemoveSpdBarItem(int iv)
{
if (this == MyPlayer) {
NetSendCmdParam1(false, CMD_DELBELTITEMS, iv);
}
SpdList[iv].clear();
CalcScrolls();
RedrawEverything();
}
[[nodiscard]] uint8_t Player::getId() const
{
return static_cast<uint8_t>(std::distance<const Player *>(&Players[0], this));
}
int Player::GetBaseAttributeValue(CharacterAttribute attribute) const
{
switch (attribute) {
case CharacterAttribute::Dexterity:
return this->_pBaseDex;
case CharacterAttribute::Magic:
return this->_pBaseMag;
case CharacterAttribute::Strength:
return this->_pBaseStr;
case CharacterAttribute::Vitality:
return this->_pBaseVit;
default:
app_fatal("Unsupported attribute");
}
}
int Player::GetCurrentAttributeValue(CharacterAttribute attribute) const
{
switch (attribute) {
case CharacterAttribute::Dexterity:
return this->_pDexterity;
case CharacterAttribute::Magic:
return this->_pMagic;
case CharacterAttribute::Strength:
return this->_pStrength;
case CharacterAttribute::Vitality:
return this->_pVitality;
default:
app_fatal("Unsupported attribute");
}
}
int Player::GetMaximumAttributeValue(CharacterAttribute attribute) const
{
const ClassAttributes &attr = getClassAttributes();
switch (attribute) {
case CharacterAttribute::Strength:
return attr.maxStr;
case CharacterAttribute::Magic:
return attr.maxMag;
case CharacterAttribute::Dexterity:
return attr.maxDex;
case CharacterAttribute::Vitality:
return attr.maxVit;
}
app_fatal("Unsupported attribute");
}
Point Player::GetTargetPosition() const
{
// clang-format off
constexpr int DirectionOffsetX[8] = { 0,-1, 1, 0,-1, 1, 1,-1 };
constexpr int DirectionOffsetY[8] = { -1, 0, 0, 1,-1,-1, 1, 1 };
// clang-format on
Point target = position.future;
for (auto step : walkpath) {
if (step == WALK_NONE)
break;
if (step > 0) {
target.x += DirectionOffsetX[step - 1];
target.y += DirectionOffsetY[step - 1];
}
}
return target;
}
int Player::GetPositionPathIndex(Point pos)
{
constexpr Displacement DirectionOffset[8] = { { 0, -1 }, { -1, 0 }, { 1, 0 }, { 0, 1 }, { -1, -1 }, { 1, -1 }, { 1, 1 }, { -1, 1 } };
Point target = position.future;
int i = 0;
for (auto step : walkpath) {
if (target == pos) return i;
if (step == WALK_NONE)
break;
if (step > 0) {
target += DirectionOffset[step - 1];
}
++i;
}
return -1;
}
void Player::Say(HeroSpeech speechId) const
{
const SfxID soundEffect = GetHeroSound(_pClass, speechId);
if (soundEffect == SfxID::None)
return;
PlaySfxLoc(soundEffect, position.tile);
}
void Player::SaySpecific(HeroSpeech speechId) const
{
const SfxID soundEffect = GetHeroSound(_pClass, speechId);
if (soundEffect == SfxID::None || effect_is_playing(soundEffect))
return;
PlaySfxLoc(soundEffect, position.tile, false);
}
void Player::Say(HeroSpeech speechId, int delay) const
{
sfxdelay = delay;
sfxdnum = GetHeroSound(_pClass, speechId);
}
void Player::Stop()
{
ClrPlrPath(*this);
destAction = ACTION_NONE;
}
bool Player::isWalking() const
{
return IsAnyOf(_pmode, PM_WALK_NORTHWARDS, PM_WALK_SOUTHWARDS, PM_WALK_SIDEWAYS);
}
int Player::GetManaShieldDamageReduction()
{
constexpr uint8_t Max = 7;
return 24 - std::min(_pSplLvl[static_cast<int8_t>(SpellID::ManaShield)], Max) * 3;
}
void Player::RestorePartialLife()
{
const int wholeHitpoints = _pMaxHP >> 6;
int l = ((wholeHitpoints / 8) + GenerateRnd(wholeHitpoints / 4)) << 6;
if (IsAnyOf(_pClass, HeroClass::Warrior, HeroClass::Barbarian))
l *= 2;
if (IsAnyOf(_pClass, HeroClass::Rogue, HeroClass::Monk, HeroClass::Bard))
l += l / 2;
_pHitPoints = std::min(_pHitPoints + l, _pMaxHP);
_pHPBase = std::min(_pHPBase + l, _pMaxHPBase);
}
void Player::RestorePartialMana()
{
const int wholeManaPoints = _pMaxMana >> 6;
int l = ((wholeManaPoints / 8) + GenerateRnd(wholeManaPoints / 4)) << 6;
if (_pClass == HeroClass::Sorcerer)
l *= 2;
if (IsAnyOf(_pClass, HeroClass::Rogue, HeroClass::Monk, HeroClass::Bard))
l += l / 2;
if (HasNoneOf(_pIFlags, ItemSpecialEffect::NoMana)) {
_pMana = std::min(_pMana + l, _pMaxMana);
_pManaBase = std::min(_pManaBase + l, _pMaxManaBase);
}
}
void Player::ReadySpellFromEquipment(inv_body_loc bodyLocation, bool forceSpell)
{
const Item &item = InvBody[bodyLocation];
if (item._itype == ItemType::Staff && IsValidSpell(item._iSpell) && item._iCharges > 0 && item._iStatFlag) {
if (forceSpell || _pRSpell == SpellID::Invalid || _pRSplType == SpellType::Invalid) {
_pRSpell = item._iSpell;
_pRSplType = SpellType::Charges;
RedrawEverything();
}
}
}
player_graphic Player::getGraphic() const
{
switch (_pmode) {
case PM_STAND:
case PM_NEWLVL:
case PM_QUIT:
return player_graphic::Stand;
case PM_WALK_NORTHWARDS:
case PM_WALK_SOUTHWARDS:
case PM_WALK_SIDEWAYS:
return player_graphic::Walk;
case PM_ATTACK:
case PM_RATTACK:
return player_graphic::Attack;
case PM_BLOCK:
return player_graphic::Block;
case PM_SPELL:
return GetPlayerGraphicForSpell(executedSpell.spellId);
case PM_GOTHIT:
return player_graphic::Hit;
case PM_DEATH:
return player_graphic::Death;
default:
app_fatal("SyncPlrAnim");
}
}
uint16_t Player::getSpriteWidth() const
{
if (!HeadlessMode)
return (*AnimInfo.sprites)[0].width();
const player_graphic graphic = getGraphic();
const HeroClass cls = GetPlayerSpriteClass(_pClass);
const PlayerWeaponGraphic weaponGraphic = GetPlayerWeaponGraphic(graphic, static_cast<PlayerWeaponGraphic>(_pgfxnum & 0xF));
return GetPlayerSpriteWidth(cls, graphic, weaponGraphic);
}
void Player::getAnimationFramesAndTicksPerFrame(player_graphic graphics, int8_t &numberOfFrames, int8_t &ticksPerFrame) const
{
ticksPerFrame = 1;
switch (graphics) {
case player_graphic::Stand:
numberOfFrames = _pNFrames;
ticksPerFrame = 4;
break;
case player_graphic::Walk:
numberOfFrames = _pWFrames;
break;
case player_graphic::Attack:
numberOfFrames = _pAFrames;
break;
case player_graphic::Hit:
numberOfFrames = _pHFrames;
break;
case player_graphic::Lightning:
case player_graphic::Fire:
case player_graphic::Magic:
numberOfFrames = _pSFrames;
break;
case player_graphic::Death:
numberOfFrames = _pDFrames;
ticksPerFrame = 2;
break;
case player_graphic::Block:
numberOfFrames = _pBFrames;
ticksPerFrame = 3;
break;
default:
app_fatal("Unknown player graphics");
}
}
void Player::UpdatePreviewCelSprite(_cmd_id cmdId, Point point, uint16_t wParam1, uint16_t wParam2)
{
// if game is not running don't show a preview
if (!gbRunGame || PauseMode != 0 || !gbProcessPlayers)
return;
// we can only show a preview if our command is executed in the next game tick
if (_pmode != PM_STAND)
return;
std::optional<player_graphic> graphic;
Direction dir = Direction::South;
int minimalWalkDistance = -1;
switch (cmdId) {
case _cmd_id::CMD_RATTACKID: {
const Monster &monster = Monsters[wParam1];
dir = GetDirection(position.future, monster.position.future);
graphic = player_graphic::Attack;
break;
}
case _cmd_id::CMD_SPELLID: {
const Monster &monster = Monsters[wParam1];
dir = GetDirection(position.future, monster.position.future);
graphic = GetPlayerGraphicForSpell(static_cast<SpellID>(wParam2));
break;
}
case _cmd_id::CMD_ATTACKID: {
const Monster &monster = Monsters[wParam1];
point = monster.position.future;
minimalWalkDistance = 2;
if (!CanTalkToMonst(monster)) {
dir = GetDirection(position.future, monster.position.future);
graphic = player_graphic::Attack;
}
break;
}
case _cmd_id::CMD_RATTACKPID: {
const Player &targetPlayer = Players[wParam1];
dir = GetDirection(position.future, targetPlayer.position.future);
graphic = player_graphic::Attack;
break;
}
case _cmd_id::CMD_SPELLPID: {
const Player &targetPlayer = Players[wParam1];
dir = GetDirection(position.future, targetPlayer.position.future);
graphic = GetPlayerGraphicForSpell(static_cast<SpellID>(wParam2));
break;
}
case _cmd_id::CMD_ATTACKPID: {
const Player &targetPlayer = Players[wParam1];
point = targetPlayer.position.future;
minimalWalkDistance = 2;
dir = GetDirection(position.future, targetPlayer.position.future);
graphic = player_graphic::Attack;
break;
}
case _cmd_id::CMD_RATTACKXY:
case _cmd_id::CMD_SATTACKXY:
dir = GetDirection(position.tile, point);
graphic = player_graphic::Attack;
break;
case _cmd_id::CMD_SPELLXY:
dir = GetDirection(position.tile, point);
graphic = GetPlayerGraphicForSpell(static_cast<SpellID>(wParam1));
break;
case _cmd_id::CMD_SPELLXYD:
dir = static_cast<Direction>(wParam2);
graphic = GetPlayerGraphicForSpell(static_cast<SpellID>(wParam1));
break;
case _cmd_id::CMD_WALKXY:
minimalWalkDistance = 1;
break;
case _cmd_id::CMD_TALKXY:
case _cmd_id::CMD_DISARMXY:
case _cmd_id::CMD_OPOBJXY:
case _cmd_id::CMD_GOTOGETITEM:
case _cmd_id::CMD_GOTOAGETITEM:
minimalWalkDistance = 2;
break;
default:
return;
}
if (minimalWalkDistance >= 0 && position.future != point) {
int8_t testWalkPath[MaxPathLengthPlayer];
const int steps = FindPath(CanStep, [this](Point tile) { return PosOkPlayer(*this, tile); }, position.future, point, testWalkPath, MaxPathLengthPlayer);
if (steps == 0) {
// Can't walk to desired location => stand still
return;
}
if (steps >= minimalWalkDistance) {
graphic = player_graphic::Walk;
switch (testWalkPath[0]) {
case WALK_N:
dir = Direction::North;
break;
case WALK_NE:
dir = Direction::NorthEast;
break;
case WALK_E:
dir = Direction::East;
break;
case WALK_SE:
dir = Direction::SouthEast;
break;
case WALK_S:
dir = Direction::South;
break;
case WALK_SW:
dir = Direction::SouthWest;
break;
case WALK_W:
dir = Direction::West;
break;
case WALK_NW:
dir = Direction::NorthWest;
break;
}
if (!PlrDirOK(*this, dir))
return;
}
}
if (!graphic || HeadlessMode)
return;
LoadPlrGFX(*this, *graphic);
const ClxSpriteList sprites = AnimationData[static_cast<size_t>(*graphic)].spritesForDirection(dir);
if (!previewCelSprite || *previewCelSprite != sprites[0]) {
previewCelSprite = sprites[0];
progressToNextGameTickWhenPreviewWasSet = ProgressToNextGameTick;
}
}
void Player::setCharacterLevel(uint8_t level)
{
this->_pLevel = std::clamp<uint8_t>(level, 1U, getMaxCharacterLevel());
}
uint8_t Player::getMaxCharacterLevel() const
{
return GetMaximumCharacterLevel();
}
uint32_t Player::getNextExperienceThreshold() const
{
return GetNextExperienceThresholdForLevel(this->getCharacterLevel());
}
int32_t Player::calculateBaseLife() const
{
const ClassAttributes &attr = getClassAttributes();
return attr.adjLife + (attr.lvlLife * getCharacterLevel()) + (attr.chrLife * _pBaseVit);
}
int32_t Player::calculateBaseMana() const
{
const ClassAttributes &attr = getClassAttributes();
return attr.adjMana + (attr.lvlMana * getCharacterLevel()) + (attr.chrMana * _pBaseMag);
}
void Player::occupyTile(Point tilePosition, bool isMoving) const
{
int16_t id = this->getId();
id += 1;
dPlayer[tilePosition.x][tilePosition.y] = isMoving ? -id : id;
}
bool Player::isLevelOwnedByLocalClient() const
{
for (const Player &other : Players) {
if (!other.plractive)
continue;
if (other._pLvlChanging)
continue;
if (other._pmode == PM_NEWLVL)
continue;
if (other.plrlevel != this->plrlevel)
continue;
if (other.plrIsOnSetLevel != this->plrIsOnSetLevel)
continue;
if (&other == MyPlayer && gbBufferMsgs != 0)
continue;
return &other == MyPlayer;
}
return false;
}
Player *PlayerAtPosition(Point position, bool ignoreMovingPlayers /*= false*/)
{
if (!InDungeonBounds(position))
return nullptr;
auto playerIndex = dPlayer[position.x][position.y];
if (playerIndex == 0 || (ignoreMovingPlayers && playerIndex < 0))
return nullptr;
return &Players[std::abs(playerIndex) - 1];
}
ClxSprite GetPlayerPortraitSprite(Player &player)
{
const bool inDungeon = (player.plrlevel != 0);
const HeroClass cls = GetPlayerSpriteClass(player._pClass);
const PlayerWeaponGraphic animWeaponId = GetPlayerWeaponGraphic(player_graphic::Stand, static_cast<PlayerWeaponGraphic>(player._pgfxnum & 0xF));
const PlayerSpriteData &spriteData = GetPlayerSpriteDataForClass(cls);
const char *path = spriteData.classPath.c_str();
std::string_view szCel = inDungeon ? "as" : "st";
player_graphic graphic = player_graphic::Stand;
if (player.hasNoLife()) {
if (animWeaponId == PlayerWeaponGraphic::Unarmed) {
szCel = "dt";
graphic = player_graphic::Death;
}
}
const char prefixBuf[3] = { spriteData.classChar, ArmourChar[player._pgfxnum >> 4], WepChar[static_cast<std::size_t>(animWeaponId)] };
char pszName[256];
GetPlayerGraphicsPath(path, std::string_view(prefixBuf, 3), szCel, pszName);
const std::string spritePath { pszName };
// Check to see if the sprite has updated.
if (player.PartyInfoSpriteLocations[inDungeon] != spritePath) {
// The sprite has changed so store the new location
player.PartyInfoSpriteLocations[inDungeon] = spritePath;
player.PartyInfoSprites[inDungeon] = std::nullopt;
// And now load the new sprite and store it
const uint16_t animationWidth = GetPlayerSpriteWidth(cls, graphic, animWeaponId);
player.PartyInfoSprites[inDungeon] = LoadCl2Sheet(pszName, animationWidth);
}
const ClxSpriteList spriteList = (*player.PartyInfoSprites[inDungeon])[static_cast<size_t>(Direction::South)];
return spriteList[(graphic == player_graphic::Stand) ? 0 : spriteList.numSprites() - 1];
}
bool IsPlayerUnarmed(Player &player)
{
const PlayerWeaponGraphic animWeaponId = GetPlayerWeaponGraphic(player_graphic::Stand, static_cast<PlayerWeaponGraphic>(player._pgfxnum & 0xF));
return animWeaponId == PlayerWeaponGraphic::Unarmed;
}
void LoadPlrGFX(Player &player, player_graphic graphic)
{
if (HeadlessMode)
return;
auto &animationData = player.AnimationData[static_cast<size_t>(graphic)];
if (animationData.sprites)
return;
const HeroClass cls = GetPlayerSpriteClass(player._pClass);
const PlayerWeaponGraphic animWeaponId = GetPlayerWeaponGraphic(graphic, static_cast<PlayerWeaponGraphic>(player._pgfxnum & 0xF));
const PlayerSpriteData &spriteData = GetPlayerSpriteDataForClass(cls);
const char *path = spriteData.classPath.c_str();
std::string_view szCel;
switch (graphic) {
case player_graphic::Stand:
szCel = "as";
if (leveltype == DTYPE_TOWN)
szCel = "st";
break;
case player_graphic::Walk:
szCel = "aw";
if (leveltype == DTYPE_TOWN)
szCel = "wl";
break;
case player_graphic::Attack:
if (leveltype == DTYPE_TOWN)
return;
szCel = "at";
break;
case player_graphic::Hit:
if (leveltype == DTYPE_TOWN)
return;
szCel = "ht";
break;
case player_graphic::Lightning:
szCel = "lm";
break;
case player_graphic::Fire:
szCel = "fm";
break;
case player_graphic::Magic:
szCel = "qm";
break;
case player_graphic::Death:
if (animWeaponId != PlayerWeaponGraphic::Unarmed)
return;
szCel = "dt";
break;
case player_graphic::Block:
if (leveltype == DTYPE_TOWN)
return;
if (!player._pBlockFlag)
return;
szCel = "bl";
break;
default:
app_fatal("PLR:2");
}
const char prefixBuf[3] = { spriteData.classChar, ArmourChar[player._pgfxnum >> 4], WepChar[static_cast<std::size_t>(animWeaponId)] };
char pszName[256];
GetPlayerGraphicsPath(path, std::string_view(prefixBuf, 3), szCel, pszName);
const uint16_t animationWidth = GetPlayerSpriteWidth(cls, graphic, animWeaponId);
animationData.sprites = LoadCl2Sheet(pszName, animationWidth);
std::optional<std::array<uint8_t, 256>> graphicTRN = GetPlayerGraphicTRN(pszName);
if (graphicTRN) {
ClxApplyTrans(*animationData.sprites, graphicTRN->data());
}
std::optional<std::array<uint8_t, 256>> classTRN = GetClassTRN(player);
if (classTRN) {
ClxApplyTrans(*animationData.sprites, classTRN->data());
}
}
void InitPlayerGFX(Player &player)
{
if (HeadlessMode)
return;
ResetPlayerGFX(player);
if (player.hasNoLife()) {
player._pgfxnum &= ~0xFU;
LoadPlrGFX(player, player_graphic::Death);
return;
}
8 years ago
for (size_t i = 0; i < enum_size<player_graphic>::value; i++) {
auto graphic = static_cast<player_graphic>(i);
if (graphic == player_graphic::Death)
continue;
LoadPlrGFX(player, graphic);
}
}
void ResetPlayerGFX(Player &player)
{
player.AnimInfo.sprites = std::nullopt;
if (!gbRunGame) {
player.PartyInfoSprites[0] = std::nullopt;
player.PartyInfoSprites[1] = std::nullopt;
}
for (PlayerAnimationData &animData : player.AnimationData) {
animData.sprites = std::nullopt;
}
}
void NewPlrAnim(Player &player, player_graphic graphic, Direction dir, AnimationDistributionFlags flags /*= AnimationDistributionFlags::None*/, int8_t numSkippedFrames /*= 0*/, int8_t distributeFramesBeforeFrame /*= 0*/)
{
LoadPlrGFX(player, graphic);
OptionalClxSpriteList sprites;
int previewShownGameTickFragments = 0;
if (!HeadlessMode) {
sprites = player.AnimationData[static_cast<size_t>(graphic)].spritesForDirection(dir);
if (player.previewCelSprite && (*sprites)[0] == *player.previewCelSprite && !player.isWalking()) {
previewShownGameTickFragments = std::clamp<int>(AnimationInfo::baseValueFraction - player.progressToNextGameTickWhenPreviewWasSet, 0, AnimationInfo::baseValueFraction);
}
}
int8_t numberOfFrames;
int8_t ticksPerFrame;
player.getAnimationFramesAndTicksPerFrame(graphic, numberOfFrames, ticksPerFrame);
player.AnimInfo.setNewAnimation(sprites, numberOfFrames, ticksPerFrame, flags, numSkippedFrames, distributeFramesBeforeFrame, static_cast<uint8_t>(previewShownGameTickFragments));
}
void SetPlrAnims(Player &player)
{
const HeroClass pc = player._pClass;
const PlayerAnimData &plrAtkAnimData = GetPlayerAnimDataForClass(pc);
auto gn = static_cast<PlayerWeaponGraphic>(player._pgfxnum & 0xFU);
if (leveltype == DTYPE_TOWN) {
player._pNFrames = plrAtkAnimData.townIdleFrames;
player._pWFrames = plrAtkAnimData.townWalkingFrames;
} else {
player._pNFrames = plrAtkAnimData.idleFrames;
player._pWFrames = plrAtkAnimData.walkingFrames;
player._pHFrames = plrAtkAnimData.recoveryFrames;
player._pBFrames = plrAtkAnimData.blockingFrames;
switch (gn) {
case PlayerWeaponGraphic::Unarmed:
player._pAFrames = plrAtkAnimData.unarmedFrames;
player._pAFNum = plrAtkAnimData.unarmedActionFrame;
break;
case PlayerWeaponGraphic::UnarmedShield:
player._pAFrames = plrAtkAnimData.unarmedShieldFrames;
player._pAFNum = plrAtkAnimData.unarmedShieldActionFrame;
break;
case PlayerWeaponGraphic::Sword:
player._pAFrames = plrAtkAnimData.swordFrames;
player._pAFNum = plrAtkAnimData.swordActionFrame;
break;
case PlayerWeaponGraphic::SwordShield:
player._pAFrames = plrAtkAnimData.swordShieldFrames;
player._pAFNum = plrAtkAnimData.swordShieldActionFrame;
break;
case PlayerWeaponGraphic::Bow:
player._pAFrames = plrAtkAnimData.bowFrames;
player._pAFNum = plrAtkAnimData.bowActionFrame;
break;
case PlayerWeaponGraphic::Axe:
player._pAFrames = plrAtkAnimData.axeFrames;
player._pAFNum = plrAtkAnimData.axeActionFrame;
break;
case PlayerWeaponGraphic::Mace:
player._pAFrames = plrAtkAnimData.maceFrames;
player._pAFNum = plrAtkAnimData.maceActionFrame;
break;
case PlayerWeaponGraphic::MaceShield:
player._pAFrames = plrAtkAnimData.maceShieldFrames;
player._pAFNum = plrAtkAnimData.maceShieldActionFrame;
break;
case PlayerWeaponGraphic::Staff:
player._pAFrames = plrAtkAnimData.staffFrames;
player._pAFNum = plrAtkAnimData.staffActionFrame;
break;
}
}
player._pDFrames = plrAtkAnimData.deathFrames;
player._pSFrames = plrAtkAnimData.castingFrames;
player._pSFNum = plrAtkAnimData.castingActionFrame;
const int armorGraphicIndex = player._pgfxnum & ~0xFU;
if (IsAnyOf(pc, HeroClass::Warrior, HeroClass::Barbarian)) {
if (gn == PlayerWeaponGraphic::Bow && leveltype != DTYPE_TOWN)
player._pNFrames = 8;
if (armorGraphicIndex > 0)
player._pDFrames = 15;
}
}
/**
* @param player The player reference.
* @param c The hero class.
*/
void CreatePlayer(Player &player, HeroClass c)
{
player = {};
SetRndSeed(SDL_GetTicks());
player.setCharacterLevel(1);
player._pClass = c;
const ClassAttributes &attr = player.getClassAttributes();
player._pBaseStr = attr.baseStr;
player._pStrength = player._pBaseStr;
player._pBaseMag = attr.baseMag;
player._pMagic = player._pBaseMag;
player._pBaseDex = attr.baseDex;
player._pDexterity = player._pBaseDex;
player._pBaseVit = attr.baseVit;
player._pVitality = player._pBaseVit;
player._pHitPoints = player.calculateBaseLife();
player._pMaxHP = player._pHitPoints;
player._pHPBase = player._pHitPoints;
player._pMaxHPBase = player._pHitPoints;
player._pMana = player.calculateBaseMana();
player._pMaxMana = player._pMana;
player._pManaBase = player._pMana;
player._pMaxManaBase = player._pMana;
player._pExperience = 0;
player._pArmorClass = 0;
player._pLightRad = 10;
player._pInfraFlag = false;
for (uint8_t &spellLevel : player._pSplLvl) {
spellLevel = 0;
}
player._pSpellFlags = SpellFlag::None;
player._pRSplType = SpellType::Invalid;
// Initializing the hotkey bindings to no selection
std::fill(player._pSplHotKey, player._pSplHotKey + NumHotkeys, SpellID::Invalid);
// CreatePlrItems calls AutoEquip which will overwrite the player graphic if required
player._pgfxnum = static_cast<uint8_t>(PlayerWeaponGraphic::Unarmed);
for (bool &levelVisited : player._pLvlVisited) {
levelVisited = false;
}
for (int i = 0; i < 10; i++) {
player._pSLvlVisited[i] = false;
}
player._pLvlChanging = false;
player.pTownWarps = 0;
player.pLvlLoad = 0;
player.pManaShield = false;
player.pDamAcFlags = ItemSpecialEffectHf::None;
player.wReflections = 0;
InitDungMsgs(player);
CreatePlrItems(player);
SetRndSeed(0);
}
int CalcStatDiff(Player &player)
{
int diff = 0;
for (auto attribute : enum_values<CharacterAttribute>()) {
diff += player.GetMaximumAttributeValue(attribute);
diff -= player.GetBaseAttributeValue(attribute);
}
return diff;
}
void NextPlrLevel(Player &player)
{
player.setCharacterLevel(player.getCharacterLevel() + 1);
CalcPlrInv(player, true);
if (CalcStatDiff(player) < 5) {
player._pStatPts = CalcStatDiff(player);
} else {
player._pStatPts += 5;
}
const int hp = player.getClassAttributes().lvlLife;
player._pMaxHP += hp;
player._pHitPoints = player._pMaxHP;
player._pMaxHPBase += hp;
player._pHPBase = player._pMaxHPBase;
if (&player == MyPlayer) {
RedrawComponent(PanelDrawComponent::Health);
}
const int mana = player.getClassAttributes().lvlMana;
player._pMaxMana += mana;
player._pMaxManaBase += mana;
if (HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) {
player._pMana = player._pMaxMana;
player._pManaBase = player._pMaxManaBase;
}
if (&player == MyPlayer) {
RedrawComponent(PanelDrawComponent::Mana);
}
if (ControlMode != ControlTypes::KeyboardAndMouse)
FocusOnCharInfo();
CalcPlrInv(player, true);
PlaySFX(SfxID::ItemArmor);
PlaySFX(SfxID::ItemSign);
}
void Player::_addExperience(uint32_t experience, int levelDelta)
{
if (this != MyPlayer || hasNoLife())
return;
if (isMaxCharacterLevel()) {
return;
}
// Adjust xp based on difference between the players current level and the target level (usually a monster level)
uint32_t clampedExp = static_cast<uint32_t>(std::clamp<int64_t>(static_cast<int64_t>(experience * (1 + levelDelta / 10.0)), 0, std::numeric_limits<uint32_t>::max()));
// Prevent power leveling
if (gbIsMultiplayer) {
// for low level characters experience gain is capped to 1/20 of current levels xp
// for high level characters experience gain is capped to 200 * current level - this is a smaller value than 1/20 of the exp needed for the next level after level 5.
clampedExp = std::min<uint32_t>({ clampedExp, /* level 1-5: */ getNextExperienceThreshold() / 20U, /* level 6-50: */ 200U * getCharacterLevel() });
}
const uint32_t maxExperience = GetNextExperienceThresholdForLevel(getMaxCharacterLevel());
// ensure we only add enough experience to reach the max experience cap so we don't overflow
_pExperience += std::min(clampedExp, maxExperience - _pExperience);
if (*GetOptions().Gameplay.experienceBar) {
RedrawEverything();
}
// Increase player level if applicable
while (!isMaxCharacterLevel() && _pExperience >= getNextExperienceThreshold()) {
// NextPlrLevel increments character level which changes the next experience threshold
NextPlrLevel(*this);
}
NetSendCmdParam1(false, CMD_PLRLEVEL, getCharacterLevel());
}
void AddPlrMonstExper(int lvl, unsigned exp, char pmask)
{
unsigned totplrs = 0;
for (size_t i = 0; i < Players.size(); i++) {
if (((1 << i) & pmask) != 0) {
totplrs++;
}
}
if (totplrs != 0) {
const unsigned e = exp / totplrs;
if ((pmask & (1 << MyPlayerId)) != 0)
MyPlayer->addExperience(e, lvl);
}
}
void InitPlayer(Player &player, bool firstTime)
{
if (firstTime) {
player._pRSplType = SpellType::Invalid;
player._pRSpell = SpellID::Invalid;
if (&player == MyPlayer)
LoadHotkeys();
player._pSBkSpell = SpellID::Invalid;
player.queuedSpell.spellId = player._pRSpell;
player.queuedSpell.spellType = player._pRSplType;
player.pManaShield = false;
player.wReflections = 0;
}
if (player.isOnActiveLevel()) {
SetPlrAnims(player);
ClearStateVariables(player);
if (!player.hasNoLife()) {
player._pmode = PM_STAND;
NewPlrAnim(player, player_graphic::Stand, Direction::South);
player.AnimInfo.currentFrame = GenerateRnd(player._pNFrames - 1);
player.AnimInfo.tickCounterOfCurrentFrame = GenerateRnd(3);
} else {
player._pgfxnum &= ~0xFU;
player._pmode = PM_DEATH;
NewPlrAnim(player, player_graphic::Death, Direction::South);
player.AnimInfo.currentFrame = player.AnimInfo.numberOfFrames - 2;
}
player._pdir = Direction::South;
if (&player == MyPlayer && (!firstTime || leveltype != DTYPE_TOWN)) {
player.position.tile = ViewPosition;
}
SetPlayerOld(player);
player.walkpath[0] = WALK_NONE;
player.destAction = ACTION_NONE;
if (&player == MyPlayer) {
player.lightId = AddLight(player.position.tile, player._pLightRad);
ChangeLightXY(player.lightId, player.position.tile); // fix for a bug where old light is still visible at the entrance after reentering level
} else {
player.lightId = NO_LIGHT;
}
ActivateVision(player.position.tile, player._pLightRad, player.getId());
}
player._pAblSpells = GetSpellBitmask(GetPlayerStartingLoadoutForClass(player._pClass).skill);
player._pInvincible = false;
if (&player == MyPlayer) {
MyPlayerIsDead = false;
}
}
void InitMultiView()
{
assert(MyPlayer != nullptr);
ViewPosition = MyPlayer->position.tile;
}
void PlrClrTrans(Point position)
{
for (int i = position.y - 1; i <= position.y + 1; i++) {
for (int j = position.x - 1; j <= position.x + 1; j++) {
TransList[dTransVal[j][i]] = false;
}
}
}
void PlrDoTrans(Point position)
{
if (IsNoneOf(leveltype, DTYPE_CATHEDRAL, DTYPE_CATACOMBS, DTYPE_CRYPT)) {
TransList[1] = true;
return;
}
for (int i = position.y - 1; i <= position.y + 1; i++) {
for (int j = position.x - 1; j <= position.x + 1; j++) {
if (IsTileNotSolid({ j, i }) && dTransVal[j][i] != 0) {
TransList[dTransVal[j][i]] = true;
}
}
}
}
void SetPlayerOld(Player &player)
{
player.position.old = player.position.tile;
}
void FixPlayerLocation(Player &player, Direction bDir)
{
player.position.future = player.position.tile;
player._pdir = bDir;
if (&player == MyPlayer) {
ViewPosition = player.position.tile;
}
ChangeLightXY(player.lightId, player.position.tile);
ChangeVisionXY(player.getId(), player.position.tile);
}
void StartStand(Player &player, Direction dir)
{
if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) {
SyncPlrKill(player, DeathReason::Unknown);
return;
}
NewPlrAnim(player, player_graphic::Stand, dir);
player._pmode = PM_STAND;
FixPlayerLocation(player, dir);
FixPlrWalkTags(player);
player.occupyTile(player.position.tile, false);
SetPlayerOld(player);
}
void StartPlrBlock(Player &player, Direction dir)
{
if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) {
SyncPlrKill(player, DeathReason::Unknown);
return;
}
PlaySfxLoc(SfxID::ItemSword, player.position.tile);
int8_t skippedAnimationFrames = 0;
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastBlock)) {
skippedAnimationFrames = (player._pBFrames - 2); // ISPL_FASTBLOCK means we cancel the animation if frame 2 was shown
}
NewPlrAnim(player, player_graphic::Block, dir, AnimationDistributionFlags::SkipsDelayOfLastFrame, skippedAnimationFrames);
player._pmode = PM_BLOCK;
FixPlayerLocation(player, dir);
SetPlayerOld(player);
}
/**
* @todo Figure out why clearing player.position.old sometimes fails
*/
void FixPlrWalkTags(const Player &player)
{
for (int y = 0; y < MAXDUNY; y++) {
for (int x = 0; x < MAXDUNX; x++) {
if (PlayerAtPosition({ x, y }) == &player)
dPlayer[x][y] = 0;
}
}
}
void StartPlrHit(Player &player, int dam, bool forcehit)
{
if (player._pInvincible && player.hasNoLife() && &player == MyPlayer) {
SyncPlrKill(player, DeathReason::Unknown);
return;
}
player.Say(HeroSpeech::ArghClang);
RedrawComponent(PanelDrawComponent::Health);
if (player._pClass == HeroClass::Barbarian) {
if (dam >> 6 < player.getCharacterLevel() + player.getCharacterLevel() / 4 && !forcehit) {
return;
}
} else if (dam >> 6 < player.getCharacterLevel() && !forcehit) {
return;
}
const Direction pd = player._pdir;
int8_t skippedAnimationFrames = 0;
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastestHitRecovery)) {
skippedAnimationFrames = 3;
} else if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FasterHitRecovery)) {
skippedAnimationFrames = 2;
} else if (HasAnyOf(player._pIFlags, ItemSpecialEffect::FastHitRecovery)) {
skippedAnimationFrames = 1;
} else {
skippedAnimationFrames = 0;
}
NewPlrAnim(player, player_graphic::Hit, pd, AnimationDistributionFlags::None, skippedAnimationFrames);
player._pmode = PM_GOTHIT;
FixPlayerLocation(player, pd);
FixPlrWalkTags(player);
player.occupyTile(player.position.tile, false);
SetPlayerOld(player);
}
#if defined(__clang__) || defined(__GNUC__)
__attribute__((no_sanitize("shift-base")))
#endif
void
StartPlayerKill(Player &player, DeathReason deathReason)
{
if (player.hasNoLife() && player._pmode == PM_DEATH) {
return;
}
if (&player == MyPlayer) {
NetSendCmdParam1(true, CMD_PLRDEAD, static_cast<uint16_t>(deathReason));
gamemenu_off();
}
const bool dropGold = !gbIsMultiplayer || !(player.isOnLevel(16) || player.isOnArenaLevel());
const bool dropItems = dropGold && deathReason == DeathReason::MonsterOrTrap;
const bool dropEar = dropGold && deathReason == DeathReason::Player;
player.Say(HeroSpeech::AuughUh);
// Are the current animations item dependent?
if (player._pgfxnum != 0) {
if (dropItems) {
// Ensure death animation show the player without weapon and armor, because they drop on death
player._pgfxnum = 0;
} else {
// Death animation aren't weapon specific, so always use the unarmed animations
player._pgfxnum &= ~0xFU;
}
ResetPlayerGFX(player);
SetPlrAnims(player);
}
NewPlrAnim(player, player_graphic::Death, player._pdir);
player._pBlockFlag = false;
player._pmode = PM_DEATH;
player._pInvincible = true;
SetPlayerHitPoints(player, 0);
if (&player != MyPlayer && dropItems) {
// Ensure that items are removed for remote players
// The dropped items will be synced separately (by the remote client)
for (Item &item : player.InvBody) {
item.clear();
}
CalcPlrInv(player, false);
}
if (player.isOnActiveLevel()) {
FixPlayerLocation(player, player._pdir);
FixPlrWalkTags(player);
dFlags[player.position.tile.x][player.position.tile.y] |= DungeonFlag::DeadPlayer;
SetPlayerOld(player);
// Only generate drops once (for the local player)
// For remote players we get separated sync messages (by the remote client)
if (&player == MyPlayer) {
RedrawComponent(PanelDrawComponent::Health);
if (!player.HoldItem.isEmpty()) {
DeadItem(player, std::move(player.HoldItem), { 0, 0 });
NewCursor(CURSOR_HAND);
}
if (dropGold) {
DropHalfPlayersGold(player);
}
if (dropEar) {
Item ear;
InitializeItem(ear, IDI_EAR);
CopyUtf8(ear._iName, fmt::format(fmt::runtime("Ear of {:s}"), player._pName), sizeof(ear._iName));
CopyUtf8(ear._iIName, player._pName, ItemNameLength);
switch (player._pClass) {
case HeroClass::Sorcerer:
ear._iCurs = ICURS_EAR_SORCERER;
break;
case HeroClass::Warrior:
ear._iCurs = ICURS_EAR_WARRIOR;
break;
case HeroClass::Rogue:
case HeroClass::Monk:
case HeroClass::Bard:
case HeroClass::Barbarian:
ear._iCurs = ICURS_EAR_ROGUE;
break;
default:
break;
}
ear._iCreateInfo = player._pName[0] << 8 | player._pName[1];
ear._iSeed = player._pName[2] << 24 | player._pName[3] << 16 | player._pName[4] << 8 | player._pName[5];
ear._ivalue = player.getCharacterLevel();
if (FindGetItem(ear._iSeed, IDI_EAR, ear._iCreateInfo) == -1) {
DeadItem(player, std::move(ear), { 0, 0 });
}
}
if (dropItems) {
Direction pdd = player._pdir;
for (Item &item : player.InvBody) {
pdd = Left(pdd);
DeadItem(player, item.pop(), Displacement(pdd));
}
CalcPlrInv(player, false);
}
}
}
SetPlayerHitPoints(player, 0);
}
void StripTopGold(Player &player)
{
for (Item &item : InventoryPlayerItemsRange { player }) {
if (item._itype != ItemType::Gold)
continue;
if (item._ivalue <= MaxGold)
continue;
Item excessGold;
MakeGoldStack(excessGold, item._ivalue - MaxGold);
item._ivalue = MaxGold;
if (GoldAutoPlace(player, excessGold))
continue;
if (!player.HoldItem.isEmpty() && ActiveItemCount + 1 >= MAXITEMS)
continue;
DeadItem(player, std::move(excessGold), { 0, 0 });
}
player._pGold = CalculateGold(player);
if (player.HoldItem.isEmpty())
return;
if (AutoEquip(player, player.HoldItem, false))
return;
if (CanFitItemInInventory(player, player.HoldItem))
return;
if (AutoPlaceItemInBelt(player, player.HoldItem))
return;
const std::optional<Point> itemTile = FindAdjacentPositionForItem(player.position.tile, player._pdir);
if (itemTile)
return;
DeadItem(player, std::move(player.HoldItem), { 0, 0 });
NewCursor(CURSOR_HAND);
}
void ApplyPlrDamage(DamageType damageType, Player &player, int dam, int minHP /*= 0*/, int frac /*= 0*/, DeathReason deathReason /*= DeathReason::MonsterOrTrap*/)
{
int totalDamage = (dam << 6) + frac;
if (&player == MyPlayer && !player.hasNoLife()) {
AddFloatingNumber(damageType, player, totalDamage);
}
if (totalDamage > 0 && player.pManaShield && HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) {
const uint8_t manaShieldLevel = player._pSplLvl[static_cast<int8_t>(SpellID::ManaShield)];
if (manaShieldLevel > 0) {
totalDamage += totalDamage / -player.GetManaShieldDamageReduction();
}
if (&player == MyPlayer)
RedrawComponent(PanelDrawComponent::Mana);
if (player._pMana >= totalDamage) {
player._pMana -= totalDamage;
player._pManaBase -= totalDamage;
totalDamage = 0;
} else {
totalDamage -= player._pMana;
if (manaShieldLevel > 0) {
totalDamage += totalDamage / (player.GetManaShieldDamageReduction() - 1);
}
player._pMana = 0;
player._pManaBase = player._pMaxManaBase - player._pMaxMana;
if (&player == MyPlayer)
NetSendCmd(true, CMD_REMSHIELD);
}
}
if (totalDamage == 0)
return;
RedrawComponent(PanelDrawComponent::Health);
player._pHitPoints -= totalDamage;
player._pHPBase -= totalDamage;
if (player._pHitPoints > player._pMaxHP) {
player._pHitPoints = player._pMaxHP;
player._pHPBase = player._pMaxHPBase;
}
const int minHitPoints = minHP << 6;
if (player._pHitPoints < minHitPoints) {
SetPlayerHitPoints(player, minHitPoints);
}
if (player.hasNoLife()) {
SyncPlrKill(player, deathReason);
}
}
void SyncPlrKill(Player &player, DeathReason deathReason)
{
SetPlayerHitPoints(player, 0);
StartPlayerKill(player, deathReason);
}
void RemovePlrMissiles(const Player &player)
{
if (leveltype != DTYPE_TOWN) {
Monster *golem;
while ((golem = FindGolemForPlayer(player)) != nullptr) {
KillGolem(*golem);
}
}
for (auto &missile : Missiles) {
if (missile._mitype == MissileID::StoneCurse && &Players[missile._misource] == &player) {
Monsters[missile.var2].mode = static_cast<MonsterMode>(missile.var1);
}
}
}
#if defined(__clang__) || defined(__GNUC__)
__attribute__((no_sanitize("shift-base")))
#endif
void
StartNewLvl(Player &player, interface_mode fom, int lvl)
{
InitLevelChange(player);
switch (fom) {
case WM_DIABNEXTLVL:
case WM_DIABPREVLVL:
case WM_DIABRTNLVL:
case WM_DIABTOWNWARP:
player.setLevel(lvl);
break;
case WM_DIABSETLVL:
if (&player == MyPlayer)
setlvlnum = (_setlevels)lvl;
player.setLevel(setlvlnum);
break;
case WM_DIABTWARPUP:
MyPlayer->pTownWarps |= 1 << (leveltype - 2);
player.setLevel(lvl);
break;
case WM_DIABRETOWN:
break;
default:
app_fatal("StartNewLvl");
}
if (&player == MyPlayer) {
player._pmode = PM_NEWLVL;
player._pInvincible = true;
SDL_Event event;
CustomEventToSdlEvent(event, fom);
SDL_PushEvent(&event);
if (gbIsMultiplayer) {
NetSendCmdParam2(true, CMD_NEWLVL, fom, lvl);
}
}
}
void RestartTownLvl(Player &player)
{
InitLevelChange(player);
player.setLevel(0);
player._pInvincible = false;
SetPlayerHitPoints(player, 64);
player._pMana = 0;
player._pManaBase = player._pMana - (player._pMaxMana - player._pMaxManaBase);
CalcPlrInv(player, false);
player._pmode = PM_NEWLVL;
if (&player == MyPlayer) {
player._pInvincible = true;
SDL_Event event;
CustomEventToSdlEvent(event, WM_DIABRETOWN);
SDL_PushEvent(&event);
}
}
void StartWarpLvl(Player &player, size_t pidx)
{
InitLevelChange(player);
if (gbIsMultiplayer) {
if (!player.isOnLevel(0)) {
player.setLevel(0);
} else {
if (Portals[pidx].setlvl)
player.setLevel(static_cast<_setlevels>(Portals[pidx].level));
else
player.setLevel(Portals[pidx].level);
}
}
if (&player == MyPlayer) {
SetCurrentPortal(pidx);
player._pmode = PM_NEWLVL;
player._pInvincible = true;
SDL_Event event;
CustomEventToSdlEvent(event, WM_DIABWARPLVL);
SDL_PushEvent(&event);
}
}
void ProcessPlayers()
{
assert(MyPlayer != nullptr);
Player &myPlayer = *MyPlayer;
if (myPlayer.pLvlLoad > 0) {
myPlayer.pLvlLoad--;
}
if (sfxdelay > 0) {
sfxdelay--;
if (sfxdelay == 0) {
switch (sfxdnum) {
case SfxID::Defiler1:
InitQTextMsg(TEXT_DEFILER1);
break;
case SfxID::Defiler2:
InitQTextMsg(TEXT_DEFILER2);
break;
case SfxID::Defiler3:
InitQTextMsg(TEXT_DEFILER3);
break;
case SfxID::Defiler4:
InitQTextMsg(TEXT_DEFILER4);
break;
default:
5 years ago
PlaySFX(sfxdnum);
}
}
}
ValidatePlayer();
for (size_t pnum = 0; pnum < Players.size(); pnum++) {
Player &player = Players[pnum];
if (player.plractive && player.isOnActiveLevel() && (&player == MyPlayer || !player._pLvlChanging)) {
if (!PlrDeathModeOK(player) && player.hasNoLife()) {
SyncPlrKill(player, DeathReason::Unknown);
}
if (&player == MyPlayer) {
if (HasAnyOf(player._pIFlags, ItemSpecialEffect::DrainLife) && leveltype != DTYPE_TOWN) {
ApplyPlrDamage(DamageType::Physical, player, 0, 0, 4);
}
if (player.pManaShield && HasAnyOf(player._pIFlags, ItemSpecialEffect::NoMana)) {
NetSendCmd(true, CMD_REMSHIELD);
}
}
bool tplayer = false;
do {
switch (player._pmode) {
case PM_STAND:
case PM_NEWLVL:
case PM_QUIT:
tplayer = false;
break;
case PM_WALK_NORTHWARDS:
case PM_WALK_SOUTHWARDS:
case PM_WALK_SIDEWAYS:
tplayer = DoWalk(player);
break;
case PM_ATTACK:
tplayer = DoAttack(player);
break;
case PM_RATTACK:
tplayer = DoRangeAttack(player);
break;
case PM_BLOCK:
tplayer = DoBlock(player);
break;
case PM_SPELL:
tplayer = DoSpell(player);
break;
case PM_GOTHIT:
tplayer = DoGotHit(player);
break;
case PM_DEATH:
tplayer = DoDeath(player);
break;
}
CheckNewPath(player, tplayer);
} while (tplayer);
player.previewCelSprite = std::nullopt;
if (player._pmode != PM_DEATH || player.AnimInfo.tickCounterOfCurrentFrame != 40)
player.AnimInfo.processAnimation();
}
}
}
void ClrPlrPath(Player &player)
{
memset(player.walkpath, WALK_NONE, sizeof(player.walkpath));
}
/**
* @brief Determines if the target position is clear for the given player to stand on.
*
* This requires an ID instead of a Player& to compare with the dPlayer lookup table values.
*
* @param player The player to check.
* @param position Dungeon tile coordinates.
* @return False if something (other than the player themselves) is blocking the tile.
*/
bool PosOkPlayer(const Player &player, Point position)
{
if (!InDungeonBounds(position))
return false;
if (!IsTileWalkable(position))
return false;
Player *otherPlayer = PlayerAtPosition(position);
if (otherPlayer != nullptr && otherPlayer != &player && !otherPlayer->hasNoLife())
return false;
if (dMonster[position.x][position.y] != 0) {
if (leveltype == DTYPE_TOWN) {
return false;
}
if (dMonster[position.x][position.y] <= 0) {
return false;
}
if (!Monsters[dMonster[position.x][position.y] - 1].hasNoLife()) {
return false;
}
}
return true;
}
void MakePlrPath(Player &player, Point targetPosition, bool endspace)
{
if (player.position.future == targetPosition) {
return;
}
int path = FindPath(CanStep, [&player](Point position) { return PosOkPlayer(player, position); }, player.position.future, targetPosition, player.walkpath, MaxPathLengthPlayer);
if (path == 0) {
return;
}
if (!endspace) {
path--;
}
player.walkpath[path] = WALK_NONE;
}
void CheckPlrSpell(bool isShiftHeld, SpellID spellID, SpellType spellType)
{
bool addflag = false;
assert(MyPlayer != nullptr);
Player &myPlayer = *MyPlayer;
if (!IsValidSpell(spellID)) {
myPlayer.Say(HeroSpeech::IDontHaveASpellReady);
return;
}
if (ControlMode == ControlTypes::KeyboardAndMouse) {
if (pcurs != CURSOR_HAND)
return;
if (GetMainPanel().contains(MousePosition)) // inside main panel
return;
if (
(IsLeftPanelOpen() && GetLeftPanel().contains(MousePosition)) // inside left panel
|| (IsRightPanelOpen() && GetRightPanel().contains(MousePosition)) // inside right panel
) {
if (spellID != SpellID::Healing
&& spellID != SpellID::Identify
&& spellID != SpellID::ItemRepair
&& spellID != SpellID::Infravision
&& spellID != SpellID::StaffRecharge)
return;
}
}
if (leveltype == DTYPE_TOWN && !GetSpellData(spellID).isAllowedInTown()) {
myPlayer.Say(HeroSpeech::ICantCastThatHere);
return;
}
SpellCheckResult spellcheck = SpellCheckResult::Success;
4 years ago
switch (spellType) {
case SpellType::Skill:
case SpellType::Spell:
spellcheck = CheckSpell(*MyPlayer, spellID, spellType, false);
addflag = spellcheck == SpellCheckResult::Success;
break;
case SpellType::Scroll:
addflag = pcurs == CURSOR_HAND && CanUseScroll(myPlayer, spellID);
break;
case SpellType::Charges:
addflag = pcurs == CURSOR_HAND && CanUseStaff(myPlayer, spellID);
break;
case SpellType::Invalid:
return;
}
if (!addflag) {
if (spellType == SpellType::Spell) {
switch (spellcheck) {
case SpellCheckResult::Fail_NoMana:
myPlayer.Say(HeroSpeech::NotEnoughMana);
break;
case SpellCheckResult::Fail_Level0:
myPlayer.Say(HeroSpeech::ICantCastThatYet);
break;
default:
myPlayer.Say(HeroSpeech::ICantDoThat);
break;
}
LastPlayerAction = PlayerActionType::None;
}
return;
}
const int spellFrom = 0;
if (IsWallSpell(spellID)) {
LastPlayerAction = PlayerActionType::Spell;
const Direction sd = GetDirection(myPlayer.position.tile, cursPosition);
NetSendCmdLocParam4(true, CMD_SPELLXYD, cursPosition, static_cast<int8_t>(spellID), static_cast<uint8_t>(spellType), static_cast<uint16_t>(sd), spellFrom);
} else if (pcursmonst != -1 && !isShiftHeld) {
LastPlayerAction = PlayerActionType::SpellMonsterTarget;
NetSendCmdParam4(true, CMD_SPELLID, pcursmonst, static_cast<int8_t>(spellID), static_cast<uint8_t>(spellType), spellFrom);
} else if (PlayerUnderCursor != nullptr && !isShiftHeld && !myPlayer.friendlyMode) {
LastPlayerAction = PlayerActionType::SpellPlayerTarget;
NetSendCmdParam4(true, CMD_SPELLPID, PlayerUnderCursor->getId(), static_cast<int8_t>(spellID), static_cast<uint8_t>(spellType), spellFrom);
} else {
Point targetedTile = cursPosition;
if (spellID == SpellID::Teleport && myPlayer.executedSpell.spellId == SpellID::Teleport) {
// Check if the player is attempting to queue Teleport onto a tile that is currently being targeted with Teleport, or a nearby tile
if (cursPosition.WalkingDistance(myPlayer.position.temp) <= 1) {
// Get the relative displacement from the player's current position to the cursor position
const WorldTileDisplacement relativeMove = cursPosition - static_cast<Point>(myPlayer.position.tile);
// Target the tile the relative distance away from the player's targeted Teleport tile
targetedTile = myPlayer.position.temp + relativeMove;
}
}
LastPlayerAction = PlayerActionType::Spell;
NetSendCmdLocParam3(true, CMD_SPELLXY, targetedTile, static_cast<int8_t>(spellID), static_cast<uint8_t>(spellType), spellFrom);
}
}
void SyncPlrAnim(Player &player)
{
const player_graphic graphic = player.getGraphic();
if (!HeadlessMode)
player.AnimInfo.sprites = player.AnimationData[static_cast<size_t>(graphic)].spritesForDirection(player._pdir);
}
void SyncInitPlrPos(Player &player)
{
if (!player.isOnActiveLevel())
return;
const WorldTileDisplacement offset[9] = { { 0, 0 }, { 1, 0 }, { 0, 1 }, { 1, 1 }, { 2, 0 }, { 0, 2 }, { 1, 2 }, { 2, 1 }, { 2, 2 } };
const Point position = [&]() {
for (int i = 0; i < 8; i++) {
Point position = player.position.tile + offset[i];
if (PosOkPlayer(player, position))
return position;
}
const std::optional<Point> nearPosition = FindClosestValidPosition(
[&player](Point testPosition) {
for (int i = 0; i < numtrigs; i++) {
if (trigs[i].position == testPosition)
return false;
}
return PosOkPlayer(player, testPosition) && !PosOkPortal(currlevel, testPosition);
},
player.position.tile,
1, // skip the starting tile since that was checked in the previous loop
50);
return nearPosition.value_or(Point { 0, 0 });
}();
player.position.tile = position;
player.occupyTile(position, false);
player.position.future = position;
if (&player == MyPlayer) {
ViewPosition = position;
}
}
void SyncInitPlr(Player &player)
{
SetPlrAnims(player);
SyncInitPlrPos(player);
if (&player != MyPlayer)
player.lightId = NO_LIGHT;
}
void CheckStats(Player &player)
{
for (auto attribute : enum_values<CharacterAttribute>()) {
const int maxStatPoint = player.GetMaximumAttributeValue(attribute);
switch (attribute) {
case CharacterAttribute::Strength:
player._pBaseStr = std::clamp(player._pBaseStr, 0, maxStatPoint);
break;
case CharacterAttribute::Magic:
player._pBaseMag = std::clamp(player._pBaseMag, 0, maxStatPoint);
break;
case CharacterAttribute::Dexterity:
player._pBaseDex = std::clamp(player._pBaseDex, 0, maxStatPoint);
break;
case CharacterAttribute::Vitality:
player._pBaseVit = std::clamp(player._pBaseVit, 0, maxStatPoint);
break;
}
}
}
void ModifyPlrStr(Player &player, int l)
{
l = std::clamp(l, 0 - player._pBaseStr, player.GetMaximumAttributeValue(CharacterAttribute::Strength) - player._pBaseStr);
player._pStrength += l;
player._pBaseStr += l;
CalcPlrInv(player, true);
if (&player == MyPlayer) {
NetSendCmdParam1(false, CMD_SETSTR, player._pBaseStr);
}
}
void ModifyPlrMag(Player &player, int l)
{
l = std::clamp(l, 0 - player._pBaseMag, player.GetMaximumAttributeValue(CharacterAttribute::Magic) - player._pBaseMag);
player._pMagic += l;
player._pBaseMag += l;
int ms = l;
ms *= player.getClassAttributes().chrMana;
player._pMaxManaBase += ms;
player._pMaxMana += ms;
if (HasNoneOf(player._pIFlags, ItemSpecialEffect::NoMana)) {
player._pManaBase += ms;
player._pMana += ms;
}
CalcPlrInv(player, true);
if (&player == MyPlayer) {
NetSendCmdParam1(false, CMD_SETMAG, player._pBaseMag);
}
}
void ModifyPlrDex(Player &player, int l)
{
l = std::clamp(l, 0 - player._pBaseDex, player.GetMaximumAttributeValue(CharacterAttribute::Dexterity) - player._pBaseDex);
player._pDexterity += l;
player._pBaseDex += l;
CalcPlrInv(player, true);
if (&player == MyPlayer) {
NetSendCmdParam1(false, CMD_SETDEX, player._pBaseDex);
}
}
void ModifyPlrVit(Player &player, int l)
{
l = std::clamp(l, 0 - player._pBaseVit, player.GetMaximumAttributeValue(CharacterAttribute::Vitality) - player._pBaseVit);
player._pVitality += l;
player._pBaseVit += l;
int ms = l;
ms *= player.getClassAttributes().chrLife;
player._pHPBase += ms;
player._pMaxHPBase += ms;
player._pHitPoints += ms;
player._pMaxHP += ms;
CalcPlrInv(player, true);
if (&player == MyPlayer) {
NetSendCmdParam1(false, CMD_SETVIT, player._pBaseVit);
}
}
void SetPlayerHitPoints(Player &player, int val)
{
player._pHitPoints = val;
player._pHPBase = val + player._pMaxHPBase - player._pMaxHP;
if (&player == MyPlayer) {
RedrawComponent(PanelDrawComponent::Health);
}
}
void SetPlrStr(Player &player, int v)
{
player._pBaseStr = v;
CalcPlrInv(player, true);
}
void SetPlrMag(Player &player, int v)
{
player._pBaseMag = v;
int m = v;
m *= player.getClassAttributes().chrMana;
player._pMaxManaBase = m;
player._pMaxMana = m;
CalcPlrInv(player, true);
}
void SetPlrDex(Player &player, int v)
{
player._pBaseDex = v;
CalcPlrInv(player, true);
}
void SetPlrVit(Player &player, int v)
{
player._pBaseVit = v;
int hp = v;
hp *= player.getClassAttributes().chrLife;
player._pHPBase = hp;
player._pMaxHPBase = hp;
CalcPlrInv(player, true);
}
void InitDungMsgs(Player &player)
{
player.pDungMsgs = 0;
player.pDungMsgs2 = 0;
}
enum {
// clang-format off
DungMsgCathedral = 1 << 0,
DungMsgCatacombs = 1 << 1,
DungMsgCaves = 1 << 2,
DungMsgHell = 1 << 3,
DungMsgDiablo = 1 << 4,
// clang-format on
};
void PlayDungMsgs()
{
assert(MyPlayer != nullptr);
Player &myPlayer = *MyPlayer;
if (!setlevel && currlevel == 1 && !myPlayer._pLvlVisited[1] && (myPlayer.pDungMsgs & DungMsgCathedral) == 0) {
myPlayer.Say(HeroSpeech::TheSanctityOfThisPlaceHasBeenFouled, 40);
myPlayer.pDungMsgs = myPlayer.pDungMsgs | DungMsgCathedral;
} else if (!setlevel && currlevel == 5 && !myPlayer._pLvlVisited[5] && (myPlayer.pDungMsgs & DungMsgCatacombs) == 0) {
myPlayer.Say(HeroSpeech::TheSmellOfDeathSurroundsMe, 40);
myPlayer.pDungMsgs |= DungMsgCatacombs;
} else if (!setlevel && currlevel == 9 && !myPlayer._pLvlVisited[9] && (myPlayer.pDungMsgs & DungMsgCaves) == 0) {
myPlayer.Say(HeroSpeech::ItsHotDownHere, 40);
myPlayer.pDungMsgs |= DungMsgCaves;
} else if (!setlevel && currlevel == 13 && !myPlayer._pLvlVisited[13] && (myPlayer.pDungMsgs & DungMsgHell) == 0) {
myPlayer.Say(HeroSpeech::IMustBeGettingClose, 40);
myPlayer.pDungMsgs |= DungMsgHell;
} else if (!setlevel && currlevel == 16 && !myPlayer._pLvlVisited[16] && (myPlayer.pDungMsgs & DungMsgDiablo) == 0) {
for (auto &monster : Monsters) {
if (monster.type().type != MT_DIABLO) continue;
if (monster.hitPoints > 0) {
sfxdelay = 40;
sfxdnum = SfxID::DiabloGreeting;
myPlayer.pDungMsgs |= DungMsgDiablo;
}
break;
}
} else if (!setlevel && currlevel == 17 && !myPlayer._pLvlVisited[17] && (myPlayer.pDungMsgs2 & 1) == 0) {
sfxdelay = 10;
sfxdnum = SfxID::Defiler1;
Quests[Q_DEFILER]._qactive = QUEST_ACTIVE;
Quests[Q_DEFILER]._qlog = true;
Quests[Q_DEFILER]._qmsg = TEXT_DEFILER1;
NetSendCmdQuest(true, Quests[Q_DEFILER]);
myPlayer.pDungMsgs2 |= 1;
} else if (!setlevel && currlevel == 19 && !myPlayer._pLvlVisited[19] && (myPlayer.pDungMsgs2 & 4) == 0) {
sfxdelay = 10;
sfxdnum = SfxID::Defiler3;
myPlayer.pDungMsgs2 |= 4;
} else if (!setlevel && currlevel == 21 && !myPlayer._pLvlVisited[21] && (myPlayer.pDungMsgs & 32) == 0) {
myPlayer.Say(HeroSpeech::ThisIsAPlaceOfGreatPower, 30);
myPlayer.pDungMsgs |= 32;
} else if (setlevel && setlvlnum == SL_SKELKING && !gbIsSpawn && !myPlayer._pSLvlVisited[SL_SKELKING] && Quests[Q_SKELKING]._qactive == QUEST_ACTIVE) {
sfxdelay = 10;
sfxdnum = SfxID::LeoricGreeting;
} else {
sfxdelay = 0;
}
}
#ifdef BUILD_TESTING
bool TestPlayerDoGotHit(Player &player)
{
return DoGotHit(player);
}
#endif
} // namespace devilution