feat(player): nou estat HURT — caure damunt d'un altre el llança projectat
This commit is contained in:
1 parent
188f035333
commit
e8407ce35e
5 files changed
+310
-119
No files matched your search
+203
-105
@@ -3,6 +3,7 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <stdexcept>
|
||||
#include <utility>
|
||||
@@ -66,6 +67,7 @@ namespace {
|
||||
Player::TurnConfig Player::turn_config;
|
||||
Player::InertiaConfig Player::inertia_config;
|
||||
Player::JumpConfig Player::jump_config;
|
||||
Player::HurtConfig Player::hurt_config;
|
||||
|
||||
void Player::loadFrom(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
@@ -108,6 +110,10 @@ void Player::loadFrom(const std::string& yaml_path) {
|
||||
// botó de salt no fa res. Extret a un mètode estàtic per acotar la
|
||||
// complexitat cognitiva de `loadFrom` sota el llindar de clang-tidy.
|
||||
readJumpBlock(yaml);
|
||||
|
||||
// Bloc opcional `hurt:` — estat post-impacte. Si manca o el speed
|
||||
// és 0, l'estat HURT mai s'activa (cas degenerat).
|
||||
readHurtBlock(yaml);
|
||||
}
|
||||
|
||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
||||
@@ -161,6 +167,14 @@ void Player::readJumpBlock(const fkyaml::node& yaml) {
|
||||
}
|
||||
}
|
||||
|
||||
void Player::readHurtBlock(const fkyaml::node& yaml) {
|
||||
hurt_config = HurtConfig{};
|
||||
if (!yaml.contains("hurt")) { return; }
|
||||
const auto& h = yaml["hurt"];
|
||||
if (h.contains("speed")) { hurt_config.initial_speed_px_per_s = h["speed"].get_value<float>(); }
|
||||
if (h.contains("brake_time_ms")) { hurt_config.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
|
||||
}
|
||||
|
||||
void Player::applyCardinal(Cardinal c) {
|
||||
switch (c) {
|
||||
case Cardinal::NORTH:
|
||||
@@ -323,7 +337,9 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||
// acostar-se fins al contacte real.
|
||||
//
|
||||
// Regla de col·lisió jugador↔jugador (segons el feel triat):
|
||||
// - Tots dos a terra → col·lisió normal.
|
||||
// - HURT a un costat o a l'altre → cap col·lisió. El jugador
|
||||
// impactat es travessa net.
|
||||
// - Tots dos a terra (PvP-tangibles) → col·lisió normal.
|
||||
// - Un en l'aire i l'altre no → SENSE col·lisió. El saltador es
|
||||
// travessa net del caminant; ningú actua de "muro" durant el
|
||||
// vol, ni a la pujada ni a la baixada.
|
||||
@@ -333,9 +349,11 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||
// mateixa franja d'alçada.
|
||||
const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box);
|
||||
const bool MY_AIR = (state_ == State::AIRBORNE);
|
||||
const bool MY_HURT = (state_ == State::HURT);
|
||||
const float MY_Z = jump_z_;
|
||||
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_Z](const Player* p) {
|
||||
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_HURT, MY_Z](const Player* p) {
|
||||
if (p == nullptr || p == this) { return false; }
|
||||
if (MY_HURT || p->isHurt()) { return false; }
|
||||
if (MY_AIR != p->isAirborne()) { return false; }
|
||||
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
||||
// per altura mira que els rangs verticals encara solapin; si no,
|
||||
@@ -422,6 +440,36 @@ void Player::tick(float delta_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
// En HURT el jugador està projectat sense control: descomptem
|
||||
// velocitat per inèrcia (decaiment lineal fins a 0). Quan arriba a
|
||||
// zero, tornem a GROUNDED i el frame següent ja torna a llegir input.
|
||||
// El moviment real l'aplica `applyMovement` (igual que els altres
|
||||
// estats), que també gestiona el xoc contra murs.
|
||||
if (state_ == State::HURT) {
|
||||
(void)consumeJumpEdge();
|
||||
const float DECEL = (hurt_config.brake_time_ms > 0)
|
||||
? hurt_config.initial_speed_px_per_s / (static_cast<float>(hurt_config.brake_time_ms) / 1000.0F)
|
||||
: 0.0F;
|
||||
const float STEP = DECEL * delta_time;
|
||||
auto decay = [STEP](float& v) {
|
||||
if (std::fabs(v) <= STEP) {
|
||||
v = 0.0F;
|
||||
} else {
|
||||
v -= std::copysign(STEP, v);
|
||||
}
|
||||
};
|
||||
decay(intended_vx_);
|
||||
decay(intended_vy_);
|
||||
if (intended_vx_ == 0.0F && intended_vy_ == 0.0F) {
|
||||
state_ = State::GROUNDED;
|
||||
}
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
setVelX(intended_vx_);
|
||||
setVelY(intended_vy_);
|
||||
return;
|
||||
}
|
||||
|
||||
// Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el
|
||||
// service menu (o qualsevol UI modal) està obert, el wrapper retorna
|
||||
// un buffer tot-a-zero perquè el jugador no es mogui mentre l'usuari
|
||||
@@ -522,6 +570,79 @@ void Player::startJump() {
|
||||
// tocaran fins a aterrar.
|
||||
}
|
||||
|
||||
void Player::enterHurt() {
|
||||
// Si la mecànica no està configurada (speed = 0), no fem res — el
|
||||
// comportament queda equivalent al d'abans, sense empenta.
|
||||
if (hurt_config.initial_speed_px_per_s <= 0.0F || hurt_config.brake_time_ms <= 0) { return; }
|
||||
|
||||
// Triem la direcció amb més recorregut lliure: provem els 4
|
||||
// cardinals i ens quedem el de major distància contra muros/sarcs.
|
||||
// El test no comprova jugadors (durant HURT el jugador és
|
||||
// intangible al PvP), només geometria del mapa.
|
||||
constexpr int MAX_PX = 32; // 2 tiles, prou per a la cerca; la inèrcia limita el recorregut real.
|
||||
struct Candidate {
|
||||
Cardinal dir;
|
||||
float dx;
|
||||
float dy;
|
||||
};
|
||||
constexpr std::array<Candidate, 4> ALL{{
|
||||
{.dir = Cardinal::EAST, .dx = 1.0F, .dy = 0.0F},
|
||||
{.dir = Cardinal::WEST, .dx = -1.0F, .dy = 0.0F},
|
||||
{.dir = Cardinal::NORTH, .dx = 0.0F, .dy = -1.0F},
|
||||
{.dir = Cardinal::SOUTH, .dx = 0.0F, .dy = 1.0F},
|
||||
}};
|
||||
const auto BASE = getBaseBox();
|
||||
int best_reach = -1;
|
||||
Cardinal best_dir = Cardinal::EAST;
|
||||
for (const auto& c : ALL) {
|
||||
int reach = MAX_PX;
|
||||
for (int i = 1; i <= MAX_PX; ++i) {
|
||||
SDL_FRect probe = BASE;
|
||||
probe.x += static_cast<float>(i) * c.dx;
|
||||
probe.y += static_cast<float>(i) * c.dy;
|
||||
if (map_.collides(probe) || !map_.inside_playfield(probe)) {
|
||||
reach = i - 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (reach > best_reach) {
|
||||
best_reach = reach;
|
||||
best_dir = c.dir;
|
||||
}
|
||||
}
|
||||
|
||||
// Activem l'estat i fixem la velocitat segons la direcció triada.
|
||||
state_ = State::HURT;
|
||||
applyCardinal(best_dir);
|
||||
last_input_cardinal_ = best_dir;
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
pending_cardinal_ = Cardinal::NONE;
|
||||
intermediate_cardinal_ = Cardinal::NONE;
|
||||
being_pushed_ = false;
|
||||
const float V = hurt_config.initial_speed_px_per_s;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
switch (best_dir) {
|
||||
case Cardinal::EAST:
|
||||
intended_vx_ = V;
|
||||
break;
|
||||
case Cardinal::WEST:
|
||||
intended_vx_ = -V;
|
||||
break;
|
||||
case Cardinal::NORTH:
|
||||
intended_vy_ = -V;
|
||||
break;
|
||||
case Cardinal::SOUTH:
|
||||
intended_vy_ = V;
|
||||
break;
|
||||
case Cardinal::NONE:
|
||||
break;
|
||||
}
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
motion_time_ms_ = 0.0F;
|
||||
}
|
||||
|
||||
auto Player::integrateJump(float delta_time) -> bool {
|
||||
if (state_ != State::AIRBORNE) { return false; }
|
||||
jump_vz_ -= jump_config.gravity_px_per_s2 * delta_time;
|
||||
@@ -540,73 +661,22 @@ auto Player::integrateJump(float delta_time) -> bool {
|
||||
}
|
||||
|
||||
void Player::resolveLandingOverlap() {
|
||||
// Quan el saltador (this) aterra al damunt d'un altre jugador, el
|
||||
// saltador NO es desplaça — es queda al lloc en LANDING_STUN. En
|
||||
// canvi, qualsevol jugador amb el qual solape entra en HURT i
|
||||
// surt projectat. Així el solapament inicial es resol per moviment
|
||||
// del solapat, no per desplaçament forçat al lloc.
|
||||
if (all_players_ == nullptr) { return; }
|
||||
for (Player* other : *all_players_) {
|
||||
if (other == nullptr || other == this) { continue; }
|
||||
// Usem la BASE-box (16×16, el cos sencer) per al test, no la
|
||||
// hit-box (8×8 de contacte). Volem separar els cossos
|
||||
// visualment: si fem servir només la hit-box, dos jugadors
|
||||
// poden quedar amb les base-boxes solapades a mig sprite (un
|
||||
// dins de l'altre) tot i tenir les hit-boxes ja apartades. La
|
||||
// base-box dóna la separació real del cos.
|
||||
// Test amb la BASE-box (cos sencer): si els cossos solapen,
|
||||
// l'altre es projecta. Usem la base-box i no la hit-box perquè
|
||||
// ens importa la separació visual completa, no només els
|
||||
// píxels de contacte.
|
||||
const SDL_FRect MY_BOX = getBaseBox();
|
||||
const SDL_FRect OT_BOX = other->getBaseBox();
|
||||
SDL_FRect inter;
|
||||
if (!SDL_GetRectIntersectionFloat(&MY_BOX, &OT_BOX, &inter)) { continue; }
|
||||
|
||||
// Eix de separació: el que té els centres més separats. És
|
||||
// l'axis-aligned MTV "natural" — equivalent a empentar l'altre
|
||||
// cap a fora seguint la direcció centre→centre, projectada al
|
||||
// costat dominant. Coincideix amb la intuïció: si el saltador
|
||||
// li cau una mica per sobre i a la dreta, l'altre va cap a la
|
||||
// dreta-baix; si li cau directament al damunt (dx≈0), l'altre
|
||||
// s'aparta verticalment.
|
||||
const float MCX = MY_BOX.x + (MY_BOX.w * 0.5F);
|
||||
const float MCY = MY_BOX.y + (MY_BOX.h * 0.5F);
|
||||
const float OCX = OT_BOX.x + (OT_BOX.w * 0.5F);
|
||||
const float OCY = OT_BOX.y + (OT_BOX.h * 0.5F);
|
||||
const float DX = OCX - MCX;
|
||||
const float DY = OCY - MCY;
|
||||
float push_x = 0.0F;
|
||||
float push_y = 0.0F;
|
||||
if (std::fabs(DX) >= std::fabs(DY)) {
|
||||
push_x = (DX >= 0.0F ? 1.0F : -1.0F) * inter.w;
|
||||
} else {
|
||||
push_y = (DY >= 0.0F ? 1.0F : -1.0F) * inter.h;
|
||||
}
|
||||
|
||||
// 1r intent: empentar l'altre. Si la nova posició no col·lisiona
|
||||
// amb res (mur, sarc, tercer jugador), commit i marquem-lo com
|
||||
// a empentat per coherència de l'animació de l'altre frame.
|
||||
const float OSX = other->getPosX();
|
||||
const float OSY = other->getPosY();
|
||||
other->setPosX(OSX + push_x);
|
||||
other->setPosY(OSY + push_y);
|
||||
if (!other->collidesWithWorld(other->getBaseBox())) {
|
||||
other->setBeingPushed(true);
|
||||
continue;
|
||||
}
|
||||
|
||||
// L'altre xocaria amb un mur: revertim i provem d'apartar el
|
||||
// saltador cap al sentit oposat. Així respectem la regla "mai
|
||||
// ficar ningú dins d'un mur": si no hi ha camí per a l'altre,
|
||||
// qui es desplaça és el qui ha triat saltar.
|
||||
other->setPosX(OSX);
|
||||
other->setPosY(OSY);
|
||||
const float MSX = getPosX();
|
||||
const float MSY = getPosY();
|
||||
setPosX(MSX - push_x);
|
||||
setPosY(MSY - push_y);
|
||||
if (!collidesWithWorld(getBaseBox())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cap dels dos pot apartar-se sense xocar amb un mur. Cas
|
||||
// degenerat (jugador encaixonat entre obstacles); el deixem com
|
||||
// estava i acceptem el solapament residual fins al pròxim frame
|
||||
// amb input.
|
||||
setPosX(MSX);
|
||||
setPosY(MSY);
|
||||
if (!SDL_HasRectIntersectionFloat(&MY_BOX, &OT_BOX)) { continue; }
|
||||
other->enterHurt();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,25 +691,13 @@ void Player::applyMovement(float delta_time) {
|
||||
resolveLandingOverlap();
|
||||
}
|
||||
|
||||
// En LANDING_STUN no movem el jugador horitzontalment (la velocitat
|
||||
// ja s'ha posat a 0 al `tick`). Encara així animem (per estar al
|
||||
// frame correcte de la posada estàtica) i emetem el tile actual,
|
||||
// perquè la tile d'aterratge sí que es pinte.
|
||||
if (state_ == State::HURT) {
|
||||
applyHurtMovement(delta_time);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state_ == State::LANDING_STUN) {
|
||||
animate(delta_time);
|
||||
setCurrentAnimation(animationName(false));
|
||||
if (bus_ != nullptr) {
|
||||
const auto [COL, ROW] = getCurrentTile();
|
||||
if (COL != last_tile_col_ || ROW != last_tile_row_) {
|
||||
last_tile_col_ = COL;
|
||||
last_tile_row_ = ROW;
|
||||
bus_->publish(Event{
|
||||
.name = "on_tile_entered",
|
||||
.payload = PaintedTile{.col = COL, .row = ROW, .tile_index = paint_tile_index_},
|
||||
.player_id = id_,
|
||||
});
|
||||
}
|
||||
}
|
||||
applyLandingStunMovement(delta_time);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -689,30 +747,56 @@ void Player::applyMovement(float delta_time) {
|
||||
const bool ANIM_AS_MOVING = MOVED && (turning_remaining_ms_ <= 0.0F) && !being_pushed_;
|
||||
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;
|
||||
// així el PaintLayer no rep falsos positius mentre el Player rellisca
|
||||
// contra una paret. Si el tile ha canviat, publiquem `on_tile_entered`
|
||||
// al bus amb l'índex de pintat d'aquest jugador; el PaintLayer filtra
|
||||
// pintabilitat i re-publica `tile_painted` als sarcòfags.
|
||||
//
|
||||
// En AIRBORNE NO publiquem cap pintat — el jugador està en l'aire i
|
||||
// no està trepitjant cap tile. NI tampoc actualitzem `last_tile_*`,
|
||||
// perquè volem que el tile d'aterratge (que es comprova al
|
||||
// LANDING_STUN) compari contra l'últim tile real (el del despegue) i
|
||||
// s'emeti com a entrada nova si han canviat.
|
||||
if (bus_ != nullptr && state_ == State::GROUNDED) {
|
||||
const auto [COL, ROW] = getCurrentTile();
|
||||
if (COL != last_tile_col_ || ROW != last_tile_row_) {
|
||||
last_tile_col_ = COL;
|
||||
last_tile_row_ = ROW;
|
||||
bus_->publish(Event{
|
||||
.name = "on_tile_entered",
|
||||
.payload = PaintedTile{.col = COL, .row = ROW, .tile_index = paint_tile_index_},
|
||||
.player_id = id_,
|
||||
});
|
||||
}
|
||||
// Detecció de canvi de tile. Només publiquem en GROUNDED — en
|
||||
// AIRBORNE el jugador no trepitja, i en HURT/LANDING_STUN les
|
||||
// pròpies branques del switch ja crida la helper si toca.
|
||||
if (state_ == State::GROUNDED) { publishTileEnteredIfChanged(); }
|
||||
}
|
||||
|
||||
void Player::applyHurtMovement(float delta_time) {
|
||||
// En HURT el jugador es desplaça segons `intended_v*` (escrit a
|
||||
// `enterHurt` i decremental a `tick`). Si `tryMove` no avança
|
||||
// perquè topa amb un mur, el parem en sec i tornem a GROUNDED —
|
||||
// honra la regla "salvo col·lisió, llavors para".
|
||||
animate(delta_time);
|
||||
const float FRAME_START_X = getPosX();
|
||||
const float FRAME_START_Y = getPosY();
|
||||
tryMove(intended_vx_ * delta_time, intended_vy_ * delta_time, [this](const SDL_FRect& box) {
|
||||
return collidesWithWorld(box);
|
||||
});
|
||||
clampToPlayfield();
|
||||
const bool MOVED = (getPosX() != FRAME_START_X) || (getPosY() != FRAME_START_Y);
|
||||
const bool WANTED_TO_MOVE = (intended_vx_ != 0.0F) || (intended_vy_ != 0.0F);
|
||||
if (!MOVED && WANTED_TO_MOVE) {
|
||||
// Bloquejat per un mur → parem en sec i recuperem el control.
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
state_ = State::GROUNDED;
|
||||
}
|
||||
setCurrentAnimation(animationName(true));
|
||||
publishTileEnteredIfChanged();
|
||||
}
|
||||
|
||||
void Player::applyLandingStunMovement(float delta_time) {
|
||||
// En LANDING_STUN no movem (la velocitat ja és 0). Animem per
|
||||
// mantenir el frame del stun i emetem la tile d'aterratge perquè
|
||||
// es pinte.
|
||||
animate(delta_time);
|
||||
setCurrentAnimation(animationName(false));
|
||||
publishTileEnteredIfChanged();
|
||||
}
|
||||
|
||||
void Player::publishTileEnteredIfChanged() {
|
||||
if (bus_ == nullptr) { return; }
|
||||
const auto [COL, ROW] = getCurrentTile();
|
||||
if (COL == last_tile_col_ && ROW == last_tile_row_) { return; }
|
||||
last_tile_col_ = COL;
|
||||
last_tile_row_ = ROW;
|
||||
bus_->publish(Event{
|
||||
.name = "on_tile_entered",
|
||||
.payload = PaintedTile{.col = COL, .row = ROW, .tile_index = paint_tile_index_},
|
||||
.player_id = id_,
|
||||
});
|
||||
}
|
||||
|
||||
void Player::render() {
|
||||
@@ -799,6 +883,20 @@ auto Player::animationName(bool moving) const -> const char* {
|
||||
}
|
||||
return "stun";
|
||||
}
|
||||
// En HURT, `hurt_*` (per direcció): el jugador llisca sense
|
||||
// control en la direcció triada per `enterHurt`. Mateixa estructura
|
||||
// que walk per facilitat (ara mateix els frames són clons).
|
||||
if (state_ == State::HURT) {
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return "hurt_up";
|
||||
case Facing::DOWN:
|
||||
return "hurt_down";
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
return "hurt";
|
||||
}
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return moving ? "walk_up" : "stand_up";
|
||||
|
||||
@@ -28,15 +28,22 @@ class Player : public AnimatedSprite {
|
||||
|
||||
// Estat d'alt nivell del jugador. Ortogonal a `Facing`/`Cardinal`
|
||||
// (direcció) i a `being_pushed_` (flag d'animació). Determina què pot
|
||||
// fer el jugador aquest frame: GROUNDED accepta tot l'input habitual
|
||||
// i el botó de salt; AIRBORNE ignora canvis de direcció (la velocitat
|
||||
// i el facing queden fixats al moment del despegue, sols la física
|
||||
// del salt mou jump_z_/jump_vz_); LANDING_STUN bloca tot l'input fins
|
||||
// que el timer s'esgota.
|
||||
// fer el jugador aquest frame:
|
||||
// GROUNDED → input normal, pot saltar.
|
||||
// AIRBORNE → física del salt; ignora input direccional, sense
|
||||
// col·lisió amb caminants però sí amb altres
|
||||
// saltadors a la mateixa franja d'alçada (rebot).
|
||||
// LANDING_STUN → 200ms post-aterratge, sense input ni moviment.
|
||||
// HURT → projectat sense control (cau d'algú al damunt o,
|
||||
// en el futur, fuetada): velocitat inicial alta
|
||||
// que decau per inèrcia fins a parar (~2 tiles).
|
||||
// No col·lisiona amb cap altre jugador. Quan
|
||||
// s'atura, torna a GROUNDED.
|
||||
enum class State : std::uint8_t {
|
||||
GROUNDED,
|
||||
AIRBORNE,
|
||||
LANDING_STUN,
|
||||
HURT,
|
||||
};
|
||||
|
||||
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
||||
@@ -169,6 +176,12 @@ class Player : public AnimatedSprite {
|
||||
// GameScene (per saber si renderitzar la sombra).
|
||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
||||
[[nodiscard]] auto isHurt() const -> bool { return state_ == State::HURT; }
|
||||
// Tangible per a contactes jugador↔jugador: només els estats a terra
|
||||
// amb cos sòlid. AIRBORNE i HURT són intangibles (es travessen).
|
||||
[[nodiscard]] auto isPvpTangible() const -> bool {
|
||||
return state_ == State::GROUNDED || state_ == State::LANDING_STUN;
|
||||
}
|
||||
|
||||
// Alçada actual del salt (píxels visuals per damunt del sòl).
|
||||
// 0 quan està a terra. L'usa el render i el sistema de col·lisions
|
||||
@@ -385,6 +398,27 @@ class Player : public AnimatedSprite {
|
||||
// manca, deixa la mecànica del salt desactivada.
|
||||
static void readJumpBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració de l'estat HURT (estat post-impacte). Si
|
||||
// `initial_speed_px_per_s == 0` el jugador entra i surt
|
||||
// instantàniament (cap moviment).
|
||||
struct HurtConfig {
|
||||
float initial_speed_px_per_s{0.0F}; // Velocitat amb què surt projectat (px/s).
|
||||
int brake_time_ms{0}; // Temps en que la velocitat decau a 0.
|
||||
};
|
||||
static HurtConfig hurt_config;
|
||||
|
||||
// Llig el bloc `hurt:` del YAML i n'omple `hurt_config`.
|
||||
static void readHurtBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Activa l'estat HURT: el jugador queda projectat amb velocitat
|
||||
// inicial `hurt_config.initial_speed_px_per_s` cap a la direcció que
|
||||
// doni més recorregut lliure (provem els 4 cardinals i triem el de
|
||||
// major distància fins a topar amb un mur). Decelera linealment fins
|
||||
// a parar i, en eixe instant, torna a GROUNDED. Sense col·lisió amb
|
||||
// altres jugadors mentre dura — ja que es travessa l'eventual
|
||||
// solapament inicial.
|
||||
void enterHurt();
|
||||
|
||||
// Estat de la màquina d'estats. Mutat per `tick`/`applyMovement`. Per
|
||||
// defecte GROUNDED al spawn.
|
||||
State state_{State::GROUNDED};
|
||||
@@ -418,17 +452,23 @@ class Player : public AnimatedSprite {
|
||||
// `applyMovement` puga disparar la resolució del solapament PvP).
|
||||
auto integrateJump(float delta_time) -> bool;
|
||||
|
||||
// En aterrar, si la hit-box del jugador solapa amb la d'un altre,
|
||||
// separa els dos sense ficar-los dins d'un mur. Algoritme:
|
||||
// 1. Calcula la direcció centre→centre i la magnitud de
|
||||
// solapament (MTV axis-aligned).
|
||||
// 2. Empenta l'altre cap a fora; si no cap (mur al darrere),
|
||||
// reverteix i empenta el saltador cap al sentit oposat.
|
||||
// 3. Si tampoc cap, es deixa l'estat com està (overlap residual);
|
||||
// a la pràctica aquest cas és molt rar i el usuari ja ha
|
||||
// acceptat que no és crític.
|
||||
// En aterrar, si la base-box del jugador solapa amb la d'un altre,
|
||||
// l'altre entra en `HURT` i surt projectat per `enterHurt`. El
|
||||
// saltador (this) es queda al lloc — el moviment per separar-los
|
||||
// el fa el HURT en cours, no una empenta forçada.
|
||||
void resolveLandingOverlap();
|
||||
|
||||
// Branques de `applyMovement` extretes per acotar la complexitat
|
||||
// cognitiva. Cada estat especial gestiona el seu propi cicle
|
||||
// (animació, tryMove o no, publicació de tile).
|
||||
void applyHurtMovement(float delta_time);
|
||||
void applyLandingStunMovement(float delta_time);
|
||||
|
||||
// Publica `on_tile_entered` al bus si el tile ha canviat respecte a
|
||||
// l'últim publicat. Comú a totes les branques de `applyMovement` que
|
||||
// pinten el sòl (GROUNDED, HURT, LANDING_STUN; en AIRBORNE no).
|
||||
void publishTileEnteredIfChanged();
|
||||
|
||||
// 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,
|
||||
|
||||
Reference in new issue
Block a user