feat(attack): esquelet WhipAttack + canal d'spawn via EventBus
This commit is contained in:
1 parent
4752eb8ad7
commit
9ad21eb5bb
12 files changed
+346
-1
No files matched your search
@@ -0,0 +1,35 @@
|
||||
#pragma once
|
||||
|
||||
#include "game/entities/entity.hpp"
|
||||
|
||||
class Player;
|
||||
|
||||
// Base d'entitats d'atac: spawnejades pels jugadors, viuen poc temps i
|
||||
// poden danyar `Hazard`s i altres `Player`s (mai el seu propi `owner_`).
|
||||
//
|
||||
// Branca paral·lela a `Hazard` (no hereta d'ella) perquè la semàntica és
|
||||
// inversa: un `Hazard` és un perill estàtic/dinàmic que toca el jugador,
|
||||
// mentre que un `Attack` és el jugador qui colpeja la resta. Mantenir-les
|
||||
// separades permet, p. ex., que un atac no aparega al test de "què colpeja
|
||||
// al Player en el seu rect" (`EntityManager::checkPlayerCollisions`) perquè
|
||||
// el sentit del filtre és un altre.
|
||||
//
|
||||
// `Attack` no col·lisiona al test de moviment (els atacs no bloquegen
|
||||
// passos). El càlcul de quins objectius reben dany el fa cada subclasse
|
||||
// segons la seua geometria (rectangle direccional per al látigo, cercle
|
||||
// per a una explosió de bomba en el futur, etc.).
|
||||
class Attack : public Entity {
|
||||
public:
|
||||
Attack() {
|
||||
collides_ = false;
|
||||
interactable_ = false;
|
||||
}
|
||||
|
||||
void setOwner(Player* owner) { owner_ = owner; }
|
||||
[[nodiscard]] auto getOwner() const -> Player* { return owner_; }
|
||||
|
||||
protected:
|
||||
// Jugador que ha llançat l'atac. No rep dany del seu propi atac
|
||||
// (anti-self-friendly-fire). `nullptr` només a tests sintètics.
|
||||
Player* owner_{nullptr};
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
#include "game/entities/concrete/whip_attack.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "game/entities/entity_factory.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;
|
||||
}
|
||||
|
||||
// Construeix l'`AnimationComponent` del swing a partir del bloc
|
||||
// `animations.swing`. Convenció igual que la resta d'entitats:
|
||||
// - `frames` : llista d'índexs dins l'spritesheet (obligatori).
|
||||
// - `frame_ms` : durada uniforme per frame (ms). Alternativament `fps`.
|
||||
// - `loop` : per defecte true; al látigo el volem false (mor en
|
||||
// completar).
|
||||
auto buildSwingAnim(const fkyaml::node& yaml, std::vector<int>& active_out)
|
||||
-> AnimationComponent {
|
||||
std::vector<int> frames = {0};
|
||||
float fps = 8.0F;
|
||||
bool loop = false;
|
||||
if (yaml.contains("animations") && yaml["animations"].contains("swing")) {
|
||||
const auto& sw = yaml["animations"]["swing"];
|
||||
if (sw.contains("frames")) {
|
||||
frames.clear();
|
||||
for (const auto& f : sw["frames"]) {
|
||||
// cppcheck-suppress useStlAlgorithm
|
||||
frames.push_back(f.get_value<int>());
|
||||
}
|
||||
if (frames.empty()) { frames.push_back(0); }
|
||||
}
|
||||
if (sw.contains("frame_ms")) {
|
||||
const auto MS = static_cast<float>(sw["frame_ms"].get_value<int>());
|
||||
if (MS > 0.0F) { fps = 1000.0F / MS; }
|
||||
} else if (sw.contains("fps")) {
|
||||
fps = sw["fps"].get_value<float>();
|
||||
}
|
||||
if (sw.contains("loop")) { loop = sw["loop"].get_value<bool>(); }
|
||||
if (sw.contains("active_frames")) {
|
||||
for (const auto& f : sw["active_frames"]) {
|
||||
// cppcheck-suppress useStlAlgorithm
|
||||
active_out.push_back(f.get_value<int>());
|
||||
}
|
||||
}
|
||||
}
|
||||
return {frames, fps, loop};
|
||||
}
|
||||
|
||||
} // 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_ = buildSwingAnim(yaml, active_frames_);
|
||||
anim_ = anim_swing_;
|
||||
}
|
||||
|
||||
void WhipAttack::configure(Direction dir, float spawn_x, float spawn_y, float range_px) {
|
||||
direction_ = dir;
|
||||
range_px_ = range_px;
|
||||
|
||||
const auto& chosen = spriteFor(dir);
|
||||
setPixelRect({.x = spawn_x, .y = spawn_y, .w = chosen.frame_w, .h = chosen.frame_h});
|
||||
|
||||
if (anim_.has_value()) { anim_->reset(); }
|
||||
}
|
||||
|
||||
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 (anim_.has_value() && anim_->completed()) { markDead(); }
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,94 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/entities/animation_component.hpp"
|
||||
#include "game/entities/attack.hpp"
|
||||
|
||||
class Entity;
|
||||
|
||||
// Atac de látigo: spawnejat pel jugador en entrar a `ATTACKING`. Viu la
|
||||
// durada de la seua animació (`swing`) i s'autoelimina al final.
|
||||
//
|
||||
// Sprites: 4 fitxers GIF distints, un per cardinal. La mida del frame
|
||||
// canvia entre direccions (E/W: 48×16, N/S: 16×48), cosa que el sistema
|
||||
// d'animació actual no permet expressar amb una sola surface (una entitat
|
||||
// = una graella). Per això el YAML llista les 4 entrades sota `sprites:`
|
||||
// i `configure()` tria la que toca segons direcció.
|
||||
//
|
||||
// Hitbox de dany: el rectangle de dany és el `pixel_rect_` retallat al
|
||||
// primer bloquejador en la línia d'atac (mur extern del playfield o
|
||||
// sarcòfag). Aquesta lògica viu a la fase de damage application; en
|
||||
// aquest skeleton només viu la part visual i de cicle de vida.
|
||||
class WhipAttack : public Attack {
|
||||
public:
|
||||
// Direcció cardinal del swing. Coincideix valors amb
|
||||
// `Player::Cardinal` (1=N, 2=S, 3=E, 4=W) per facilitar la
|
||||
// conversió a la frontera de l'event `whip_spawn`.
|
||||
enum class Direction : std::uint8_t {
|
||||
NORTH = 1,
|
||||
SOUTH = 2,
|
||||
EAST = 3,
|
||||
WEST = 4,
|
||||
};
|
||||
|
||||
// Definició estàtica d'una direcció: clau al `Resource::Cache` i mida
|
||||
// del frame en píxels. Llegida del YAML una vegada al ctor; usada per
|
||||
// configure() per omplir surface_ i pixel_rect_. Pública perquè el
|
||||
// helper d'anàlisi del YAML viu en un namespace anònim al .cpp.
|
||||
struct DirSprite {
|
||||
std::string surface_key;
|
||||
float frame_w{0.0F};
|
||||
float frame_h{0.0F};
|
||||
};
|
||||
|
||||
explicit WhipAttack(const fkyaml::node& yaml);
|
||||
|
||||
// Configura l'estat runtime de la instància. Cal cridar-lo abans del
|
||||
// primer `update`. Selecciona la surface i la mida del pixel_rect_
|
||||
// segons direcció, i posa la cantonada superior-esquerra de la
|
||||
// hitbox a (spawn_x, spawn_y).
|
||||
void configure(Direction dir, float spawn_x, float spawn_y, float range_px);
|
||||
|
||||
// Avança animació; quan completa, marca dead.
|
||||
void update(float delta_time) override;
|
||||
|
||||
[[nodiscard]] auto getDirection() const -> Direction { return direction_; }
|
||||
|
||||
// True si el frame actual de l'animació és un dels `active_frames`
|
||||
// declarats al YAML (els frames durant els quals la hitbox aplica
|
||||
// dany). Wind-up i recovery retornen false.
|
||||
[[nodiscard]] auto isFrameActive() const -> bool;
|
||||
|
||||
// Llistat dels frames actius (lectura del YAML, immutable per
|
||||
// instància). Exposat per als smoke tests.
|
||||
[[nodiscard]] auto getActiveFrames() const -> const std::vector<int>& { return active_frames_; }
|
||||
|
||||
// Llargada de la hitbox abans de retallar contra murs/sarcs (en píxels).
|
||||
[[nodiscard]] auto getRangePx() const -> float { return range_px_; }
|
||||
|
||||
// Clau al `Resource::Cache` per a la surface de la direcció actual
|
||||
// (la que ha triat `configure()`). El caller (típicament `GameScene`)
|
||||
// resol la surface i la passa via `setSurface()`. Aquesta separació
|
||||
// manté el codi de l'entitat sense dependència directa al
|
||||
// `Resource::Cache`, igual que la resta d'entitats del joc.
|
||||
[[nodiscard]] auto getSurfaceKey() const -> const std::string&;
|
||||
|
||||
private:
|
||||
[[nodiscard]] auto spriteFor(Direction dir) const -> const DirSprite&;
|
||||
|
||||
Direction direction_{Direction::EAST};
|
||||
AnimationComponent anim_swing_;
|
||||
std::vector<int> active_frames_;
|
||||
std::set<Entity*> already_hit_;
|
||||
float range_px_{0.0F};
|
||||
|
||||
DirSprite sprite_north_;
|
||||
DirSprite sprite_south_;
|
||||
DirSprite sprite_east_;
|
||||
DirSprite sprite_west_;
|
||||
};
|
||||
@@ -12,6 +12,7 @@
|
||||
// Forward: els events poden dur un punter a `Entity` (p. ex. quin sarcòfag
|
||||
// s'ha obert). No cal incloure el header complet aquí.
|
||||
class Entity;
|
||||
class Player;
|
||||
|
||||
// Event pub/sub minimalista. Hi ha un únic `EventBus` per mundus (propietat
|
||||
// de l'`EntityManager`); les entitats que el necessiten en reben un punter
|
||||
@@ -49,9 +50,28 @@ struct WorldPos {
|
||||
float y{0.0F};
|
||||
};
|
||||
|
||||
// Payload de `whip_spawn`: el `Player` el publica quan entra a ATTACKING
|
||||
// i el `GameScene` el consumeix per crear un `WhipAttack` via
|
||||
// `EntityManager::add`. Així el Player no depèn ni de `EntityManager` ni de
|
||||
// `WhipAttack`, i el contracte queda dins del bus.
|
||||
// - `owner` : Player que ha llançat l'atac (no rep dany propi).
|
||||
// - `direction` : Player::Cardinal castat a int (1=N, 2=S, 3=E, 4=W) per
|
||||
// evitar dependència cíclica al header.
|
||||
// - `spawn_x/y` : cantonada superior-esquerra de la hitbox màxima
|
||||
// (sense retallar) en píxels.
|
||||
// - `range_px` : longitud de la hitbox abans de retallar contra murs i
|
||||
// sarcòfags. Llegida del YAML del jugador.
|
||||
struct WhipSpawnPayload {
|
||||
Player* owner{nullptr};
|
||||
int direction{0};
|
||||
float spawn_x{0.0F};
|
||||
float spawn_y{0.0F};
|
||||
float range_px{0.0F};
|
||||
};
|
||||
|
||||
struct Event {
|
||||
std::string name;
|
||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos>;
|
||||
using Payload = std::variant<std::monostate, int, std::string, Entity*, TilePos, PaintedTile, WorldPos, WhipSpawnPayload>;
|
||||
Payload payload;
|
||||
// Jugador que ha originat (o provocat) l'event. -1 = sense jugador
|
||||
// associat (events del món, com `sarcophagus_opened` des de la pròpia
|
||||
@@ -101,6 +121,9 @@ class EventBus {
|
||||
void publish(const std::string& name, WorldPos pos) {
|
||||
publish(Event{.name = name, .payload = pos});
|
||||
}
|
||||
void publish(const std::string& name, const WhipSpawnPayload& whip) {
|
||||
publish(Event{.name = name, .payload = whip});
|
||||
}
|
||||
|
||||
private:
|
||||
struct Subscription {
|
||||
|
||||
Reference in new issue
Block a user