mirror of
https://github.com/libretro/devilutionX.git
synced 2026-11-11 02:41:35 -10:00
When rendering directly to the output buffer, we need to maintain the state of what has been drawn and what needs redrawing per-buffer. We previously tried to do it implicitly by checking `SDL_DOUBLEBUF` and other flags. The previous implementation was broken in several ways, resulting in rendering issues on devices that support 8-bit output directly. Changes this mechanism to explicitly maintain buffer state per output buffer. The new mechanism doesn't require knowledge of the number of buffers, and thus also works correctly with triple-buffering. Fixes #5447
390 lines
8.8 KiB
C++
390 lines
8.8 KiB
C++
/**
|
|
* @file spells.cpp
|
|
*
|
|
* Implementation of functionality for casting player spells.
|
|
*/
|
|
#include "spells.h"
|
|
|
|
#include "control.h"
|
|
#include "cursor.h"
|
|
#ifdef _DEBUG
|
|
#include "debug.h"
|
|
#endif
|
|
#include "engine/backbuffer_state.hpp"
|
|
#include "engine/point.hpp"
|
|
#include "engine/random.hpp"
|
|
#include "gamemenu.h"
|
|
#include "inv.h"
|
|
#include "missiles.h"
|
|
|
|
namespace devilution {
|
|
|
|
namespace {
|
|
|
|
/**
|
|
* @brief Gets a value indicating whether the player's current readied spell is a valid spell. Readied spells can be
|
|
* invalidaded in a few scenarios where the spell comes from items, for example (like dropping the only scroll that
|
|
* provided the spell).
|
|
* @param player The player whose readied spell is to be checked.
|
|
* @return 'true' when the readied spell is currently valid, and 'false' otherwise.
|
|
*/
|
|
bool IsReadiedSpellValid(const Player &player)
|
|
{
|
|
switch (player._pRSplType) {
|
|
case RSPLTYPE_SKILL:
|
|
case RSPLTYPE_SPELL:
|
|
case RSPLTYPE_INVALID:
|
|
return true;
|
|
|
|
case RSPLTYPE_CHARGES:
|
|
return (player._pISpells & GetSpellBitmask(player._pRSpell)) != 0;
|
|
|
|
case RSPLTYPE_SCROLL:
|
|
return (player._pScrlSpells & GetSpellBitmask(player._pRSpell)) != 0;
|
|
|
|
default:
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @brief Clears the current player's readied spell selection.
|
|
* @note Will force a UI redraw in case the values actually change, so that the new spell reflects on the bottom panel.
|
|
* @param player The player whose readied spell is to be cleared.
|
|
*/
|
|
void ClearReadiedSpell(Player &player)
|
|
{
|
|
if (player._pRSpell != SPL_INVALID) {
|
|
player._pRSpell = SPL_INVALID;
|
|
RedrawEverything();
|
|
}
|
|
|
|
if (player._pRSplType != RSPLTYPE_INVALID) {
|
|
player._pRSplType = RSPLTYPE_INVALID;
|
|
RedrawEverything();
|
|
}
|
|
}
|
|
|
|
void PlacePlayer(Player &player)
|
|
{
|
|
if (!player.isOnActiveLevel())
|
|
return;
|
|
|
|
Point newPosition = [&]() {
|
|
Point okPosition = {};
|
|
|
|
for (int i = 0; i < 8; i++) {
|
|
okPosition = player.position.tile + Displacement { plrxoff2[i], plryoff2[i] };
|
|
if (PosOkPlayer(player, okPosition))
|
|
return okPosition;
|
|
}
|
|
|
|
for (int max = 1, min = -1; min > -50; max++, min--) {
|
|
for (int y = min; y <= max; y++) {
|
|
okPosition.y = player.position.tile.y + y;
|
|
|
|
for (int x = min; x <= max; x++) {
|
|
okPosition.x = player.position.tile.x + x;
|
|
|
|
if (PosOkPlayer(player, okPosition))
|
|
return okPosition;
|
|
}
|
|
}
|
|
}
|
|
|
|
return okPosition;
|
|
}();
|
|
|
|
player.position.tile = newPosition;
|
|
|
|
dPlayer[newPosition.x][newPosition.y] = player.getId() + 1;
|
|
|
|
if (&player == MyPlayer) {
|
|
ViewPosition = newPosition;
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
bool IsValidSpell(spell_id spl)
|
|
{
|
|
return spl > SPL_NULL
|
|
&& spl <= SPL_LAST
|
|
&& (spl <= SPL_LASTDIABLO || gbIsHellfire);
|
|
}
|
|
|
|
bool IsWallSpell(spell_id spl)
|
|
{
|
|
return spl == SPL_FIREWALL || spl == SPL_LIGHTWALL;
|
|
}
|
|
|
|
bool TargetsMonster(spell_id id)
|
|
{
|
|
return id == SPL_FIREBALL
|
|
|| id == SPL_FIREWALL
|
|
|| id == SPL_FLAME
|
|
|| id == SPL_LIGHTNING
|
|
|| id == SPL_STONE
|
|
|| id == SPL_WAVE;
|
|
}
|
|
|
|
int GetManaAmount(const Player &player, spell_id sn)
|
|
{
|
|
int ma; // mana amount
|
|
|
|
// mana adjust
|
|
int adj = 0;
|
|
|
|
// spell level
|
|
int sl = std::max(player.GetSpellLevel(sn) - 1, 0);
|
|
|
|
if (sl > 0) {
|
|
adj = sl * spelldata[sn].sManaAdj;
|
|
}
|
|
if (sn == SPL_FIREBOLT) {
|
|
adj /= 2;
|
|
}
|
|
if (sn == SPL_RESURRECT && sl > 0) {
|
|
adj = sl * (spelldata[SPL_RESURRECT].sManaCost / 8);
|
|
}
|
|
|
|
if (sn == SPL_HEAL || sn == SPL_HEALOTHER) {
|
|
ma = (spelldata[SPL_HEAL].sManaCost + 2 * player._pLevel - adj);
|
|
} else if (spelldata[sn].sManaCost == 255) {
|
|
ma = (player._pMaxManaBase >> 6) - adj;
|
|
} else {
|
|
ma = (spelldata[sn].sManaCost - adj);
|
|
}
|
|
|
|
ma = std::max(ma, 0);
|
|
ma <<= 6;
|
|
|
|
if (gbIsHellfire && player._pClass == HeroClass::Sorcerer) {
|
|
ma /= 2;
|
|
} else if (player._pClass == HeroClass::Rogue || player._pClass == HeroClass::Monk || player._pClass == HeroClass::Bard) {
|
|
ma -= ma / 4;
|
|
}
|
|
|
|
if (spelldata[sn].sMinMana > ma >> 6) {
|
|
ma = spelldata[sn].sMinMana << 6;
|
|
}
|
|
|
|
return ma;
|
|
}
|
|
|
|
void ConsumeSpell(Player &player, spell_id sn)
|
|
{
|
|
switch (player.executedSpell.spellType) {
|
|
case RSPLTYPE_SKILL:
|
|
case RSPLTYPE_INVALID:
|
|
break;
|
|
case RSPLTYPE_SCROLL:
|
|
ConsumeScroll(player);
|
|
break;
|
|
case RSPLTYPE_CHARGES:
|
|
ConsumeStaffCharge(player);
|
|
break;
|
|
case RSPLTYPE_SPELL:
|
|
#ifdef _DEBUG
|
|
if (DebugGodMode)
|
|
break;
|
|
#endif
|
|
int ma = GetManaAmount(player, sn);
|
|
player._pMana -= ma;
|
|
player._pManaBase -= ma;
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
break;
|
|
}
|
|
if (sn == SPL_FLARE) {
|
|
ApplyPlrDamage(player, 5);
|
|
}
|
|
if (sn == SPL_BONESPIRIT) {
|
|
ApplyPlrDamage(player, 6);
|
|
}
|
|
}
|
|
|
|
void EnsureValidReadiedSpell(Player &player)
|
|
{
|
|
if (!IsReadiedSpellValid(player)) {
|
|
ClearReadiedSpell(player);
|
|
}
|
|
}
|
|
|
|
SpellCheckResult CheckSpell(const Player &player, spell_id sn, spell_type st, bool manaonly)
|
|
{
|
|
#ifdef _DEBUG
|
|
if (DebugGodMode)
|
|
return SpellCheckResult::Success;
|
|
#endif
|
|
|
|
if (!manaonly && pcurs != CURSOR_HAND) {
|
|
return SpellCheckResult::Fail_Busy;
|
|
}
|
|
|
|
if (st == RSPLTYPE_SKILL) {
|
|
return SpellCheckResult::Success;
|
|
}
|
|
|
|
if (player.GetSpellLevel(sn) <= 0) {
|
|
return SpellCheckResult::Fail_Level0;
|
|
}
|
|
|
|
if (player._pMana < GetManaAmount(player, sn)) {
|
|
return SpellCheckResult::Fail_NoMana;
|
|
}
|
|
|
|
return SpellCheckResult::Success;
|
|
}
|
|
|
|
void CastSpell(int id, spell_id spl, int sx, int sy, int dx, int dy, int spllvl)
|
|
{
|
|
Player &player = Players[id];
|
|
Direction dir = player._pdir;
|
|
if (IsWallSpell(spl)) {
|
|
dir = player.tempDirection;
|
|
}
|
|
|
|
bool fizzled = false;
|
|
for (int i = 0; i < 3 && spelldata[spl].sMissiles[i] != MIS_NULL; i++) {
|
|
Missile *missile = AddMissile({ sx, sy }, { dx, dy }, dir, spelldata[spl].sMissiles[i], TARGET_MONSTERS, id, 0, spllvl);
|
|
fizzled |= (missile == nullptr);
|
|
}
|
|
if (spl == SPL_CBOLT) {
|
|
for (int i = (spllvl / 2) + 3; i > 0; i--) {
|
|
Missile *missile = AddMissile({ sx, sy }, { dx, dy }, dir, MIS_CBOLT, TARGET_MONSTERS, id, 0, spllvl);
|
|
fizzled |= (missile == nullptr);
|
|
}
|
|
}
|
|
if (!fizzled) {
|
|
ConsumeSpell(player, spl);
|
|
}
|
|
}
|
|
|
|
void DoResurrect(size_t pnum, Player &target)
|
|
{
|
|
if (pnum >= Players.size()) {
|
|
return;
|
|
}
|
|
|
|
AddMissile(target.position.tile, target.position.tile, Direction::South, MIS_RESURRECTBEAM, TARGET_MONSTERS, pnum, 0, 0);
|
|
|
|
if (target._pHitPoints != 0)
|
|
return;
|
|
|
|
if (&target == MyPlayer) {
|
|
MyPlayerIsDead = false;
|
|
gamemenu_off();
|
|
RedrawComponent(PanelDrawComponent::Health);
|
|
RedrawComponent(PanelDrawComponent::Mana);
|
|
}
|
|
|
|
ClrPlrPath(target);
|
|
target.destAction = ACTION_NONE;
|
|
target._pInvincible = false;
|
|
PlacePlayer(target);
|
|
|
|
int hp = 10 << 6;
|
|
if (target._pMaxHPBase < (10 << 6)) {
|
|
hp = target._pMaxHPBase;
|
|
}
|
|
SetPlayerHitPoints(target, hp);
|
|
|
|
target._pHPBase = target._pHitPoints + (target._pMaxHPBase - target._pMaxHP); // CODEFIX: does the same stuff as SetPlayerHitPoints above, can be removed
|
|
target._pMana = 0;
|
|
target._pManaBase = target._pMana + (target._pMaxManaBase - target._pMaxMana);
|
|
|
|
CalcPlrInv(target, true);
|
|
|
|
if (target.isOnActiveLevel()) {
|
|
StartStand(target, target._pdir);
|
|
} else {
|
|
target._pmode = PM_STAND;
|
|
}
|
|
}
|
|
|
|
void DoHealOther(const Player &caster, Player &target)
|
|
{
|
|
if ((target._pHitPoints >> 6) <= 0) {
|
|
return;
|
|
}
|
|
|
|
int hp = (GenerateRnd(10) + 1) << 6;
|
|
for (int i = 0; i < caster._pLevel; i++) {
|
|
hp += (GenerateRnd(4) + 1) << 6;
|
|
}
|
|
for (int i = 0; i < caster.GetSpellLevel(SPL_HEALOTHER); i++) {
|
|
hp += (GenerateRnd(6) + 1) << 6;
|
|
}
|
|
|
|
if (caster._pClass == HeroClass::Warrior || caster._pClass == HeroClass::Barbarian) {
|
|
hp *= 2;
|
|
} else if (caster._pClass == HeroClass::Rogue || caster._pClass == HeroClass::Bard) {
|
|
hp += hp / 2;
|
|
} else if (caster._pClass == HeroClass::Monk) {
|
|
hp *= 3;
|
|
}
|
|
|
|
target._pHitPoints = std::min(target._pHitPoints + hp, target._pMaxHP);
|
|
target._pHPBase = std::min(target._pHPBase + hp, target._pMaxHPBase);
|
|
|
|
if (&target == MyPlayer) {
|
|
RedrawComponent(PanelDrawComponent::Health);
|
|
}
|
|
}
|
|
|
|
int GetSpellBookLevel(spell_id s)
|
|
{
|
|
if (gbIsSpawn) {
|
|
switch (s) {
|
|
case SPL_STONE:
|
|
case SPL_GUARDIAN:
|
|
case SPL_GOLEM:
|
|
case SPL_ELEMENT:
|
|
case SPL_FLARE:
|
|
case SPL_BONESPIRIT:
|
|
return -1;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!gbIsHellfire) {
|
|
switch (s) {
|
|
case SPL_NOVA:
|
|
case SPL_APOCA:
|
|
return -1;
|
|
default:
|
|
if (s > SPL_LASTDIABLO)
|
|
return -1;
|
|
break;
|
|
}
|
|
}
|
|
|
|
return spelldata[s].sBookLvl;
|
|
}
|
|
|
|
int GetSpellStaffLevel(spell_id s)
|
|
{
|
|
if (gbIsSpawn) {
|
|
switch (s) {
|
|
case SPL_STONE:
|
|
case SPL_GUARDIAN:
|
|
case SPL_GOLEM:
|
|
case SPL_APOCA:
|
|
case SPL_ELEMENT:
|
|
case SPL_FLARE:
|
|
case SPL_BONESPIRIT:
|
|
return -1;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (!gbIsHellfire && s > SPL_LASTDIABLO)
|
|
return -1;
|
|
|
|
return spelldata[s].sStaffLvl;
|
|
}
|
|
|
|
} // namespace devilution
|