Files
aee-arcade/source/game/entities/concrete/whip_attack.cpp
T

330 lines
13 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "game/entities/concrete/whip_attack.hpp"
#include <algorithm>
#include <filesystem>
#include <iterator>
#include <string>
#include <vector>
#include "game/entities/entity_factory.hpp"
#include "game/entities/entity_manager.hpp"
#include "game/entities/player.hpp"
#include "game/gameplay_config.hpp"
#include "game/map/map.hpp"
namespace {
// Llig una entrada `sprites.<dir>: { path, size: [w, h] }` del YAML.
// Si manquen camps, retorna un DirSprite buit (queda invalidat al
// configure() per `surface_key.empty()`).
auto readDirSprite(const fkyaml::node& s) -> WhipAttack::DirSprite {
WhipAttack::DirSprite sprite;
if (s.contains("path")) {
const auto REL = s["path"].get_value<std::string>();
sprite.surface_key = std::filesystem::path(REL).filename().string();
}
if (s.contains("size")) {
const auto& sz = s["size"];
if (sz.is_sequence() && sz.size() == 2) {
sprite.frame_w = static_cast<float>(sz[0].get_value<int>());
sprite.frame_h = static_cast<float>(sz[1].get_value<int>());
}
}
return sprite;
}
// Llig el camp `active_frames` del bloc `animations.swing` — específic
// del látigo: els índexs llistats són els frames on la hitbox aplica
// dany (wind-up i recovery no danyen). La resta de l'animació la
// construeix `AnimationComponent::fromYaml`.
void readActiveFrames(const fkyaml::node& yaml, std::vector<int>& active_out) {
if (!yaml.contains("animations") || !yaml["animations"].contains("swing")) { return; }
const auto& sw = yaml["animations"]["swing"];
if (!sw.contains("active_frames")) { return; }
std::ranges::transform(sw["active_frames"], std::back_inserter(active_out), [](const fkyaml::node& f) { return f.template get_value<int>(); });
}
} // namespace
WhipAttack::WhipAttack(const fkyaml::node& yaml) {
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("whip_attack");
collides_ = false;
interactable_ = false;
if (yaml.contains("sprites")) {
const auto& s = yaml["sprites"];
if (s.contains("north")) { sprite_north_ = readDirSprite(s["north"]); }
if (s.contains("south")) { sprite_south_ = readDirSprite(s["south"]); }
if (s.contains("east")) { sprite_east_ = readDirSprite(s["east"]); }
if (s.contains("west")) { sprite_west_ = readDirSprite(s["west"]); }
}
anim_swing_ = AnimationComponent::fromYaml(yaml, "swing");
readActiveFrames(yaml, active_frames_);
anim_ = anim_swing_;
}
void WhipAttack::configure(Direction dir, const SDL_FRect& owner_box, float range_px) {
direction_ = dir;
range_px_ = range_px;
// Draw rect (= pixel_rect_): cobreix tot el sprite del látigo segons
// la taula d'offsets del disseny:
// E: (player.x - 16, player.y ), 80×32
// W: (player.x - 48, player.y ), 80×32
// N: (player.x, player.y - 48 ), 16×96
// S: (player.x, player.y - 16 ), 16×96
const auto& chosen = spriteFor(dir);
SDL_FRect draw{0.0F, 0.0F, chosen.frame_w, chosen.frame_h};
switch (dir) {
case Direction::EAST:
draw.x = owner_box.x - 16.0F;
draw.y = owner_box.y;
break;
case Direction::WEST:
draw.x = owner_box.x - 48.0F;
draw.y = owner_box.y;
break;
case Direction::NORTH:
draw.x = owner_box.x;
draw.y = owner_box.y - 48.0F;
break;
case Direction::SOUTH:
draw.x = owner_box.x;
draw.y = owner_box.y - 16.0F;
break;
}
setPixelRect(draw);
// Damage rect: 48×16 (E/W) o 16×48 (N/S) al nivell dels peus del
// jugador (els últims 16 px del seu pixel_rect — convenció estàndard
// del player AEE), pegat al borde en la direcció d'atac.
// computeDamageRect rep la cantonada del rect MÀXIM (sense retallar)
// i recorre internament la línia per topar amb el primer bloquejador.
const float COLLISION_TOP_Y = owner_box.y + owner_box.h - 16.0F;
const float OWNER_RIGHT = owner_box.x + owner_box.w;
const float OWNER_BOTTOM = owner_box.y + owner_box.h;
float spawn_x = 0.0F;
float spawn_y = 0.0F;
switch (dir) {
case Direction::EAST:
spawn_x = OWNER_RIGHT;
spawn_y = COLLISION_TOP_Y;
break;
case Direction::WEST:
spawn_x = owner_box.x - range_px;
spawn_y = COLLISION_TOP_Y;
break;
case Direction::NORTH:
spawn_x = owner_box.x;
spawn_y = COLLISION_TOP_Y - range_px;
break;
case Direction::SOUTH:
spawn_x = owner_box.x;
spawn_y = OWNER_BOTTOM;
break;
}
computeDamageRect(dir, spawn_x, spawn_y, range_px);
// Snapshot del feet_y del jugador. La convenció estàndard del Player
// AEE té el seu feet_y a la base inferior del pixel_rect (collision
// 16×16 als últims 16 px del sprite 16×32), així que owner_box.y +
// owner_box.h reprodueix `Player::getFeetY()` exactament.
owner_feet_y_ = owner_box.y + owner_box.h;
if (anim_.has_value()) { anim_->reset(); }
}
auto WhipAttack::getFeetY() const -> float {
return owner_feet_y_;
}
void WhipAttack::setWorldContext(EntityManager* manager, const std::vector<Player*>* players) {
entity_manager_ = manager;
players_ = players;
}
void WhipAttack::computeDamageRect(Direction dir, float spawn_x, float spawn_y, float range_px) {
// Caminem píxel a píxel des de la vora del jugador en la direcció
// d'atac, fins a topar amb un mur o sarc, o esgotar `range_px`.
// L'amplada perpendicular és `tile_size` (els passadissos del joc
// tenen exactament aquesta mida i l'atac és lateral).
//
// Si el tweak `whip.damage_pierces_walls` està actiu, la hitbox
// ignora els sarcòfags (passa a través del bloc complet) però
// continua respectant el `insidePlayfield`: fora de la sala no hi
// ha res a colpejar i ampliar-ho generaria visuals/edge cases
// estranys. El mur extern, doncs, sempre retalla.
const auto TILE = static_cast<float>(Map::tile_size);
const int MAX_PX = static_cast<int>(std::max(0.0F, range_px));
const auto MAX = static_cast<float>(MAX_PX);
const bool PIERCES = GameplayConfig::get().whipDamagePiercesWalls();
float length = 0.0F;
auto try_probe = [&](float l) -> bool {
SDL_FRect probe{0.0F, 0.0F, 0.0F, 0.0F};
switch (dir) {
case Direction::EAST:
probe = {.x = spawn_x, .y = spawn_y, .w = l, .h = TILE};
break;
case Direction::WEST:
// Per a W, la cara "del jugador" és la dreta; com creix
// `l`, la vora esquerra retrocedeix. spawn_x apunta al
// límit esquerre amb l = MAX (cas no retallat).
probe = {.x = spawn_x + (MAX - l), .y = spawn_y, .w = l, .h = TILE};
break;
case Direction::SOUTH:
probe = {.x = spawn_x, .y = spawn_y, .w = TILE, .h = l};
break;
case Direction::NORTH:
probe = {.x = spawn_x, .y = spawn_y + (MAX - l), .w = TILE, .h = l};
break;
}
if (!Map::insidePlayfield(probe)) { return false; }
if (PIERCES) { return true; }
return !Map::collides(probe);
};
for (int i = 1; i <= MAX_PX; ++i) {
const auto L = static_cast<float>(i);
if (try_probe(L)) {
length = L;
} else {
break;
}
}
switch (dir) {
case Direction::EAST:
damage_rect_ = {.x = spawn_x, .y = spawn_y, .w = length, .h = TILE};
return;
case Direction::WEST:
damage_rect_ = {.x = spawn_x + (MAX - length), .y = spawn_y, .w = length, .h = TILE};
return;
case Direction::SOUTH:
damage_rect_ = {.x = spawn_x, .y = spawn_y, .w = TILE, .h = length};
return;
case Direction::NORTH:
damage_rect_ = {.x = spawn_x, .y = spawn_y + (MAX - length), .w = TILE, .h = length};
return;
}
}
auto WhipAttack::getSurfaceKey() const -> const std::string& {
return spriteFor(direction_).surface_key;
}
auto WhipAttack::spriteFor(Direction dir) const -> const DirSprite& {
switch (dir) {
case Direction::NORTH:
return sprite_north_;
case Direction::SOUTH:
return sprite_south_;
case Direction::EAST:
return sprite_east_;
case Direction::WEST:
return sprite_west_;
}
return sprite_east_;
}
void WhipAttack::update(float delta_time) {
Entity::update(delta_time);
if (isFrameActive()) { applyDamage(); }
if (anim_.has_value() && anim_->completed()) { markDead(); }
}
namespace {
// (El mapping WhipAttack::Direction → Mummy::Direction viu ara a
// mummy.cpp, on s'usa des de `Mummy::onWhipHit`. Aquí només queda el
// mapping a Player::Cardinal — usat per al friendly-fire entre
// jugadors, que no és polimòrfic via Entity.)
auto whipDirToPlayerCardinal(WhipAttack::Direction d) -> Player::Cardinal {
switch (d) {
case WhipAttack::Direction::NORTH:
return Player::Cardinal::NORTH;
case WhipAttack::Direction::SOUTH:
return Player::Cardinal::SOUTH;
case WhipAttack::Direction::EAST:
return Player::Cardinal::EAST;
case WhipAttack::Direction::WEST:
return Player::Cardinal::WEST;
}
return Player::Cardinal::NONE;
}
auto playerIsTargetable(const Player* p) -> bool {
if (p == nullptr) { return false; }
const auto STATE = p->getState();
return STATE != Player::State::HURT && STATE != Player::State::EXITED && STATE != Player::State::DISABLED;
}
// Tracta una entitat candidata: si la seva caixa solapa amb
// `damage_rect`, delega la decisió a `entity->onWhipHit(attack)`.
// Cada entitat decideix què fer (Mummy entra HURT, Torch retorna
// false sense efecte, RollingRock no override → no-op). El retorn
// true marca el set `already_hit` per evitar segons impactes dins del
// mateix swing. Polimorfisme net — sense dynamic_cast.
void hitEntityIfApplicable(
Entity* entity,
const SDL_FRect& damage_rect,
WhipAttack* attack,
std::unordered_set<void*>& already_hit) {
if (entity == nullptr || entity->isDead()) { return; }
if (already_hit.contains(static_cast<void*>(entity))) { return; }
const auto BOX = entity->getCollisionRect();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
if (entity->onWhipHit(attack)) {
already_hit.insert(static_cast<void*>(entity));
}
}
void hitPlayerIfApplicable(
Player* p,
const Player* owner,
const SDL_FRect& damage_rect,
WhipAttack::Direction dir,
std::unordered_set<void*>& already_hit) {
if (p == owner || !playerIsTargetable(p)) { return; }
// Tweak `combat.friendly_fire`: si està desactivat, els altres
// jugadors són immunes al látigo aliat. L'owner ja queda exclòs
// per la primera guarda; aquest filtre cobreix només cross-player.
if (!GameplayConfig::get().friendlyFire()) { return; }
if (already_hit.contains(static_cast<void*>(p))) { return; }
const auto BOX = p->getBaseBox();
if (!SDL_HasRectIntersectionFloat(&damage_rect, &BOX)) { return; }
p->enterHurt(whipDirToPlayerCardinal(dir));
already_hit.insert(static_cast<void*>(p));
}
} // namespace
void WhipAttack::applyDamage() {
if (entity_manager_ != nullptr) {
// Iterem totes les entitats. La discriminació la fa cada una via
// `onWhipHit`: Mummy entra HURT si és whippable, la resta retorna
// false (no-op). El cost d'iterar antorxes/sarcs/forats és una
// crida virtual buida cada una — menyspreable comparat amb la
// claredat del callsite. Si demà un sarc vol trencar-se al
// fueteig, només cal override de `onWhipHit` allà.
for (auto* entity : entity_manager_->getEntities()) {
hitEntityIfApplicable(entity, damage_rect_, this, already_hit_);
}
}
if (players_ != nullptr) {
for (auto* p : *players_) {
hitPlayerIfApplicable(p, owner_, damage_rect_, direction_, already_hit_);
}
}
}
auto WhipAttack::isFrameActive() const -> bool {
if (!anim_.has_value()) { return false; }
const int FRAME = anim_->currentFrame();
return std::ranges::any_of(active_frames_, [FRAME](int f) { return f == FRAME; });
}
REGISTER_ENTITY("whip_attack", WhipAttack)