/*
* The ManaPlus Client
* Copyright (C) 2010 The Mana Developers
* Copyright (C) 2011-2020 The ManaPlus Developers
* Copyright (C) 2020-2023 The ManaVerse Developers
*
* This file is part of The ManaPlus Client.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see .
*/
#include "being/actorsprite.h"
#include "configuration.h"
#include "statuseffect.h"
#include "being/localplayer.h"
#include "const/utils/timer.h"
#include "gui/theme.h"
#include "listeners/debugmessagelistener.h"
#include "particle/particle.h"
#include "resources/db/statuseffectdb.h"
#include "resources/loaders/imageloader.h"
#include "resources/sprite/animatedsprite.h"
#include "resources/sprite/imagesprite.h"
#include "resources/sprite/spritereference.h"
#include "utils/checkutils.h"
#include "utils/delete2.h"
#include "utils/foreach.h"
#include "utils/timer.h"
#include "debug.h"
#define for_each_cursors() \
for (int size = CAST_S32(TargetCursorSize::SMALL); \
size < CAST_S32(TargetCursorSize::NUM_TC); \
size ++) \
{ \
for (int type = CAST_S32(TargetCursorType::NORMAL); \
type < CAST_S32(TargetCursorType::NUM_TCT); \
type ++) \
#define end_foreach }
AnimatedSprite *ActorSprite::targetCursor
[CAST_SIZE(TargetCursorType::NUM_TCT)]
[CAST_SIZE(TargetCursorSize::NUM_TC)];
bool ActorSprite::loaded = false;
ActorSprite::ActorSprite(const BeingId id) :
CompoundSprite(),
Actor(),
mStatusEffects(),
mStatusParticleEffects(nullptr, true),
mChildParticleEffects(&mStatusParticleEffects, false),
mHorseId(0),
mId(id),
mUsedTargetCursor(nullptr),
mActorSpriteListeners(),
mCursorPaddingX(0),
mCursorPaddingY(0),
mMustResetParticles(false),
mPoison(false),
mHaveCart(false),
mTrickDead(false)
{
}
ActorSprite::~ActorSprite()
{
mChildParticleEffects.clear();
mMustResetParticles = true;
mUsedTargetCursor = nullptr;
if (localPlayer != nullptr &&
localPlayer != this &&
localPlayer->getTarget() == this)
{
localPlayer->setTarget(nullptr);
}
// Notify listeners of the destruction.
FOR_EACH (ActorSpriteListenerIterator, iter, mActorSpriteListeners)
{
if (reportFalse(*iter))
(*iter)->actorSpriteDestroyed(*this);
}
}
void ActorSprite::logic()
{
BLOCK_START("ActorSprite::logic")
// Update sprite animations
update(tick_time * MILLISECONDS_IN_A_TICK);
// Restart status/particle effects, if needed
if (mMustResetParticles)
{
mMustResetParticles = false;
FOR_EACH (std::set::const_iterator, it, mStatusEffects)
{
const StatusEffect *const effect
= StatusEffectDB::getStatusEffect(*it, Enable_true);
if (effect != nullptr &&
effect->mIsPersistent)
{
updateStatusEffect(*it,
Enable_true,
IsStart_false);
}
}
}
// Update particle effects
mChildParticleEffects.moveTo(mPos.x, mPos.y);
BLOCK_END("ActorSprite::logic")
}
void ActorSprite::setMap(Map *const map)
{
Actor::setMap(map);
// Clear particle effect list because child particles became invalid
mChildParticleEffects.clear();
mMustResetParticles = true; // Reset status particles on next redraw
}
void ActorSprite::controlAutoParticle(Particle *const particle)
{
if (particle != nullptr)
{
particle->setActor(mId);
mChildParticleEffects.addLocally(particle);
}
}
void ActorSprite::controlCustomParticle(Particle *const particle)
{
if (particle != nullptr)
{
// The effect may not die without the beings permission or we segfault
particle->disableAutoDelete();
mChildParticleEffects.addLocally(particle);
}
}
void ActorSprite::controlParticleDeleted(const Particle *const particle)
{
if (particle != nullptr)
mChildParticleEffects.removeLocally(particle);
}
void ActorSprite::setTargetType(const TargetCursorTypeT type)
{
if (type == TargetCursorType::NONE)
{
untarget();
}
else
{
const size_t sz = CAST_SIZE(getTargetCursorSize());
mUsedTargetCursor = targetCursor[CAST_S32(type)][sz];
if (mUsedTargetCursor != nullptr)
{
static const int targetWidths[CAST_SIZE(
TargetCursorSize::NUM_TC)]
= {0, 0, 0};
static const int targetHeights[CAST_SIZE(
TargetCursorSize::NUM_TC)]
= {-mapTileSize / 2, -mapTileSize / 2, -mapTileSize};
mCursorPaddingX = CAST_S32(targetWidths[sz]);
mCursorPaddingY = CAST_S32(targetHeights[sz]);
}
}
}
void ActorSprite::setStatusEffect(const int32_t index,
const Enable active,
const IsStart start)
{
const Enable wasActive = fromBool(
mStatusEffects.find(index) != mStatusEffects.end(), Enable);
if (active != wasActive)
{
updateStatusEffect(index, active, start);
if (active == Enable_true)
{
mStatusEffects.insert(index);
}
else
{
mStatusEffects.erase(index);
}
}
}
static void applyEffectByOption(ActorSprite *const actor,
uint32_t option,
const char *const name,
const OptionsMap& options)
{
FOR_EACH (OptionsMapCIter, it, options)
{
const uint32_t opt = (*it).first;
const int32_t id = (*it).second;
const Enable enable = (opt & option) != 0 ? Enable_true : Enable_false;
option |= opt;
option ^= opt;
actor->setStatusEffect(id,
enable,
IsStart_false);
}
if (option != 0U &&
config.getBoolValue("unimplimentedLog"))
{
const std::string str = strprintf(
"Error: unknown effect by %s. "
"Left value: %u",
name,
option);
logger->log(str);
DebugMessageListener::distributeEvent(str);
}
}
static void applyEffectByOption1(ActorSprite *const actor,
uint32_t option,
const char *const name,
const OptionsMap& options)
{
FOR_EACH (OptionsMapCIter, it, options)
{
const uint32_t opt = (*it).first;
const int32_t id = (*it).second;
if (opt == option)
{
actor->setStatusEffect(id,
Enable_true,
IsStart_false);
option = 0U;
}
else
{
actor->setStatusEffect(id,
Enable_false,
IsStart_false);
}
}
if (option != 0 &&
config.getBoolValue("unimplimentedLog"))
{
const std::string str = strprintf(
"Error: unknown effect by %s. "
"Left value: %u",
name,
option);
logger->log(str);
DebugMessageListener::distributeEvent(str);
}
}
void ActorSprite::setStatusEffectOpitons(const uint32_t option,
const uint32_t opt1,
const uint32_t opt2,
const uint32_t opt3)
{
applyEffectByOption(this, option, "option",
StatusEffectDB::getOptionMap());
applyEffectByOption1(this, opt1, "opt1",
StatusEffectDB::getOpt1Map());
applyEffectByOption(this, opt2, "opt2",
StatusEffectDB::getOpt2Map());
applyEffectByOption(this, opt3, "opt3",
StatusEffectDB::getOpt3Map());
}
void ActorSprite::setStatusEffectOpitons(const uint32_t option,
const uint32_t opt1,
const uint32_t opt2)
{
applyEffectByOption(this, option, "option",
StatusEffectDB::getOptionMap());
applyEffectByOption1(this, opt1, "opt1",
StatusEffectDB::getOpt1Map());
applyEffectByOption(this, opt2, "opt2",
StatusEffectDB::getOpt2Map());
}
void ActorSprite::setStatusEffectOpiton0(const uint32_t option)
{
applyEffectByOption(this, option, "option",
StatusEffectDB::getOptionMap());
}
void ActorSprite::updateStatusEffect(const int32_t index,
const Enable newStatus,
const IsStart start)
{
StatusEffect *const effect = StatusEffectDB::getStatusEffect(
index, newStatus);
if (effect == nullptr)
return;
if (effect->mIsPoison && getType() == ActorType::Player)
setPoison(newStatus == Enable_true);
else if (effect->mIsCart && localPlayer == this)
setHaveCart(newStatus == Enable_true);
else if (effect->mIsRiding)
setRiding(newStatus == Enable_true);
else if (effect->mIsTrickDead)
setTrickDead(newStatus == Enable_true);
else if (effect->mIsPostDelay)
stopCast(newStatus == Enable_true);
handleStatusEffect(effect, index, newStatus, start);
}
void ActorSprite::handleStatusEffect(const StatusEffect *const effect,
const int32_t effectId,
const Enable newStatus,
const IsStart start)
{
if (effect == nullptr)
return;
if (newStatus == Enable_true)
{
if (effectId >= 0)
{
Particle *particle = nullptr;
if (start == IsStart_true)
particle = effect->getStartParticle();
if (particle == nullptr)
particle = effect->getParticle();
if (particle != nullptr)
mStatusParticleEffects.setLocally(effectId, particle);
}
}
else
{
mStatusParticleEffects.delLocally(effectId);
}
}
void ActorSprite::setupSpriteDisplay(const SpriteDisplay &display,
const ForceDisplay forceDisplay,
const DisplayTypeT displayType,
const std::string &color)
{
clear();
FOR_EACH (SpriteRefs, it, display.sprites)
{
if (*it == nullptr)
continue;
const std::string file = pathJoin(paths.getStringValue("sprites"),
combineDye3((*it)->sprite, color));
const int variant = (*it)->variant;
addSprite(AnimatedSprite::delayedLoad(file, variant));
}
// Ensure that something is shown, if desired
if (mSprites.empty() && forceDisplay == ForceDisplay_true)
{
if (display.image.empty())
{
addSprite(AnimatedSprite::delayedLoad(pathJoin(
paths.getStringValue("sprites"),
paths.getStringValue("spriteErrorFile")),
0));
}
else
{
std::string imagePath;
switch (displayType)
{
case DisplayType::Item:
default:
imagePath = pathJoin(paths.getStringValue("itemIcons"),
display.image);
break;
case DisplayType::Floor:
imagePath = pathJoin(paths.getStringValue("itemIcons"),
display.floor);
break;
}
imagePath = combineDye2(imagePath, color);
Image *img = Loader::getImage(imagePath);
if (img == nullptr)
img = Theme::getImageFromTheme("unknown-item.png");
addSprite(new ImageSprite(img));
if (img != nullptr)
img->decRef();
}
}
mChildParticleEffects.clear();
// setup particle effects
if (ParticleEngine::enabled && (particleEngine != nullptr))
{
FOR_EACH (StringVectCIter, itr, display.particles)
{
Particle *const p = particleEngine->addEffect(*itr, 0, 0, 0);
controlAutoParticle(p);
}
}
mMustResetParticles = true;
}
void ActorSprite::load()
{
if (loaded)
unload();
initTargetCursor();
loaded = true;
}
void ActorSprite::unload()
{
if (reportTrue(!loaded))
return;
cleanupTargetCursors();
loaded = false;
}
void ActorSprite::addActorSpriteListener(ActorSpriteListener *const listener)
{
mActorSpriteListeners.push_front(listener);
}
void ActorSprite::removeActorSpriteListener(ActorSpriteListener *const
listener)
{
mActorSpriteListeners.remove(listener);
}
static const char *cursorType(const TargetCursorTypeT type)
{
switch (type)
{
case TargetCursorType::IN_RANGE:
return "in-range";
default:
case TargetCursorType::NONE:
case TargetCursorType::NUM_TCT:
case TargetCursorType::NORMAL:
return "normal";
}
}
static const char *cursorSize(const TargetCursorSizeT size)
{
switch (size)
{
case TargetCursorSize::LARGE:
return "l";
case TargetCursorSize::MEDIUM:
return "m";
default:
case TargetCursorSize::NUM_TC:
case TargetCursorSize::SMALL:
return "s";
}
}
void ActorSprite::initTargetCursor()
{
static const std::string targetCursorFile("target-cursor-%s-%s.xml");
// Load target cursors
for_each_cursors()
{
targetCursor[type][size] = AnimatedSprite::load(
Theme::resolveThemePath(strprintf(
targetCursorFile.c_str(),
cursorType(static_cast(type)),
cursorSize(static_cast(size)))),
0);
}
end_foreach
}
void ActorSprite::cleanupTargetCursors()
{
for_each_cursors()
{
delete2(targetCursor[type][size])
}
end_foreach
}
std::string ActorSprite::getStatusEffectsString() const
{
std::string effectsStr;
if (!mStatusEffects.empty())
{
FOR_EACH (std::set::const_iterator, it, mStatusEffects)
{
const StatusEffect *const effect =
StatusEffectDB::getStatusEffect(
*it,
Enable_true);
if (effect == nullptr)
continue;
if (!effectsStr.empty())
effectsStr.append(", ");
effectsStr.append(effect->mName);
}
}
return effectsStr;
}