feat(mummy): evita tiles amb decal trigger (Hole) en decidir direcció
This commit is contained in:
1 parent
3e5217066f
commit
3bfca35f3a
5 files changed
+111
-27
No files matched your search
@@ -8,6 +8,7 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
#include "game/entities/entity_factory.hpp"
|
#include "game/entities/entity_factory.hpp"
|
||||||
|
#include "game/entities/entity_manager.hpp"
|
||||||
#include "game/entities/event_bus.hpp"
|
#include "game/entities/event_bus.hpp"
|
||||||
#include "game/entities/player.hpp"
|
#include "game/entities/player.hpp"
|
||||||
#include "game/map/map.hpp"
|
#include "game/map/map.hpp"
|
||||||
@@ -235,8 +236,15 @@ void Mummy::step(float delta_time) {
|
|||||||
|
|
||||||
// Cal girar. Preferim perpendiculars; només fem U-turn si no hi ha cap
|
// Cal girar. Preferim perpendiculars; només fem U-turn si no hi ha cap
|
||||||
// altra opció (comportament típic d'Oh Mummy!: no es recula sense motiu).
|
// altra opció (comportament típic d'Oh Mummy!: no es recula sense motiu).
|
||||||
const auto POOLS = collectDirectionPools(speed_ * delta_time);
|
// Primer intentem amb el filtre de decals (Hole) actiu; si totes les
|
||||||
const auto& pool = !POOLS.perp.empty() ? POOLS.perp : POOLS.uturn;
|
// alternatives apunten a un decal trigger, fallback al pool sense filtre
|
||||||
|
// — millor caure al forat que quedar-se paralitzada en una sala mal
|
||||||
|
// dissenyada (la mòmia no mor pels forats).
|
||||||
|
auto pools = collectDirectionPools(speed_ * delta_time, /*avoid_decals=*/true);
|
||||||
|
if (pools.perp.empty() && pools.uturn.empty()) {
|
||||||
|
pools = collectDirectionPools(speed_ * delta_time, /*avoid_decals=*/false);
|
||||||
|
}
|
||||||
|
const auto& pool = !pools.perp.empty() ? pools.perp : pools.uturn;
|
||||||
applyChoice(chooseDirection(pool));
|
applyChoice(chooseDirection(pool));
|
||||||
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
|
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
|
||||||
}
|
}
|
||||||
@@ -260,24 +268,38 @@ auto Mummy::currentDirection() const -> Direction {
|
|||||||
void Mummy::decideAtIntersection(float step_px) {
|
void Mummy::decideAtIntersection(float step_px) {
|
||||||
// Pool base: perpendiculars lliures (collectDirectionPools ja exclou la
|
// Pool base: perpendiculars lliures (collectDirectionPools ja exclou la
|
||||||
// direcció actual i la mitja volta va al seu propi calaix, que ací
|
// direcció actual i la mitja volta va al seu propi calaix, que ací
|
||||||
// ignorem — mai fem U-turn a un creuament).
|
// ignorem — mai fem U-turn a un creuament). Primera passa amb filtre
|
||||||
auto pools = collectDirectionPools(step_px);
|
// de decals (Hole) actiu.
|
||||||
|
auto pools = collectDirectionPools(step_px, /*avoid_decals=*/true);
|
||||||
std::vector<Direction> pool = std::move(pools.perp);
|
std::vector<Direction> pool = std::move(pools.perp);
|
||||||
|
|
||||||
// Afegim la recta si segueix essent lliure. A un creuament "net" ho és
|
// Afegim la recta si segueix essent lliure. A un creuament "net" ho és
|
||||||
// sempre (la mòmia hi ha arribat per aquest corredor), però als creua-
|
// sempre (la mòmia hi ha arribat per aquest corredor), però als creua-
|
||||||
// ments de la vora del playfield la recta pot estar al límit i no
|
// ments de la vora del playfield la recta pot estar al límit i no
|
||||||
// permetre un pas més — en aquest cas la descartem.
|
// permetre un pas més — en aquest cas la descartem. També la descartem
|
||||||
|
// si apunta a un decal trigger: aquesta és la regla "evita forats" en
|
||||||
|
// mode "seguir recte".
|
||||||
const auto STRAIGHT_BOX = candidateBox(dir_x_ * step_px, dir_y_ * step_px);
|
const auto STRAIGHT_BOX = candidateBox(dir_x_ * step_px, dir_y_ * step_px);
|
||||||
if (Map::insidePlayfield(STRAIGHT_BOX) && !Map::collides(STRAIGHT_BOX)) {
|
const bool STRAIGHT_OPEN = Map::insidePlayfield(STRAIGHT_BOX) && !Map::collides(STRAIGHT_BOX);
|
||||||
|
if (STRAIGHT_OPEN && !isDecalAheadInDir(dir_x_, dir_y_)) {
|
||||||
pool.push_back(currentDirection());
|
pool.push_back(currentDirection());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Fallback: si totes les opcions filtrades porten a decal o estan
|
||||||
|
// bloquejades, repetim el pool sense filtre i tornem a afegir la recta
|
||||||
|
// si és lliure (encara que vaja a forat). Així la mòmia mai es queda
|
||||||
|
// paralitzada en un creuament rodejat de forats.
|
||||||
|
if (pool.empty()) {
|
||||||
|
auto pools_unfiltered = collectDirectionPools(step_px, /*avoid_decals=*/false);
|
||||||
|
pool = std::move(pools_unfiltered.perp);
|
||||||
|
if (STRAIGHT_OPEN) { pool.push_back(currentDirection()); }
|
||||||
|
}
|
||||||
|
|
||||||
if (pool.empty()) { return; }
|
if (pool.empty()) { return; }
|
||||||
applyChoice(chooseDirection(pool));
|
applyChoice(chooseDirection(pool));
|
||||||
}
|
}
|
||||||
|
|
||||||
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
auto Mummy::collectDirectionPools(float step, bool avoid_decals) const -> DirectionPools {
|
||||||
struct Candidate {
|
struct Candidate {
|
||||||
float dx;
|
float dx;
|
||||||
float dy;
|
float dy;
|
||||||
@@ -297,6 +319,11 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
|||||||
if (IS_CURRENT) { continue; }
|
if (IS_CURRENT) { continue; }
|
||||||
const auto TEST = candidateBox(c.dx, c.dy);
|
const auto TEST = candidateBox(c.dx, c.dy);
|
||||||
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
|
if (!Map::insidePlayfield(TEST) || Map::collides(TEST)) { continue; }
|
||||||
|
// Filtre de decals (Hole i futurs decals trigger): comprova el
|
||||||
|
// tile cardinal contigu; si conté un trigger, descartem la
|
||||||
|
// direcció. El caller pot demanar `avoid_decals=false` com a
|
||||||
|
// fallback quan totes les filtrades estan bloquejades.
|
||||||
|
if (avoid_decals && isDecalAheadInDir(c.dx, c.dy)) { continue; }
|
||||||
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
|
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
|
||||||
if (IS_UTURN) {
|
if (IS_UTURN) {
|
||||||
pools.uturn.push_back(c.dir);
|
pools.uturn.push_back(c.dir);
|
||||||
@@ -307,6 +334,31 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
|||||||
return pools;
|
return pools;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto Mummy::isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool {
|
||||||
|
// Sense manager injectat (smoke tests sense `EntityManager::add`), el
|
||||||
|
// filtre és no-op: la mòmia es comporta exactament com abans del feature.
|
||||||
|
if (entity_manager_ == nullptr) { return false; }
|
||||||
|
if (dx_signed == 0.0F && dy_signed == 0.0F) { return false; }
|
||||||
|
// El rect a consultar és el AABB del tile cardinal contigu (mida
|
||||||
|
// `Map::tile_size`), NO el AABB de la mòmia desplaçat un pas — així
|
||||||
|
// captura "si la mòmia estiguera centrada en aquell tile, hi pisaria
|
||||||
|
// un decal?" amb marge.
|
||||||
|
const auto BOX = getCollisionRect();
|
||||||
|
const auto TS = static_cast<float>(Map::tile_size);
|
||||||
|
const auto SIGN_OF = [](float v) -> float {
|
||||||
|
if (v > 0.0F) { return 1.0F; }
|
||||||
|
if (v < 0.0F) { return -1.0F; }
|
||||||
|
return 0.0F;
|
||||||
|
};
|
||||||
|
const SDL_FRect NEXT{
|
||||||
|
.x = BOX.x + (SIGN_OF(dx_signed) * TS),
|
||||||
|
.y = BOX.y + (SIGN_OF(dy_signed) * TS),
|
||||||
|
.w = TS,
|
||||||
|
.h = TS,
|
||||||
|
};
|
||||||
|
return entity_manager_->findFloorTriggerAt(NEXT) != nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
void Mummy::applyChoice(Direction dir) {
|
void Mummy::applyChoice(Direction dir) {
|
||||||
switch (dir) {
|
switch (dir) {
|
||||||
case Direction::NORTH:
|
case Direction::NORTH:
|
||||||
|
|||||||
@@ -85,12 +85,25 @@ class Mummy : public DynamicHazard {
|
|||||||
// Recull les direccions alternatives viables des de la posició actual:
|
// Recull les direccions alternatives viables des de la posició actual:
|
||||||
// `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou
|
// `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou
|
||||||
// perquè ja sabem que està bloquejada. Cada candidata es valida amb
|
// perquè ja sabem que està bloquejada. Cada candidata es valida amb
|
||||||
// `Map::insidePlayfield` + `Map::collides` sobre la caja resultant.
|
// `Map::insidePlayfield` + `Map::collides` sobre la caja resultant. Si
|
||||||
|
// `avoid_decals` és true i hi ha `EntityManager` injectat, també es
|
||||||
|
// descarten les direccions que apunten a un tile amb un decal trigger
|
||||||
|
// (Hole o futurs pinchos plans/charcos). El caller és responsable de
|
||||||
|
// re-cridar amb `avoid_decals=false` com a fallback si totes les
|
||||||
|
// direccions filtrades estan bloquejades.
|
||||||
struct DirectionPools {
|
struct DirectionPools {
|
||||||
std::vector<Direction> perp;
|
std::vector<Direction> perp;
|
||||||
std::vector<Direction> uturn;
|
std::vector<Direction> uturn;
|
||||||
};
|
};
|
||||||
[[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools;
|
[[nodiscard]] auto collectDirectionPools(float step, bool avoid_decals = true) const -> DirectionPools;
|
||||||
|
|
||||||
|
// True si el tile cardinal contigu en la direcció (signe de dx, signe
|
||||||
|
// de dy) conté un decal trigger (`is_floor_decal_ && triggers_on_overlap_`)
|
||||||
|
// segons `EntityManager::findFloorTriggerAt`. Captura `Hole` i qualsevol
|
||||||
|
// futur decal trigger sense hardcodejar tipus. Si `entity_manager_` no
|
||||||
|
// està injectat (smoke tests sense manager), retorna false silenciosament
|
||||||
|
// — la mòmia es comporta com abans del feature.
|
||||||
|
[[nodiscard]] auto isDecalAheadInDir(float dx_signed, float dy_signed) const -> bool;
|
||||||
|
|
||||||
// Canvi de direcció + selecció de l'animació corresponent (un cardinal
|
// Canvi de direcció + selecció de l'animació corresponent (un cardinal
|
||||||
// per branch). Tots els camins que trien nova direcció (ctor amb
|
// per branch). Tots els camins que trien nova direcció (ctor amb
|
||||||
|
|||||||
@@ -12,9 +12,13 @@
|
|||||||
// `source/game/entities/player.hpp` i només es referencia per referència
|
// `source/game/entities/player.hpp` i només es referencia per referència
|
||||||
// dins `onPlayerContact`; `EventBus` és opcional a cada entitat i viu al
|
// dins `onPlayerContact`; `EventBus` és opcional a cada entitat i viu al
|
||||||
// seu header propi. `Surface` és el backend de render paletitzat del motor.
|
// seu header propi. `Surface` és el backend de render paletitzat del motor.
|
||||||
|
// `EntityManager` només es referencia per punter (injectat opcionalment
|
||||||
|
// per a entitats que necessiten consultar el món d'entitats — p.ex. la
|
||||||
|
// mòmia per saber si el pròxim tile té un decal trigger tipus Hole).
|
||||||
class Player;
|
class Player;
|
||||||
class EventBus;
|
class EventBus;
|
||||||
class Surface;
|
class Surface;
|
||||||
|
class EntityManager;
|
||||||
|
|
||||||
// Entitat genèrica del món. Actua com a dada en comú per a tots els actors
|
// Entitat genèrica del món. Actua com a dada en comú per a tots els actors
|
||||||
// del joc (decoratius, interactables, perills mòbils i estàtics): manté
|
// del joc (decoratius, interactables, perills mòbils i estàtics): manté
|
||||||
@@ -122,6 +126,14 @@ class Entity {
|
|||||||
// estàtiques de moment) el poden ignorar.
|
// estàtiques de moment) el poden ignorar.
|
||||||
void setEventBus(EventBus* bus) { bus_ = bus; }
|
void setEventBus(EventBus* bus) { bus_ = bus; }
|
||||||
|
|
||||||
|
// Injecció opcional del manager d'entitats (mateix patró que
|
||||||
|
// `setEventBus`: el manager s'auto-injecta a `add`/`setDoor`/
|
||||||
|
// `addSarcophagus`). El consumeixen entitats que volen consultar el
|
||||||
|
// món d'entitats — avui la mòmia, per saber si el tile següent
|
||||||
|
// conté un decal trigger (Hole) i evitar caure-hi. Entitats que no
|
||||||
|
// l'usen el poden ignorar.
|
||||||
|
void setEntityManager(EntityManager* manager) { entity_manager_ = manager; }
|
||||||
|
|
||||||
// Assigna la caixa de render en píxels. La invoca `EntityManager` durant
|
// Assigna la caixa de render en píxels. La invoca `EntityManager` durant
|
||||||
// `loadFromLevel` un cop sap la posició definitiva de l'entitat al mapa.
|
// `loadFromLevel` un cop sap la posició definitiva de l'entitat al mapa.
|
||||||
void setPixelRect(SDL_FRect rect) { pixel_rect_ = rect; }
|
void setPixelRect(SDL_FRect rect) { pixel_rect_ = rect; }
|
||||||
@@ -199,4 +211,5 @@ class Entity {
|
|||||||
bool dead_{false};
|
bool dead_{false};
|
||||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
|
EntityManager* entity_manager_{nullptr};
|
||||||
};
|
};
|
||||||
@@ -72,6 +72,7 @@ void EntityManager::forceOpenDoor() {
|
|||||||
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
||||||
if (entity == nullptr) { return nullptr; }
|
if (entity == nullptr) { return nullptr; }
|
||||||
entity->setEventBus(&bus_);
|
entity->setEventBus(&bus_);
|
||||||
|
entity->setEntityManager(this);
|
||||||
Entity* raw = entity.get();
|
Entity* raw = entity.get();
|
||||||
entities_.push_back(std::move(entity));
|
entities_.push_back(std::move(entity));
|
||||||
// cppcheck-suppress returnDanglingLifetime
|
// cppcheck-suppress returnDanglingLifetime
|
||||||
@@ -80,12 +81,16 @@ auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
|||||||
|
|
||||||
void EntityManager::setDoor(Door* door) {
|
void EntityManager::setDoor(Door* door) {
|
||||||
door_ = door;
|
door_ = door;
|
||||||
if (door_ != nullptr) { door_->setEventBus(&bus_); }
|
if (door_ != nullptr) {
|
||||||
|
door_->setEventBus(&bus_);
|
||||||
|
door_->setEntityManager(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcophagus* {
|
auto EntityManager::addSarcophagus(std::unique_ptr<Sarcophagus> sarc) -> Sarcophagus* {
|
||||||
if (sarc == nullptr) { return nullptr; }
|
if (sarc == nullptr) { return nullptr; }
|
||||||
sarc->setEventBus(&bus_);
|
sarc->setEventBus(&bus_);
|
||||||
|
sarc->setEntityManager(this);
|
||||||
Sarcophagus* raw = sarc.get();
|
Sarcophagus* raw = sarc.get();
|
||||||
entities_.push_back(std::move(sarc));
|
entities_.push_back(std::move(sarc));
|
||||||
++total_sarcs_;
|
++total_sarcs_;
|
||||||
@@ -110,6 +115,24 @@ void EntityManager::notifyPlayerTilePainted(int col, int row) {
|
|||||||
for (auto& e : entities_) { e->notifyTilePainted(col, row); }
|
for (auto& e : entities_) { e->notifyTilePainted(col, row); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* {
|
||||||
|
// Cerca lineal sobre `entities_`: cost menyspreable amb poques
|
||||||
|
// desenes d'entitats per sala i una crida per frame des dels
|
||||||
|
// consumidors actius (Player a l'aterratge, Mòmia a la decisió de
|
||||||
|
// direcció). Si esdevé calent, es podria indexar per cel·la del
|
||||||
|
// tilemap. La doble condició filtra exclusivament decals de sòl que
|
||||||
|
// volen rebre triggers — avui Hole; en el futur pinchos plans,
|
||||||
|
// plaques de pressió, charcos.
|
||||||
|
for (const auto& entity : entities_) {
|
||||||
|
if (!entity->triggersOnOverlap()) { continue; }
|
||||||
|
if (!entity->isFloorDecal()) { continue; }
|
||||||
|
const auto BOX = entity->getCollisionRect();
|
||||||
|
const bool OVERLAP = rect.x + rect.w > BOX.x && BOX.x + BOX.w > rect.x && rect.y + rect.h > BOX.y && BOX.y + BOX.h > rect.y;
|
||||||
|
if (OVERLAP) { return entity.get(); }
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
auto EntityManager::getEntities() const -> std::vector<Entity*> {
|
||||||
std::vector<Entity*> out;
|
std::vector<Entity*> out;
|
||||||
out.reserve(entities_.size());
|
out.reserve(entities_.size());
|
||||||
|
|||||||
@@ -58,23 +58,6 @@ void EntityManager::renderAll() {
|
|||||||
for (const auto& e : entities_) { renderEntity(*e); }
|
for (const auto& e : entities_) { renderEntity(*e); }
|
||||||
}
|
}
|
||||||
|
|
||||||
auto EntityManager::findFloorTriggerAt(const SDL_FRect& rect) const -> const Entity* {
|
|
||||||
// Cerca lineal sobre `entities_`: cost menyspreable amb poques
|
|
||||||
// desenes d'entitats per sala i una sola crida per frame i per
|
|
||||||
// jugador (a l'aterratge). Si esdevé calent, es podria indexar
|
|
||||||
// per cel·la del tilemap. La doble condició filtra exclusivament
|
|
||||||
// decals de sòl que volen rebre triggers — avui Hole; en el
|
|
||||||
// futur pinchos plans, plaques de pressió, charcos.
|
|
||||||
for (const auto& entity : entities_) {
|
|
||||||
if (!entity->triggersOnOverlap()) { continue; }
|
|
||||||
if (!entity->isFloorDecal()) { continue; }
|
|
||||||
const auto BOX = entity->getCollisionRect();
|
|
||||||
const bool OVERLAP = rect.x + rect.w > BOX.x && BOX.x + BOX.w > rect.x && rect.y + rect.h > BOX.y && BOX.y + BOX.h > rect.y;
|
|
||||||
if (OVERLAP) { return entity.get(); }
|
|
||||||
}
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
void EntityManager::checkPlayerCollisions(const std::vector<Player*>& players) {
|
||||||
if (players.empty()) { return; }
|
if (players.empty()) { return; }
|
||||||
for (const auto& entity : entities_) {
|
for (const auto& entity : entities_) {
|
||||||
|
|||||||
Reference in new issue
Block a user