feat(entities): caja-peus concèntrica i animacions direccionals a la momia, còpia de data/ sempre sincronitzada; v0.19.0
This commit is contained in:
1 parent
a3592d6c73
commit
32dcf264c4
9 files changed
+251
-58
No files matched your search
+9
-4
@@ -150,14 +150,19 @@ target_include_directories(${PROJECT_NAME} PRIVATE
|
|||||||
|
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
target_link_libraries(${PROJECT_NAME} PRIVATE SDL3::SDL3)
|
||||||
|
|
||||||
# Copia recursius de la carpeta data/ al costat del binari per que les rutes
|
# Còpia recursiva de data/ al costat del binari perquè les rutes relatives
|
||||||
# relatives ("data/player/player.gif") funcionen sense desplegament.
|
# ("data/player/player.gif") funcionin sense desplegament. Es defineix com
|
||||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
# a custom target amb `ALL` — s'executa a cada `cmake --build` encara que
|
||||||
|
# el binari no hagi canviat, de manera que editar YAML/GIF sense tocar codi
|
||||||
|
# igualment sincronitza el build. Abans era un POST_BUILD, i si només es
|
||||||
|
# modificava una dada la còpia no es disparava.
|
||||||
|
add_custom_target(sync_data ALL
|
||||||
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
COMMAND ${CMAKE_COMMAND} -E copy_directory
|
||||||
"${CMAKE_SOURCE_DIR}/data"
|
"${CMAKE_SOURCE_DIR}/data"
|
||||||
"$<TARGET_FILE_DIR:${PROJECT_NAME}>/data"
|
"${CMAKE_BINARY_DIR}/data"
|
||||||
COMMENT "Copying data/ next to the binary"
|
COMMENT "Copying data/ next to the binary"
|
||||||
)
|
)
|
||||||
|
add_dependencies(${PROJECT_NAME} sync_data)
|
||||||
|
|
||||||
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
|
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Wpedantic)
|
||||||
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
target_compile_definitions(${PROJECT_NAME} PRIVATE
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 833 B After Width: | Height: | Size: 1.8 KiB |
@@ -4,23 +4,32 @@ sprite:
|
|||||||
path: mummy.gif
|
path: mummy.gif
|
||||||
size: [12, 24]
|
size: [12, 24]
|
||||||
tile_size: [1, 1]
|
tile_size: [1, 1]
|
||||||
# La momia usa falsa perspectiva: el sprite fa 24 px d'alt però la caixa
|
# La momia usa falsa perspectiva: el sprite fa 12×24 (dos 12×12: cap i
|
||||||
# AABB només ocupa els 12×12 de baix (els "peus"). L'spawn `position` del
|
# peus) però la caja de col·lisió amb el mapa és 16×16, concèntrica amb
|
||||||
# map és el tile dels peus: el sprite es centra horitzontalment i la
|
# el rectangle-base 12×12 dels peus (igual que fa el jugador). Així la
|
||||||
# seua vora inferior s'alinea amb la vora inferior del tile.
|
# caixa s'estén 2 px a cada costat i 2 px per sota dels peus visuals,
|
||||||
|
# i amb `anchor: feet` s'alinea exactament amb el tile 16×16 on naix —
|
||||||
|
# la momia va pel centre del corredor i la caja ocupa la cel·la completa.
|
||||||
anchor: feet
|
anchor: feet
|
||||||
collision:
|
collision:
|
||||||
size: [12, 12]
|
size: [16, 16]
|
||||||
|
base_size: [12, 12]
|
||||||
flags:
|
flags:
|
||||||
collides: true
|
collides: true
|
||||||
interactable: false
|
interactable: false
|
||||||
|
# Strip de 12 frames en graella 12×1 (mateix esquema que el jugador):
|
||||||
|
# 0..3 → walk_down (cara avall)
|
||||||
|
# 4..7 → walk_up (cara amunt)
|
||||||
|
# 8..11 → walk (lateral; flip horitzontal segons el sentit)
|
||||||
|
# La momia no té variants `stand_*` perquè mai no para.
|
||||||
animations:
|
animations:
|
||||||
idle: { frames: [0], fps: 1, loop: true }
|
walk_down: { frames: [0, 1, 2, 3], fps: 8, loop: true }
|
||||||
walk: { frames: [0], fps: 8, loop: true }
|
walk_up: { frames: [4, 5, 6, 7], fps: 8, loop: true }
|
||||||
|
walk: { frames: [8, 9, 10, 11], fps: 8, loop: true }
|
||||||
ai:
|
ai:
|
||||||
type: corridor_wanderer
|
type: corridor_wanderer
|
||||||
direction_choice: random
|
direction_choice: random
|
||||||
initial_direction: right
|
initial_direction: right
|
||||||
stats:
|
stats:
|
||||||
speed: 40
|
speed: 30
|
||||||
damage: 1
|
damage: 1
|
||||||
@@ -130,3 +130,5 @@ entities:
|
|||||||
position: [9, 1]
|
position: [9, 1]
|
||||||
- type: mummy
|
- type: mummy
|
||||||
position: [10, 7]
|
position: [10, 7]
|
||||||
|
- type: mummy
|
||||||
|
position: [7, 9]
|
||||||
+30
-2
@@ -248,9 +248,37 @@ Tanca els hooks d'IA i col·lisió que quedaven apuntalats a les fases 1–3.
|
|||||||
- **Smoke tests**: T12 (CollisionBox), T13 (Mummy::step), T11 (sarc YAML),
|
- **Smoke tests**: T12 (CollisionBox), T13 (Mummy::step), T11 (sarc YAML),
|
||||||
T10 (torch anim). Total 14/14.
|
T10 (torch anim). Total 14/14.
|
||||||
|
|
||||||
## 7. Estat global
|
## 7. Fase 5 — Convenció de caixa "peus" com el jugador + animacions direccionals (DONE)
|
||||||
|
|
||||||
- **Andamiatge, integració, render animat, falsa perspectiva, moviment**: DONE. Els 26 actors del `pyramid.yaml` (4 torches, 20 sarcs, 1 door, 1 mummy) es carreguen, animen, es col·loquen amb l'anchor correcte, col·lisionen segons caixa lògica i el z-order interleava per peus.
|
Alinea la momia amb la convenció de col·lisió i animació del Player, i soluciona regressions de navegació derivades del canvi de caixa.
|
||||||
|
|
||||||
|
### Caixa concèntrica amb el rectangle-base
|
||||||
|
|
||||||
|
- **`collision.base_size: [bw, bh]`** (nou camp YAML, opcional). Si apareix, la caja no s'alinea a la vora inferior del sprite sinó que queda **concèntrica amb el rectangle-base `bw×bh` dels peus** (els `bh` píxels inferiors del sprite). Fórmula: `off_y = sprite_h − (base_h + H) / 2`. Sense `base_size`, comportament antic (alineada a la vora inferior del sprite).
|
||||||
|
- **`anchor: feet` ara alinea la caja, no el sprite**. Si l'entitat té `CollisionBox`, el loader col·loca `pixel_rect_` perquè la caja quedi exactament al tile. Sense caja, fallback a la convenció antiga (sprite centrat horitzontalment + vora inferior al tile). Per la momia: sprite 12×24 a (tile_x+2, tile_y−10); caja 16×16 a (tile_x, tile_y) — omple el tile sencer com fa el jugador.
|
||||||
|
- **`mummy.yaml`**: `collision: { size: [16, 16], base_size: [12, 12] }`. Abans era `[12, 12]` alineada a baix; visualment quedava "enganxada al mur inferior" dins dels corredors horitzontals.
|
||||||
|
|
||||||
|
### Snap-to-edge per girar al playfield
|
||||||
|
|
||||||
|
La caja 16×16 té exactament l'amplada del corredor (16 px). Amb el moviment contínu `speed × delta_time` (~0.66 px/frame), la momia s'aturava a una posició subpíxel dins de l'última cel·la (p. ex. `x=279.87` en lloc de `x=280`) i qualsevol gir perpendicular col·lidia amb el sarc adjacent al limit. [`Mummy::snapToPlayfieldEdge`](../source/game/entities/concrete/mummy.cpp) redueix el `dx/dy` fins a la vora exacta quan el candidat sortiria del playfield; el gir posterior ja parteix d'una posició alineada amb el lane.
|
||||||
|
|
||||||
|
### Animacions direccionals
|
||||||
|
|
||||||
|
- **`mummy.gif` a 12 frames** (graella 12×1) amb el mateix esquema que el jugador: 0–3 walk_down, 4–7 walk_up, 8–11 walk lateral. Sense variants `stand_*` (mai no para).
|
||||||
|
- **`Mummy` té tres `AnimationComponent`** privats + un helper `changeDirection(dx, dy)` (únic punt d'entrada per canviar direcció) que selecciona l'animació i aplica el flip: `dx ≠ 0` → lateral amb flip horitzontal segons signe; `dy < 0` → up; `dy > 0` → down.
|
||||||
|
- **`Entity::flip_` a la base** + `getFlip()`. `renderEntity` el passa a `Surface::render`, que ja suportava `SDL_FlipMode`. Les entitats estàtiques queden a `SDL_FLIP_NONE` automàticament.
|
||||||
|
|
||||||
|
### Pipeline de build: sincronització de `data/`
|
||||||
|
|
||||||
|
Canvi a [CMakeLists.txt](../CMakeLists.txt): la còpia de `data/` al build era un `add_custom_command(POST_BUILD)`, que només es dispara quan el binari es relinkeja. Si s'editava només un YAML o un GIF sense tocar codi, `build/data/` quedava obsolet. Ara és un `add_custom_target(sync_data ALL ...)` amb dependència del target principal — els custom targets sense BYPRODUCTS es consideren sempre out-of-date, així que la còpia s'executa a cada `cmake --build`.
|
||||||
|
|
||||||
|
### Refactors de suport
|
||||||
|
|
||||||
|
- `Mummy::step` partit: `snapToPlayfieldEdge`, `collectDirectionPools`, `applyChoice` — per mantenir la complexitat cognitiva sota el llindar de clang-tidy (< 25).
|
||||||
|
|
||||||
|
## 8. Estat global
|
||||||
|
|
||||||
|
- **Andamiatge, integració, render animat, falsa perspectiva, moviment, animació direccional de momia**: DONE. El `pyramid.yaml` actual (4 torches, 20 sarcs, 1 door, 2 mummies) es carrega, anima, col·loca amb l'anchor correcte, col·lisiona segons caixa lògica, el z-order interleava per peus, i la momia gira visualment a cada intersecció.
|
||||||
- **Pendents majors**: ENT-HAZ-01 (sistema de vides), ENT-DOOR-01 (transició d'escena), ENT-IA-02 (bias IA), PER-01 (perímetre amb blocked_predicate quan els sarcs siguin veïns).
|
- **Pendents majors**: ENT-HAZ-01 (sistema de vides), ENT-DOOR-01 (transició d'escena), ENT-IA-02 (bias IA), PER-01 (perímetre amb blocked_predicate quan els sarcs siguin veïns).
|
||||||
- **Smoke tests**: 14/14 passant; regla durable de cobrir amb smoke tota lògica no trivial respectada a totes les fases.
|
- **Smoke tests**: 14/14 passant; regla durable de cobrir amb smoke tota lògica no trivial respectada a totes les fases.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
#include "game/entities/concrete/mummy.hpp"
|
#include "game/entities/concrete/mummy.hpp"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <random>
|
#include <random>
|
||||||
#include <string>
|
#include <string>
|
||||||
@@ -20,9 +21,36 @@ namespace {
|
|||||||
return k_rng_;
|
return k_rng_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Llegeix un bloc d'animació del YAML amb fallbacks tolerants (mateix
|
||||||
|
// patró que Door/Sarcophagus). Si la clau no apareix, retorna una
|
||||||
|
// animació d'1 frame loopant, neutra — la momia es podrà moure però
|
||||||
|
// visualment es quedarà estàtica.
|
||||||
|
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||||
|
std::vector<int> frames = {0};
|
||||||
|
float fps = 8.0F;
|
||||||
|
bool loop = true;
|
||||||
|
if (yaml.contains("animations") && yaml["animations"].contains(key)) {
|
||||||
|
const auto& anim = yaml["animations"][key];
|
||||||
|
if (anim.contains("frames")) {
|
||||||
|
frames.clear();
|
||||||
|
for (const auto& f : anim["frames"]) {
|
||||||
|
// cppcheck-suppress useStlAlgorithm
|
||||||
|
frames.push_back(f.get_value<int>());
|
||||||
|
}
|
||||||
|
if (frames.empty()) { frames.push_back(0); }
|
||||||
|
}
|
||||||
|
if (anim.contains("fps")) { fps = anim["fps"].get_value<float>(); }
|
||||||
|
if (anim.contains("loop")) { loop = anim["loop"].get_value<bool>(); }
|
||||||
|
}
|
||||||
|
return {frames, fps, loop};
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Mummy::Mummy(const fkyaml::node& yaml) {
|
Mummy::Mummy(const fkyaml::node& yaml)
|
||||||
|
: anim_walk_down_(buildAnimation(yaml, "walk_down")),
|
||||||
|
anim_walk_up_(buildAnimation(yaml, "walk_up")),
|
||||||
|
anim_walk_lateral_(buildAnimation(yaml, "walk")) {
|
||||||
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
name_ = yaml.contains("name") ? yaml["name"].get_value<std::string>() : std::string("mummy");
|
||||||
collides_ = true;
|
collides_ = true;
|
||||||
interactable_ = false;
|
interactable_ = false;
|
||||||
@@ -43,16 +71,36 @@ Mummy::Mummy(const fkyaml::node& yaml) {
|
|||||||
initial = yaml["ai"]["initial_direction"].get_value<std::string>();
|
initial = yaml["ai"]["initial_direction"].get_value<std::string>();
|
||||||
}
|
}
|
||||||
if (initial == "right") {
|
if (initial == "right") {
|
||||||
setDirection(1.0F, 0.0F);
|
changeDirection(1.0F, 0.0F);
|
||||||
} else if (initial == "left") {
|
} else if (initial == "left") {
|
||||||
setDirection(-1.0F, 0.0F);
|
changeDirection(-1.0F, 0.0F);
|
||||||
} else if (initial == "up") {
|
} else if (initial == "up") {
|
||||||
setDirection(0.0F, -1.0F);
|
changeDirection(0.0F, -1.0F);
|
||||||
} else if (initial == "down") {
|
} else if (initial == "down") {
|
||||||
setDirection(0.0F, 1.0F);
|
changeDirection(0.0F, 1.0F);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Mummy::changeDirection(float dx, float dy) {
|
||||||
|
setDirection(dx, dy);
|
||||||
|
// Prevalença lateral: si hi ha component horitzontal s'usa l'anim
|
||||||
|
// lateral + flip segons el signe. Només direccions purament verticals
|
||||||
|
// disparen walk_up/walk_down. Quan la direcció queda (0,0) (cul-de-sac
|
||||||
|
// amb NONE) no toquem l'animació: la momia es quedarà al frame actual
|
||||||
|
// fins que step() li torni a assignar direcció.
|
||||||
|
if (dx != 0.0F) {
|
||||||
|
anim_ = anim_walk_lateral_;
|
||||||
|
flip_ = (dx < 0.0F) ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE;
|
||||||
|
} else if (dy < 0.0F) {
|
||||||
|
anim_ = anim_walk_up_;
|
||||||
|
flip_ = SDL_FLIP_NONE;
|
||||||
|
} else if (dy > 0.0F) {
|
||||||
|
anim_ = anim_walk_down_;
|
||||||
|
flip_ = SDL_FLIP_NONE;
|
||||||
|
}
|
||||||
|
if (anim_.has_value()) { anim_->reset(); }
|
||||||
|
}
|
||||||
|
|
||||||
auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction {
|
auto Mummy::chooseDirection(const std::vector<Direction>& allowed) -> Direction {
|
||||||
if (allowed.empty()) { return Direction::NONE; }
|
if (allowed.empty()) { return Direction::NONE; }
|
||||||
std::uniform_int_distribution<std::size_t> dist(0, allowed.size() - 1);
|
std::uniform_int_distribution<std::size_t> dist(0, allowed.size() - 1);
|
||||||
@@ -66,6 +114,31 @@ auto Mummy::candidateBox(float dx, float dy) const -> SDL_FRect {
|
|||||||
return box;
|
return box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Mummy::snapToPlayfieldEdge(float dx, float dy) {
|
||||||
|
// La caja 16×16 té exactament l'amplada del corredor; si ens aturàvem
|
||||||
|
// a una posició subpíxel dins de l'última cel·la, qualsevol gir
|
||||||
|
// perpendicular col·lidiria amb el sarc adjacent al limit. Per això
|
||||||
|
// reduïm el dx/dy fins a la vora exacta del playfield en l'eix actiu
|
||||||
|
// (l'altre sempre és 0 perquè la momia només es mou en 1D per frame).
|
||||||
|
const auto BOX = getCollisionRect();
|
||||||
|
float snap_dx = 0.0F;
|
||||||
|
float snap_dy = 0.0F;
|
||||||
|
if (dx > 0.0F) {
|
||||||
|
snap_dx = std::max(0.0F, static_cast<float>(Map::playfield_x + Map::playfield_w) - BOX.x - BOX.w);
|
||||||
|
} else if (dx < 0.0F) {
|
||||||
|
snap_dx = std::min(0.0F, static_cast<float>(Map::playfield_x) - BOX.x);
|
||||||
|
}
|
||||||
|
if (dy > 0.0F) {
|
||||||
|
snap_dy = std::max(0.0F, static_cast<float>(Map::playfield_y + Map::playfield_h) - BOX.y - BOX.h);
|
||||||
|
} else if (dy < 0.0F) {
|
||||||
|
snap_dy = std::min(0.0F, static_cast<float>(Map::playfield_y) - BOX.y);
|
||||||
|
}
|
||||||
|
if (!Map::collides(candidateBox(snap_dx, snap_dy))) {
|
||||||
|
pixel_rect_.x += snap_dx;
|
||||||
|
pixel_rect_.y += snap_dy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void Mummy::step(float delta_time) {
|
void Mummy::step(float delta_time) {
|
||||||
// Desplaçament nominal en aquest frame segons la direcció actual.
|
// Desplaçament nominal en aquest frame segons la direcció actual.
|
||||||
const float DX = dir_x_ * speed_ * delta_time;
|
const float DX = dir_x_ * speed_ * delta_time;
|
||||||
@@ -84,60 +157,63 @@ void Mummy::step(float delta_time) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bloqueig: cal girar. Recollim les direccions perpendiculars a l'actual
|
// Bloqueig per la vora del playfield: amarrem la caja a la vora exacta.
|
||||||
// que tinguin un pròxim tram lliure. Evitem U-turn llevat que no hi hagi
|
if (!Map::insidePlayfield(CANDIDATE)) { snapToPlayfieldEdge(DX, DY); }
|
||||||
// cap alternativa (comportament típic d'Oh Mummy!: no es recula sense
|
|
||||||
// motiu). Si la direcció perpendicular també està bloquejada, no s'hi
|
// Cal girar. Preferim perpendiculars; només fem U-turn si no hi ha cap
|
||||||
// inclou; si totes les perpendiculars estan bloquejades, afegim l'opcio
|
// altra opció (comportament típic d'Oh Mummy!: no es recula sense motiu).
|
||||||
// oposada com a últim recurs (cul-de-sac).
|
const auto POOLS = collectDirectionPools(speed_ * delta_time);
|
||||||
const float STEP = speed_ * delta_time;
|
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.
|
||||||
|
}
|
||||||
|
|
||||||
|
auto Mummy::collectDirectionPools(float step) const -> DirectionPools {
|
||||||
struct Candidate {
|
struct Candidate {
|
||||||
float dx;
|
float dx;
|
||||||
float dy;
|
float dy;
|
||||||
Direction dir;
|
Direction dir;
|
||||||
};
|
};
|
||||||
const std::array<Candidate, 4> ALL{{
|
const std::array<Candidate, 4> ALL{{
|
||||||
{.dx = STEP, .dy = 0.0F, .dir = Direction::RIGHT},
|
{.dx = step, .dy = 0.0F, .dir = Direction::RIGHT},
|
||||||
{.dx = -STEP, .dy = 0.0F, .dir = Direction::LEFT},
|
{.dx = -step, .dy = 0.0F, .dir = Direction::LEFT},
|
||||||
{.dx = 0.0F, .dy = -STEP, .dir = Direction::UP},
|
{.dx = 0.0F, .dy = -step, .dir = Direction::UP},
|
||||||
{.dx = 0.0F, .dy = STEP, .dir = Direction::DOWN},
|
{.dx = 0.0F, .dy = step, .dir = Direction::DOWN},
|
||||||
}};
|
}};
|
||||||
|
DirectionPools pools;
|
||||||
std::vector<Direction> perp_allowed;
|
|
||||||
std::vector<Direction> uturn_allowed;
|
|
||||||
for (const auto& c : ALL) {
|
for (const auto& c : ALL) {
|
||||||
const bool IS_CURRENT = (c.dx > 0.0F && dir_x_ > 0.0F) || (c.dx < 0.0F && dir_x_ < 0.0F) || (c.dy > 0.0F && dir_y_ > 0.0F) || (c.dy < 0.0F && dir_y_ < 0.0F);
|
const bool IS_CURRENT = (c.dx * dir_x_ > 0.0F) || (c.dy * dir_y_ > 0.0F);
|
||||||
const bool IS_UTURN = (c.dx > 0.0F && dir_x_ < 0.0F) || (c.dx < 0.0F && dir_x_ > 0.0F) || (c.dy > 0.0F && dir_y_ < 0.0F) || (c.dy < 0.0F && dir_y_ > 0.0F);
|
if (IS_CURRENT) { continue; } // ja sabem que està bloquejada
|
||||||
if (IS_CURRENT) { continue; } // ja hem vist que està bloquejada
|
|
||||||
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; }
|
||||||
|
const bool IS_UTURN = (c.dx * dir_x_ < 0.0F) || (c.dy * dir_y_ < 0.0F);
|
||||||
if (IS_UTURN) {
|
if (IS_UTURN) {
|
||||||
uturn_allowed.push_back(c.dir);
|
pools.uturn.push_back(c.dir);
|
||||||
} else {
|
} else {
|
||||||
perp_allowed.push_back(c.dir);
|
pools.perp.push_back(c.dir);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return pools;
|
||||||
|
}
|
||||||
|
|
||||||
const auto& pool = !perp_allowed.empty() ? perp_allowed : uturn_allowed;
|
void Mummy::applyChoice(Direction dir) {
|
||||||
const auto CHOICE = chooseDirection(pool);
|
switch (dir) {
|
||||||
switch (CHOICE) {
|
|
||||||
case Direction::UP:
|
case Direction::UP:
|
||||||
setDirection(0.0F, -1.0F);
|
changeDirection(0.0F, -1.0F);
|
||||||
break;
|
break;
|
||||||
case Direction::DOWN:
|
case Direction::DOWN:
|
||||||
setDirection(0.0F, 1.0F);
|
changeDirection(0.0F, 1.0F);
|
||||||
break;
|
break;
|
||||||
case Direction::LEFT:
|
case Direction::LEFT:
|
||||||
setDirection(-1.0F, 0.0F);
|
changeDirection(-1.0F, 0.0F);
|
||||||
break;
|
break;
|
||||||
case Direction::RIGHT:
|
case Direction::RIGHT:
|
||||||
setDirection(1.0F, 0.0F);
|
changeDirection(1.0F, 0.0F);
|
||||||
break;
|
break;
|
||||||
case Direction::NONE:
|
case Direction::NONE:
|
||||||
setDirection(0.0F, 0.0F);
|
changeDirection(0.0F, 0.0F);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// No avancem aquest frame: la direcció nova s'aplicarà al pròxim step.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Mummy::onPlayerContact(Player* /*player*/) {
|
void Mummy::onPlayerContact(Player* /*player*/) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "external/fkyaml_node.hpp"
|
#include "external/fkyaml_node.hpp"
|
||||||
|
#include "game/entities/animation_component.hpp"
|
||||||
#include "game/entities/hazard.hpp"
|
#include "game/entities/hazard.hpp"
|
||||||
|
|
||||||
// Mòmia: perill dinàmic que deambula pels passadís. A cada intersecció
|
// Mòmia: perill dinàmic que deambula pels passadís. A cada intersecció
|
||||||
@@ -44,4 +45,36 @@ class Mummy : public DynamicHazard {
|
|||||||
private:
|
private:
|
||||||
// Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy).
|
// Caixa de col·lisió candidata si l'entitat es desplaçara (dx, dy).
|
||||||
[[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect;
|
[[nodiscard]] auto candidateBox(float dx, float dy) const -> SDL_FRect;
|
||||||
|
|
||||||
|
// Amarra la caja a la vora exacta del playfield quan el candidat hi
|
||||||
|
// sortiria — sense això, amb la caja 16×16 iguala el corredor, la
|
||||||
|
// posició subpíxel dins de l'última cel·la impediria girar a cap
|
||||||
|
// perpendicular (col·lisionaria amb el sarc adjacent). Es crida abans
|
||||||
|
// de la lògica de gir.
|
||||||
|
void snapToPlayfieldEdge(float dx, float dy);
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
struct DirectionPools {
|
||||||
|
std::vector<Direction> perp;
|
||||||
|
std::vector<Direction> uturn;
|
||||||
|
};
|
||||||
|
[[nodiscard]] auto collectDirectionPools(float step) const -> DirectionPools;
|
||||||
|
|
||||||
|
// Canvi de direcció + aplicació de l'animació i flip corresponents.
|
||||||
|
// Tots els camins que trien nova direcció (ctor amb `initial_direction`,
|
||||||
|
// gir després d'un bloqueig) passen per ací per mantenir anim_ i
|
||||||
|
// flip_ en sincronia amb (dir_x_, dir_y_).
|
||||||
|
void changeDirection(float dx, float dy);
|
||||||
|
|
||||||
|
// Aplica la direcció triada (wrapper del switch en una sola crida).
|
||||||
|
void applyChoice(Direction dir);
|
||||||
|
|
||||||
|
// Animacions carregades del YAML, una per cada facing. Per a horitzontal
|
||||||
|
// només en cal una (`walk`) i el flip ja diferencia esquerra/dreta.
|
||||||
|
AnimationComponent anim_walk_down_;
|
||||||
|
AnimationComponent anim_walk_up_;
|
||||||
|
AnimationComponent anim_walk_lateral_;
|
||||||
};
|
};
|
||||||
@@ -128,6 +128,18 @@ class Entity {
|
|||||||
// col·lisió coincideix amb el sprite.
|
// col·lisió coincideix amb el sprite.
|
||||||
void setCollisionBox(CollisionBox box) { collision_box_ = box; }
|
void setCollisionBox(CollisionBox box) { collision_box_ = box; }
|
||||||
|
|
||||||
|
// Accés a la caixa crua (offsets + mida) — el loader la consulta per
|
||||||
|
// a col·locar el sprite quan `anchor: feet` ha d'alinear la caja (no
|
||||||
|
// el sprite) amb el tile. Nullopt si l'entitat no en té.
|
||||||
|
[[nodiscard]] auto getCollisionBox() const -> const std::optional<CollisionBox>& { return collision_box_; }
|
||||||
|
|
||||||
|
// Flip del sprite. Les entitats estàtiques mai no el toquen (queda a
|
||||||
|
// SDL_FLIP_NONE); les dinàmiques que miren cap als costats (p. ex.
|
||||||
|
// Mummy en direcció horitzontal) el canvien quan gira. El manager
|
||||||
|
// el passa a Surface::render.
|
||||||
|
[[nodiscard]] auto getFlip() const -> SDL_FlipMode { return flip_; }
|
||||||
|
void setFlip(SDL_FlipMode flip) { flip_ = flip; }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
std::string name_;
|
std::string name_;
|
||||||
TilePos tile_{};
|
TilePos tile_{};
|
||||||
@@ -137,5 +149,6 @@ class Entity {
|
|||||||
std::shared_ptr<Surface> surface_{nullptr};
|
std::shared_ptr<Surface> surface_{nullptr};
|
||||||
bool collides_{false};
|
bool collides_{false};
|
||||||
bool interactable_{false};
|
bool interactable_{false};
|
||||||
|
SDL_FlipMode flip_{SDL_FLIP_NONE};
|
||||||
EventBus* bus_{nullptr};
|
EventBus* bus_{nullptr};
|
||||||
};
|
};
|
||||||
@@ -51,7 +51,7 @@ void EntityManager::renderEntity(const Entity& e) {
|
|||||||
.w = RECT.w,
|
.w = RECT.w,
|
||||||
.h = RECT.h,
|
.h = RECT.h,
|
||||||
};
|
};
|
||||||
surface->render(static_cast<int>(RECT.x), static_cast<int>(RECT.y), &SRC);
|
surface->render(static_cast<int>(RECT.x), static_cast<int>(RECT.y), &SRC, e.getFlip());
|
||||||
}
|
}
|
||||||
|
|
||||||
void EntityManager::renderAll() {
|
void EntityManager::renderAll() {
|
||||||
@@ -105,9 +105,16 @@ namespace {
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bloc `collision: { size: [w, h], offset: [x, y] }` de la plantilla
|
// Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }`
|
||||||
// d'entitat. `offset` és opcional: si no apareix, s'entén "la caixa
|
// de la plantilla d'entitat.
|
||||||
// queda bottom-center del sprite" (convenció de falsa perspectiva).
|
// - Sense `base_size` ni `offset`: la caja queda alineada a la vora
|
||||||
|
// inferior del sprite i centrada horitzontalment (convenció antiga).
|
||||||
|
// - Amb `base_size: [bw, bh]`: la caja queda concèntrica amb el
|
||||||
|
// rectangle inferior bw×bh del sprite (els "peus" visuals), igual
|
||||||
|
// que fa el Player. Pot sobresortir per sobre/sota/costats del
|
||||||
|
// rectangle-base simètricament — així la caja representa el volum
|
||||||
|
// sòlid al voltant dels peus, no la silueta de tot el sprite.
|
||||||
|
// - `offset: [x, y]`: si apareix, sobreescriu el càlcul anterior.
|
||||||
// Retorna nullopt si no hi ha bloc, i l'entitat es queda sense caixa
|
// Retorna nullopt si no hi ha bloc, i l'entitat es queda sense caixa
|
||||||
// explícita (getCollisionRect torna tot el sprite).
|
// explícita (getCollisionRect torna tot el sprite).
|
||||||
auto readCollisionBox(const fkyaml::node& tmpl, float sprite_w, float sprite_h) -> std::optional<Entity::CollisionBox> {
|
auto readCollisionBox(const fkyaml::node& tmpl, float sprite_w, float sprite_h) -> std::optional<Entity::CollisionBox> {
|
||||||
@@ -120,6 +127,16 @@ namespace {
|
|||||||
const int H = sz[1].get_value<int>();
|
const int H = sz[1].get_value<int>();
|
||||||
int off_x = static_cast<int>((sprite_w - static_cast<float>(W)) / 2.0F);
|
int off_x = static_cast<int>((sprite_w - static_cast<float>(W)) / 2.0F);
|
||||||
int off_y = static_cast<int>(sprite_h - static_cast<float>(H));
|
int off_y = static_cast<int>(sprite_h - static_cast<float>(H));
|
||||||
|
if (c.contains("base_size")) {
|
||||||
|
const auto& bs = c["base_size"];
|
||||||
|
if (bs.is_sequence() && bs.size() == 2) {
|
||||||
|
const int BH = bs[1].get_value<int>();
|
||||||
|
// Centre vertical del rectangle-base (alineat a la vora
|
||||||
|
// inferior del sprite): sprite_h − bh/2. Caja centrada hi:
|
||||||
|
// off_y = sprite_h − bh/2 − H/2 = sprite_h − (bh + H)/2.
|
||||||
|
off_y = static_cast<int>(sprite_h) - ((BH + H) / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (c.contains("offset")) {
|
if (c.contains("offset")) {
|
||||||
const auto& o = c["offset"];
|
const auto& o = c["offset"];
|
||||||
if (o.is_sequence() && o.size() == 2) {
|
if (o.is_sequence() && o.size() == 2) {
|
||||||
@@ -258,16 +275,26 @@ namespace {
|
|||||||
const auto& p = spawn["position"];
|
const auto& p = spawn["position"];
|
||||||
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
|
const auto PX = tileToPixel(p[0].get_value<int>(), p[1].get_value<int>());
|
||||||
|
|
||||||
// Anchor "feet": el tile [col,row] és el tile dels peus. El sprite
|
// Anchor "feet": el tile [col,row] és el tile dels peus de l'actor.
|
||||||
// es centra horitzontalment sobre el tile i la seua vora inferior
|
// Si l'entitat té caixa de col·lisió explícita, la caixa s'alinea
|
||||||
// s'alinea amb la vora inferior del tile. Els píxels que sobresurten
|
// exactament amb el tile (el sprite queda col·locat per als offsets
|
||||||
// (alçada del sprite per damunt del tile dels peus) queden "per
|
// que la caja defineix — típicament sobresurt cap amunt per falsa
|
||||||
// sobre" sense afectar cap col·lisió — falsa perspectiva.
|
// perspectiva). Això és coherent amb el jugador, que també centra
|
||||||
|
// la seua 16×16 sobre el rectangle-base; la caja del tile i la del
|
||||||
|
// mapeig són la mateixa. Sense caixa, fallback històric: sprite
|
||||||
|
// centrat horitzontalment i amb la vora inferior a la vora inferior
|
||||||
|
// del tile.
|
||||||
float rect_x = PX.x;
|
float rect_x = PX.x;
|
||||||
float rect_y = PX.y;
|
float rect_y = PX.y;
|
||||||
if (anchor == "feet") {
|
if (anchor == "feet") {
|
||||||
rect_x = PX.x + (static_cast<float>(Map::tile_size) - sprite.width) / 2.0F;
|
const auto& box_opt = entity->getCollisionBox();
|
||||||
rect_y = PX.y + static_cast<float>(Map::tile_size) - sprite.height;
|
if (box_opt.has_value()) {
|
||||||
|
rect_x = PX.x - static_cast<float>(box_opt->offset_x);
|
||||||
|
rect_y = PX.y - static_cast<float>(box_opt->offset_y);
|
||||||
|
} else {
|
||||||
|
rect_x = PX.x + (static_cast<float>(Map::tile_size) - sprite.width) / 2.0F;
|
||||||
|
rect_y = PX.y + static_cast<float>(Map::tile_size) - sprite.height;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
entity->setPixelRect({.x = rect_x, .y = rect_y, .w = sprite.width, .h = sprite.height});
|
entity->setPixelRect({.x = rect_x, .y = rect_y, .w = sprite.width, .h = sprite.height});
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user