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 "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/entity_manager.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
#include "game/entities/player.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
|
||||
// altra opció (comportament típic d'Oh Mummy!: no es recula sense motiu).
|
||||
const auto POOLS = collectDirectionPools(speed_ * delta_time);
|
||||
const auto& pool = !POOLS.perp.empty() ? POOLS.perp : POOLS.uturn;
|
||||
// Primer intentem amb el filtre de decals (Hole) actiu; si totes les
|
||||
// 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));
|
||||
// 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) {
|
||||
// Pool base: perpendiculars lliures (collectDirectionPools ja exclou la
|
||||
// direcció actual i la mitja volta va al seu propi calaix, que ací
|
||||
// ignorem — mai fem U-turn a un creuament).
|
||||
auto pools = collectDirectionPools(step_px);
|
||||
// ignorem — mai fem U-turn a un creuament). Primera passa amb filtre
|
||||
// de decals (Hole) actiu.
|
||||
auto pools = collectDirectionPools(step_px, /*avoid_decals=*/true);
|
||||
std::vector<Direction> pool = std::move(pools.perp);
|
||||
|
||||
// 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-
|
||||
// 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);
|
||||
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());
|
||||
}
|
||||
|
||||
// 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; }
|
||||
applyChoice(chooseDirection(pool));
|
||||
}
|
||||
|
||||
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||
auto Mummy::collectDirectionPools(float step, bool avoid_decals) const -> DirectionPools {
|
||||
struct Candidate {
|
||||
float dx;
|
||||
float dy;
|
||||
@@ -297,6 +319,11 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||
if (IS_CURRENT) { continue; }
|
||||
const auto TEST = candidateBox(c.dx, c.dy);
|
||||
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);
|
||||
if (IS_UTURN) {
|
||||
pools.uturn.push_back(c.dir);
|
||||
@@ -307,6 +334,31 @@ auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||
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) {
|
||||
switch (dir) {
|
||||
case Direction::NORTH:
|
||||
|
||||
@@ -85,12 +85,25 @@ class Mummy : public DynamicHazard {
|
||||
// Recull les direccions alternatives viables des de la posició actual:
|
||||
// `perpendiculars` (girs) i `uturn` (inversa). La direcció actual s'exclou
|
||||
// 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 {
|
||||
std::vector<Direction> perp;
|
||||
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
|
||||
// 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
|
||||
// dins `onPlayerContact`; `EventBus` és opcional a cada entitat i viu al
|
||||
// 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 EventBus;
|
||||
class Surface;
|
||||
class EntityManager;
|
||||
|
||||
// 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é
|
||||
@@ -122,6 +126,14 @@ class Entity {
|
||||
// estàtiques de moment) el poden ignorar.
|
||||
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
|
||||
// `loadFromLevel` un cop sap la posició definitiva de l'entitat al mapa.
|
||||
void setPixelRect(SDL_FRect rect) { pixel_rect_ = rect; }
|
||||
@@ -199,4 +211,5 @@ class Entity {
|
||||
bool dead_{false};
|
||||
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||
EventBus* bus_{nullptr};
|
||||
EntityManager* entity_manager_{nullptr};
|
||||
};
|
||||
@@ -72,6 +72,7 @@ void EntityManager::forceOpenDoor() {
|
||||
auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
||||
if (entity == nullptr) { return nullptr; }
|
||||
entity->setEventBus(&bus_);
|
||||
entity->setEntityManager(this);
|
||||
Entity* raw = entity.get();
|
||||
entities_.push_back(std::move(entity));
|
||||
// cppcheck-suppress returnDanglingLifetime
|
||||
@@ -80,12 +81,16 @@ auto EntityManager::add(std::unique_ptr<Entity> entity) -> Entity* {
|
||||
|
||||
void EntityManager::setDoor(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* {
|
||||
if (sarc == nullptr) { return nullptr; }
|
||||
sarc->setEventBus(&bus_);
|
||||
sarc->setEntityManager(this);
|
||||
Sarcophagus* raw = sarc.get();
|
||||
entities_.push_back(std::move(sarc));
|
||||
++total_sarcs_;
|
||||
@@ -110,6 +115,24 @@ void EntityManager::notifyPlayerTilePainted(int col, int 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*> {
|
||||
std::vector<Entity*> out;
|
||||
out.reserve(entities_.size());
|
||||
|
||||
@@ -58,23 +58,6 @@ void EntityManager::renderAll() {
|
||||
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) {
|
||||
if (players.empty()) { return; }
|
||||
for (const auto& entity : entities_) {
|
||||
|
||||
Reference in new issue
Block a user