feat(player): gir de 180° amb pose intermèdia configurable al YAML
This commit is contained in:
1 parent
5c70019eb5
commit
74dad71cc2
5 files changed
+215
-56
No files matched your search
+155
-18
@@ -45,6 +45,8 @@ namespace {
|
||||
|
||||
} // namespace
|
||||
|
||||
Player::TurnConfig Player::turn_config;
|
||||
|
||||
void Player::loadFrom(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
if (bytes.empty()) {
|
||||
@@ -71,6 +73,153 @@ void Player::loadFrom(const std::string& yaml_path) {
|
||||
collision_padding = (collision_size - base_size) / 2;
|
||||
collision_x_offset = -collision_padding;
|
||||
collision_y_offset = (sprite_h - base_size) - collision_padding;
|
||||
|
||||
// Bloc opcional `turn:` — si manca, `duration_ms` queda a 0 i la
|
||||
// mecànica del gir de 180° no s'activa (canvis de direcció instantanis).
|
||||
turn_config = TurnConfig{};
|
||||
if (yaml.contains("turn")) {
|
||||
const auto& t = yaml["turn"];
|
||||
if (t.contains("duration_ms")) { turn_config.duration_ms = t["duration_ms"].get_value<int>(); }
|
||||
if (t.contains("transitions")) {
|
||||
const auto& tr = t["transitions"];
|
||||
if (tr.contains("west_to_east")) { turn_config.w_to_e = cardinalFromString(tr["west_to_east"].get_value<std::string>()); }
|
||||
if (tr.contains("east_to_west")) { turn_config.e_to_w = cardinalFromString(tr["east_to_west"].get_value<std::string>()); }
|
||||
if (tr.contains("north_to_south")) { turn_config.n_to_s = cardinalFromString(tr["north_to_south"].get_value<std::string>()); }
|
||||
if (tr.contains("south_to_north")) { turn_config.s_to_n = cardinalFromString(tr["south_to_north"].get_value<std::string>()); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::applyCardinal(Cardinal c) {
|
||||
switch (c) {
|
||||
case Cardinal::NORTH:
|
||||
facing_ = Facing::UP;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
break;
|
||||
case Cardinal::SOUTH:
|
||||
facing_ = Facing::DOWN;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
break;
|
||||
case Cardinal::EAST:
|
||||
facing_ = Facing::SIDE;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
break;
|
||||
case Cardinal::WEST:
|
||||
facing_ = Facing::SIDE;
|
||||
setFlip(SDL_FLIP_HORIZONTAL);
|
||||
break;
|
||||
case Cardinal::NONE:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
auto Player::cardinalFromInput(float vx, float vy) -> Cardinal {
|
||||
// Mateixa precedència que la lògica original: l'horitzontal preval
|
||||
// sobre el vertical (els diagonals s'interpreten com a laterals).
|
||||
if (vx > 0.0F) { return Cardinal::EAST; }
|
||||
if (vx < 0.0F) { return Cardinal::WEST; }
|
||||
if (vy < 0.0F) { return Cardinal::NORTH; }
|
||||
if (vy > 0.0F) { return Cardinal::SOUTH; }
|
||||
return Cardinal::NONE;
|
||||
}
|
||||
|
||||
auto Player::cardinalFromString(const std::string& s) -> Cardinal {
|
||||
if (s == "north" || s == "up") { return Cardinal::NORTH; }
|
||||
if (s == "south" || s == "down") { return Cardinal::SOUTH; }
|
||||
if (s == "east" || s == "right") { return Cardinal::EAST; }
|
||||
if (s == "west" || s == "left") { return Cardinal::WEST; }
|
||||
throw std::runtime_error("Player: cardinal desconegut '" + s + "' al bloc turn.transitions");
|
||||
}
|
||||
|
||||
auto Player::opposite(Cardinal c) -> Cardinal {
|
||||
switch (c) {
|
||||
case Cardinal::NORTH:
|
||||
return Cardinal::SOUTH;
|
||||
case Cardinal::SOUTH:
|
||||
return Cardinal::NORTH;
|
||||
case Cardinal::EAST:
|
||||
return Cardinal::WEST;
|
||||
case Cardinal::WEST:
|
||||
return Cardinal::EAST;
|
||||
case Cardinal::NONE:
|
||||
return Cardinal::NONE;
|
||||
}
|
||||
return Cardinal::NONE;
|
||||
}
|
||||
|
||||
auto Player::intermediateFor(Cardinal target) -> Cardinal {
|
||||
switch (target) {
|
||||
case Cardinal::EAST:
|
||||
return turn_config.w_to_e;
|
||||
case Cardinal::WEST:
|
||||
return turn_config.e_to_w;
|
||||
case Cardinal::SOUTH:
|
||||
return turn_config.n_to_s;
|
||||
case Cardinal::NORTH:
|
||||
return turn_config.s_to_n;
|
||||
case Cardinal::NONE:
|
||||
return Cardinal::NONE;
|
||||
}
|
||||
return Cardinal::NONE;
|
||||
}
|
||||
|
||||
void Player::updateFacingAndTurn(float delta_time, float vx, float vy) {
|
||||
// Cardinal d'INPUT cru. Important: la detecció del gir de 180° s'ha
|
||||
// de basar en l'input (intent del jugador), no en la velocitat
|
||||
// resultant. Mentre no hi haja inèrcia, vx/vy són directament l'input
|
||||
// i serveixen com a tal; quan s'introduïsca, cal preservar aquesta
|
||||
// separació (calcular el cardinal a partir de les tecles/gamepad
|
||||
// crus, no de la velocitat suavitzada).
|
||||
const Cardinal INPUT_CARD = cardinalFromInput(vx, vy);
|
||||
|
||||
// Si hi ha un gir en curs, descomptem el timer i mantenim el facing
|
||||
// forçat al cardinal intermedi. Quan s'esgota, apliquem el cardinal
|
||||
// pendent. El moviment físic continua sense alterar-se (vx/vy ja
|
||||
// s'han enviat a setVel al caller).
|
||||
if (turning_remaining_ms_ > 0.0F) {
|
||||
turning_remaining_ms_ -= delta_time * 1000.0F;
|
||||
if (turning_remaining_ms_ <= 0.0F) {
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
applyCardinal(pending_cardinal_);
|
||||
pending_cardinal_ = Cardinal::NONE;
|
||||
intermediate_cardinal_ = Cardinal::NONE;
|
||||
} else {
|
||||
applyCardinal(intermediate_cardinal_);
|
||||
}
|
||||
} else {
|
||||
// Detecció del gir: `last_input_cardinal_` és l'últim cardinal
|
||||
// d'input no nul (es preserva entre releases). Així, el primer
|
||||
// input des de l'estat idle no dispara cap gir — només dispara
|
||||
// quan el jugador "canvia d'opinió" prement la direcció oposada
|
||||
// a la prèvia. Requereix també que el bloc `turn:` del YAML
|
||||
// estiga activat (`duration_ms > 0`).
|
||||
const bool TRIGGER_TURN =
|
||||
(turn_config.duration_ms > 0) && INPUT_CARD != Cardinal::NONE && last_input_cardinal_ != Cardinal::NONE && INPUT_CARD == opposite(last_input_cardinal_);
|
||||
if (TRIGGER_TURN) {
|
||||
pending_cardinal_ = INPUT_CARD;
|
||||
intermediate_cardinal_ = intermediateFor(INPUT_CARD);
|
||||
turning_remaining_ms_ = static_cast<float>(turn_config.duration_ms);
|
||||
applyCardinal(intermediate_cardinal_);
|
||||
} else if (vx != 0.0F) {
|
||||
// Tria de facing original: prevalença lateral, flip segons el
|
||||
// signe de vx; verticals purs van a UP/DOWN amb flip NONE; en
|
||||
// aturar-se conservem `facing_` per a l'animació stand.
|
||||
facing_ = Facing::SIDE;
|
||||
setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
|
||||
} else if (vy < 0.0F) {
|
||||
facing_ = Facing::UP;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
} else if (vy > 0.0F) {
|
||||
facing_ = Facing::DOWN;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
}
|
||||
}
|
||||
|
||||
// Memoritzem l'últim cardinal d'input no nul per a la propera detecció
|
||||
// de gir (ignorant els zeros: l'estat idle no reinicia la memòria).
|
||||
if (INPUT_CARD != Cardinal::NONE) {
|
||||
last_input_cardinal_ = INPUT_CARD;
|
||||
}
|
||||
}
|
||||
|
||||
Player::Player(const std::string& anim_key, float x, float y, const Controls& controls, const Map::Info& map_info, int paint_tile_index)
|
||||
@@ -106,22 +255,7 @@ void Player::update(float delta_time) {
|
||||
setVelX(vx);
|
||||
setVelY(vy);
|
||||
|
||||
// Tria de facing amb prevalença lateral: si hi ha component horitzontal
|
||||
// (inclòs el diagonal), l'animació és lateral i el flip segueix el signe
|
||||
// de `vx`. Només en moviment purament vertical passem a UP/DOWN, i en
|
||||
// aquest cas el flip es força a NONE perquè els sprites frontals no
|
||||
// s'han d'espillar segons l'últim lateral. En aturar-se conservem
|
||||
// `facing_` per a l'animació stand corresponent.
|
||||
if (vx != 0.0F) {
|
||||
facing_ = Facing::SIDE;
|
||||
setFlip(vx < 0.0F ? SDL_FLIP_HORIZONTAL : SDL_FLIP_NONE);
|
||||
} else if (vy < 0.0F) {
|
||||
facing_ = Facing::UP;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
} else if (vy > 0.0F) {
|
||||
facing_ = Facing::DOWN;
|
||||
setFlip(SDL_FLIP_NONE);
|
||||
}
|
||||
updateFacingAndTurn(delta_time, vx, vy);
|
||||
|
||||
// Snap al passadís: si només hi ha moviment horitzontal (o vertical) i
|
||||
// la caixa està prou prop d'una lane perpendicular (≤ mitja caixa), la
|
||||
@@ -156,9 +290,12 @@ void Player::update(float delta_time) {
|
||||
|
||||
// Animació basada en moviment real: si la col·lisió (o l'absència
|
||||
// d'input) ha deixat el jugador al mateix lloc que al principi del
|
||||
// frame, queda en `stand` del facing actual.
|
||||
// frame, queda en `stand` del facing actual. Durant un gir de 180°
|
||||
// forcem també `stand` encara que el jugador s'estiga movent — la
|
||||
// pose intermèdia és sempre estàtica per disseny.
|
||||
const bool MOVED = (getPosX() != FRAME_START_X) || (getPosY() != FRAME_START_Y);
|
||||
setCurrentAnimation(animationName(MOVED));
|
||||
const bool ANIM_AS_MOVING = MOVED && (turning_remaining_ms_ <= 0.0F);
|
||||
setCurrentAnimation(animationName(ANIM_AS_MOVING));
|
||||
|
||||
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
|
||||
// col·lisions perquè la posició aquí ja és la definitiva del frame;
|
||||
|
||||
@@ -23,7 +23,8 @@ class Player : public AnimatedSprite {
|
||||
SDL_Scancode right;
|
||||
};
|
||||
|
||||
// Geometria carregada des de `data/player/player.yaml`. `sprite_*` són
|
||||
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
||||
// `sprite_*` són
|
||||
// la mida del frame (sprite 12×24 amb perspectiva falsa: visualment, els
|
||||
// 12 píxels superiors són el cap i els 12 inferiors la base).
|
||||
// La caixa de col·lisió és un quadrat `collision_size` concèntric amb el
|
||||
@@ -121,6 +122,52 @@ class Player : public AnimatedSprite {
|
||||
DOWN };
|
||||
Facing facing_{Facing::SIDE};
|
||||
|
||||
// Cardinal complet (N/S/E/W) usat per a la detecció de girs de 180°.
|
||||
// El parell (Facing, flip) només té 3+1 estats; aquesta enum n'afegeix
|
||||
// la 4a sense reorganitzar el sistema d'animacions.
|
||||
enum class Cardinal : std::uint8_t { NONE,
|
||||
NORTH,
|
||||
SOUTH,
|
||||
EAST,
|
||||
WEST };
|
||||
|
||||
// Configuració del gir de 180°, llegida del bloc `turn:` del YAML del
|
||||
// player. Si `duration_ms` és 0 (o el bloc no existeix) la mecànica
|
||||
// queda desactivada i els canvis de direcció són instantanis.
|
||||
struct TurnConfig {
|
||||
int duration_ms{0};
|
||||
Cardinal w_to_e{Cardinal::SOUTH};
|
||||
Cardinal e_to_w{Cardinal::NORTH};
|
||||
Cardinal n_to_s{Cardinal::EAST};
|
||||
Cardinal s_to_n{Cardinal::WEST};
|
||||
};
|
||||
// Definida out-of-class al .cpp: dins de la classe, els initializers
|
||||
// per defecte de TurnConfig encara no són visibles per al compilador.
|
||||
static TurnConfig turn_config;
|
||||
|
||||
// Estat del gir en curs. Mentre `turning_remaining_ms_ > 0`, el facing/
|
||||
// flip es manté forçat a `intermediate_cardinal_` i l'animació es força
|
||||
// a `stand` encara que el moviment continuï. En esgotar-se el timer,
|
||||
// s'aplica `pending_cardinal_`. `last_input_cardinal_` recorda l'últim
|
||||
// cardinal d'input no nul: serveix per disparar el gir només quan
|
||||
// l'usuari canvia d'opinió, no la primera vegada que prem una tecla
|
||||
// des de l'estat idle.
|
||||
float turning_remaining_ms_{0.0F};
|
||||
Cardinal pending_cardinal_{Cardinal::NONE};
|
||||
Cardinal intermediate_cardinal_{Cardinal::NONE};
|
||||
Cardinal last_input_cardinal_{Cardinal::NONE};
|
||||
|
||||
// Aplica un cardinal a (`facing_`, flip). NONE no fa res — preserva el
|
||||
// facing actual (cas idle: l'última orientació es manté).
|
||||
void applyCardinal(Cardinal c);
|
||||
// Actualitza facing_/flip i l'estat del gir a partir de l'input del frame.
|
||||
// Extret de `update` per acotar la complexitat cognitiva (clang-tidy).
|
||||
void updateFacingAndTurn(float delta_time, float vx, float vy);
|
||||
static auto cardinalFromInput(float vx, float vy) -> Cardinal;
|
||||
static auto cardinalFromString(const std::string& s) -> Cardinal;
|
||||
static auto opposite(Cardinal c) -> Cardinal;
|
||||
static auto intermediateFor(Cardinal target) -> Cardinal;
|
||||
|
||||
// Bus del nivell (opcional). Propietat externa (EntityManager); el
|
||||
// Player només en guarda un punter per publicar events.
|
||||
EventBus* bus_{nullptr};
|
||||
|
||||
Reference in new issue
Block a user