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
+303
-112
No files matched your search
@@ -56,6 +56,18 @@ animations:
|
|||||||
frames: [8, 9, 10, 11]
|
frames: [8, 9, 10, 11]
|
||||||
frame_ms: 100
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt_down:
|
||||||
|
frames: [0, 1, 2, 3]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt_up:
|
||||||
|
frames: [4, 5, 6, 7]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt:
|
||||||
|
frames: [8, 9, 10, 11]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||||
@@ -103,3 +115,15 @@ jump:
|
|||||||
shadow_y_offset: 3
|
shadow_y_offset: 3
|
||||||
sound: jump.wav
|
sound: jump.wav
|
||||||
land_sound: land.wav
|
land_sound: land.wav
|
||||||
|
|
||||||
|
# Estat HURT: el jugador queda projectat sense control per un impacte
|
||||||
|
# (algú que cau al damunt; en el futur també fuetada de l'altre jugador).
|
||||||
|
# `enterHurt` tria automàticament la direcció amb més recorregut lliure
|
||||||
|
# entre els 4 cardinals, posa la velocitat inicial a `speed` i la fa
|
||||||
|
# decaure linealment fins a 0 en `brake_time_ms`. Si pel camí xoca amb
|
||||||
|
# un mur, para en sec. En tornar a velocitat 0, recupera l'estat
|
||||||
|
# GROUNDED i el control. Mentre dura, no col·lisiona amb altres jugadors.
|
||||||
|
# Amb speed=120 i brake_time=500ms ⇒ recorregut ≈ 30 px (≈2 tiles).
|
||||||
|
hurt:
|
||||||
|
speed: 120
|
||||||
|
brake_time_ms: 500
|
||||||
@@ -56,6 +56,18 @@ animations:
|
|||||||
frames: [8, 9, 10, 11]
|
frames: [8, 9, 10, 11]
|
||||||
frame_ms: 100
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt_down:
|
||||||
|
frames: [0, 1, 2, 3]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt_up:
|
||||||
|
frames: [4, 5, 6, 7]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
|
hurt:
|
||||||
|
frames: [8, 9, 10, 11]
|
||||||
|
frame_ms: 100
|
||||||
|
|
||||||
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
# Inèrcia només de frenada. L'acceleració és sempre instantània (soltar
|
||||||
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
# el teclat + prémer una direcció = velocitat plena); en canvi, quan es
|
||||||
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
# solta la direcció el jugador decau linealment a 0 en `brake_time_ms`.
|
||||||
@@ -103,3 +115,15 @@ jump:
|
|||||||
shadow_y_offset: 3
|
shadow_y_offset: 3
|
||||||
sound: jump.wav
|
sound: jump.wav
|
||||||
land_sound: land.wav
|
land_sound: land.wav
|
||||||
|
|
||||||
|
# Estat HURT: el jugador queda projectat sense control per un impacte
|
||||||
|
# (algú que cau al damunt; en el futur també fuetada de l'altre jugador).
|
||||||
|
# `enterHurt` tria automàticament la direcció amb més recorregut lliure
|
||||||
|
# entre els 4 cardinals, posa la velocitat inicial a `speed` i la fa
|
||||||
|
# decaure linealment fins a 0 en `brake_time_ms`. Si pel camí xoca amb
|
||||||
|
# un mur, para en sec. En tornar a velocitat 0, recupera l'estat
|
||||||
|
# GROUNDED i el control. Mentre dura, no col·lisiona amb altres jugadors.
|
||||||
|
# Amb speed=120 i brake_time=500ms ⇒ recorregut ≈ 30 px (≈2 tiles).
|
||||||
|
hurt:
|
||||||
|
speed: 120
|
||||||
|
brake_time_ms: 500
|
||||||
+196
-98
@@ -3,6 +3,7 @@
|
|||||||
#include <SDL3/SDL.h>
|
#include <SDL3/SDL.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
@@ -66,6 +67,7 @@ namespace {
|
|||||||
Player::TurnConfig Player::turn_config;
|
Player::TurnConfig Player::turn_config;
|
||||||
Player::InertiaConfig Player::inertia_config;
|
Player::InertiaConfig Player::inertia_config;
|
||||||
Player::JumpConfig Player::jump_config;
|
Player::JumpConfig Player::jump_config;
|
||||||
|
Player::HurtConfig Player::hurt_config;
|
||||||
|
|
||||||
void Player::loadFrom(const std::string& yaml_path) {
|
void Player::loadFrom(const std::string& yaml_path) {
|
||||||
auto bytes = Resource::Helper::loadFile(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
|
// 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.
|
// complexitat cognitiva de `loadFrom` sota el llindar de clang-tidy.
|
||||||
readJumpBlock(yaml);
|
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) {
|
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) {
|
void Player::applyCardinal(Cardinal c) {
|
||||||
switch (c) {
|
switch (c) {
|
||||||
case Cardinal::NORTH:
|
case Cardinal::NORTH:
|
||||||
@@ -323,7 +337,9 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
|||||||
// acostar-se fins al contacte real.
|
// acostar-se fins al contacte real.
|
||||||
//
|
//
|
||||||
// Regla de col·lisió jugador↔jugador (segons el feel triat):
|
// 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
|
// - Un en l'aire i l'altre no → SENSE col·lisió. El saltador es
|
||||||
// travessa net del caminant; ningú actua de "muro" durant el
|
// travessa net del caminant; ningú actua de "muro" durant el
|
||||||
// vol, ni a la pujada ni a la baixada.
|
// 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.
|
// mateixa franja d'alçada.
|
||||||
const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box);
|
const SDL_FRect PROPOSED_HIT = hitBoxFromBase(box);
|
||||||
const bool MY_AIR = (state_ == State::AIRBORNE);
|
const bool MY_AIR = (state_ == State::AIRBORNE);
|
||||||
|
const bool MY_HURT = (state_ == State::HURT);
|
||||||
const float MY_Z = jump_z_;
|
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 (p == nullptr || p == this) { return false; }
|
||||||
|
if (MY_HURT || p->isHurt()) { return false; }
|
||||||
if (MY_AIR != p->isAirborne()) { return false; }
|
if (MY_AIR != p->isAirborne()) { return false; }
|
||||||
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
// Aquí els dos estats coincideixen. Si tots dos volen, el filtre
|
||||||
// per altura mira que els rangs verticals encara solapin; si no,
|
// per altura mira que els rangs verticals encara solapin; si no,
|
||||||
@@ -422,6 +440,36 @@ void Player::tick(float delta_time) {
|
|||||||
return;
|
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
|
// Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el
|
||||||
// service menu (o qualsevol UI modal) està obert, el wrapper retorna
|
// 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
|
// 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.
|
// 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 {
|
auto Player::integrateJump(float delta_time) -> bool {
|
||||||
if (state_ != State::AIRBORNE) { return false; }
|
if (state_ != State::AIRBORNE) { return false; }
|
||||||
jump_vz_ -= jump_config.gravity_px_per_s2 * delta_time;
|
jump_vz_ -= jump_config.gravity_px_per_s2 * delta_time;
|
||||||
@@ -540,73 +661,22 @@ auto Player::integrateJump(float delta_time) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void Player::resolveLandingOverlap() {
|
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; }
|
if (all_players_ == nullptr) { return; }
|
||||||
for (Player* other : *all_players_) {
|
for (Player* other : *all_players_) {
|
||||||
if (other == nullptr || other == this) { continue; }
|
if (other == nullptr || other == this) { continue; }
|
||||||
// Usem la BASE-box (16×16, el cos sencer) per al test, no la
|
// Test amb la BASE-box (cos sencer): si els cossos solapen,
|
||||||
// hit-box (8×8 de contacte). Volem separar els cossos
|
// l'altre es projecta. Usem la base-box i no la hit-box perquè
|
||||||
// visualment: si fem servir només la hit-box, dos jugadors
|
// ens importa la separació visual completa, no només els
|
||||||
// poden quedar amb les base-boxes solapades a mig sprite (un
|
// píxels de contacte.
|
||||||
// dins de l'altre) tot i tenir les hit-boxes ja apartades. La
|
|
||||||
// base-box dóna la separació real del cos.
|
|
||||||
const SDL_FRect MY_BOX = getBaseBox();
|
const SDL_FRect MY_BOX = getBaseBox();
|
||||||
const SDL_FRect OT_BOX = other->getBaseBox();
|
const SDL_FRect OT_BOX = other->getBaseBox();
|
||||||
SDL_FRect inter;
|
if (!SDL_HasRectIntersectionFloat(&MY_BOX, &OT_BOX)) { continue; }
|
||||||
if (!SDL_GetRectIntersectionFloat(&MY_BOX, &OT_BOX, &inter)) { continue; }
|
other->enterHurt();
|
||||||
|
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -621,25 +691,13 @@ void Player::applyMovement(float delta_time) {
|
|||||||
resolveLandingOverlap();
|
resolveLandingOverlap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// En LANDING_STUN no movem el jugador horitzontalment (la velocitat
|
if (state_ == State::HURT) {
|
||||||
// ja s'ha posat a 0 al `tick`). Encara així animem (per estar al
|
applyHurtMovement(delta_time);
|
||||||
// frame correcte de la posada estàtica) i emetem el tile actual,
|
return;
|
||||||
// perquè la tile d'aterratge sí que es pinte.
|
}
|
||||||
|
|
||||||
if (state_ == State::LANDING_STUN) {
|
if (state_ == State::LANDING_STUN) {
|
||||||
animate(delta_time);
|
applyLandingStunMovement(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_,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,21 +747,49 @@ void Player::applyMovement(float delta_time) {
|
|||||||
const bool ANIM_AS_MOVING = MOVED && (turning_remaining_ms_ <= 0.0F) && !being_pushed_;
|
const bool ANIM_AS_MOVING = MOVED && (turning_remaining_ms_ <= 0.0F) && !being_pushed_;
|
||||||
setCurrentAnimation(animationName(ANIM_AS_MOVING));
|
setCurrentAnimation(animationName(ANIM_AS_MOVING));
|
||||||
|
|
||||||
// Detecció de canvi de tile. Es calcula *després* del clamp i de les
|
// Detecció de canvi de tile. Només publiquem en GROUNDED — en
|
||||||
// col·lisions perquè la posició aquí ja és la definitiva del frame;
|
// AIRBORNE el jugador no trepitja, i en HURT/LANDING_STUN les
|
||||||
// així el PaintLayer no rep falsos positius mentre el Player rellisca
|
// pròpies branques del switch ja crida la helper si toca.
|
||||||
// contra una paret. Si el tile ha canviat, publiquem `on_tile_entered`
|
if (state_ == State::GROUNDED) { publishTileEnteredIfChanged(); }
|
||||||
// al bus amb l'índex de pintat d'aquest jugador; el PaintLayer filtra
|
}
|
||||||
// pintabilitat i re-publica `tile_painted` als sarcòfags.
|
|
||||||
//
|
void Player::applyHurtMovement(float delta_time) {
|
||||||
// En AIRBORNE NO publiquem cap pintat — el jugador està en l'aire i
|
// En HURT el jugador es desplaça segons `intended_v*` (escrit a
|
||||||
// no està trepitjant cap tile. NI tampoc actualitzem `last_tile_*`,
|
// `enterHurt` i decremental a `tick`). Si `tryMove` no avança
|
||||||
// perquè volem que el tile d'aterratge (que es comprova al
|
// perquè topa amb un mur, el parem en sec i tornem a GROUNDED —
|
||||||
// LANDING_STUN) compari contra l'últim tile real (el del despegue) i
|
// honra la regla "salvo col·lisió, llavors para".
|
||||||
// s'emeti com a entrada nova si han canviat.
|
animate(delta_time);
|
||||||
if (bus_ != nullptr && state_ == State::GROUNDED) {
|
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();
|
const auto [COL, ROW] = getCurrentTile();
|
||||||
if (COL != last_tile_col_ || ROW != last_tile_row_) {
|
if (COL == last_tile_col_ && ROW == last_tile_row_) { return; }
|
||||||
last_tile_col_ = COL;
|
last_tile_col_ = COL;
|
||||||
last_tile_row_ = ROW;
|
last_tile_row_ = ROW;
|
||||||
bus_->publish(Event{
|
bus_->publish(Event{
|
||||||
@@ -711,8 +797,6 @@ void Player::applyMovement(float delta_time) {
|
|||||||
.payload = PaintedTile{.col = COL, .row = ROW, .tile_index = paint_tile_index_},
|
.payload = PaintedTile{.col = COL, .row = ROW, .tile_index = paint_tile_index_},
|
||||||
.player_id = id_,
|
.player_id = id_,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Player::render() {
|
void Player::render() {
|
||||||
@@ -799,6 +883,20 @@ auto Player::animationName(bool moving) const -> const char* {
|
|||||||
}
|
}
|
||||||
return "stun";
|
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_) {
|
switch (facing_) {
|
||||||
case Facing::UP:
|
case Facing::UP:
|
||||||
return moving ? "walk_up" : "stand_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`
|
// Estat d'alt nivell del jugador. Ortogonal a `Facing`/`Cardinal`
|
||||||
// (direcció) i a `being_pushed_` (flag d'animació). Determina què pot
|
// (direcció) i a `being_pushed_` (flag d'animació). Determina què pot
|
||||||
// fer el jugador aquest frame: GROUNDED accepta tot l'input habitual
|
// fer el jugador aquest frame:
|
||||||
// i el botó de salt; AIRBORNE ignora canvis de direcció (la velocitat
|
// GROUNDED → input normal, pot saltar.
|
||||||
// i el facing queden fixats al moment del despegue, sols la física
|
// AIRBORNE → física del salt; ignora input direccional, sense
|
||||||
// del salt mou jump_z_/jump_vz_); LANDING_STUN bloca tot l'input fins
|
// col·lisió amb caminants però sí amb altres
|
||||||
// que el timer s'esgota.
|
// 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 {
|
enum class State : std::uint8_t {
|
||||||
GROUNDED,
|
GROUNDED,
|
||||||
AIRBORNE,
|
AIRBORNE,
|
||||||
LANDING_STUN,
|
LANDING_STUN,
|
||||||
|
HURT,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Geometria carregada des del YAML del player (p.ex. `data/player/sam.yaml`).
|
// 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).
|
// GameScene (per saber si renderitzar la sombra).
|
||||||
[[nodiscard]] auto getState() const -> State { return state_; }
|
[[nodiscard]] auto getState() const -> State { return state_; }
|
||||||
[[nodiscard]] auto isAirborne() const -> bool { return state_ == State::AIRBORNE; }
|
[[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).
|
// 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
|
// 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.
|
// manca, deixa la mecànica del salt desactivada.
|
||||||
static void readJumpBlock(const fkyaml::node& yaml);
|
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
|
// Estat de la màquina d'estats. Mutat per `tick`/`applyMovement`. Per
|
||||||
// defecte GROUNDED al spawn.
|
// defecte GROUNDED al spawn.
|
||||||
State state_{State::GROUNDED};
|
State state_{State::GROUNDED};
|
||||||
@@ -418,17 +452,23 @@ class Player : public AnimatedSprite {
|
|||||||
// `applyMovement` puga disparar la resolució del solapament PvP).
|
// `applyMovement` puga disparar la resolució del solapament PvP).
|
||||||
auto integrateJump(float delta_time) -> bool;
|
auto integrateJump(float delta_time) -> bool;
|
||||||
|
|
||||||
// En aterrar, si la hit-box del jugador solapa amb la d'un altre,
|
// En aterrar, si la base-box del jugador solapa amb la d'un altre,
|
||||||
// separa els dos sense ficar-los dins d'un mur. Algoritme:
|
// l'altre entra en `HURT` i surt projectat per `enterHurt`. El
|
||||||
// 1. Calcula la direcció centre→centre i la magnitud de
|
// saltador (this) es queda al lloc — el moviment per separar-los
|
||||||
// solapament (MTV axis-aligned).
|
// el fa el HURT en cours, no una empenta forçada.
|
||||||
// 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.
|
|
||||||
void resolveLandingOverlap();
|
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/
|
// 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
|
// 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,
|
// a `stand` encara que el moviment continuï. En esgotar-se el timer,
|
||||||
|
|||||||
@@ -106,6 +106,11 @@ namespace {
|
|||||||
const bool A_AIR = a->isAirborne();
|
const bool A_AIR = a->isAirborne();
|
||||||
const bool B_AIR = b->isAirborne();
|
const bool B_AIR = b->isAirborne();
|
||||||
|
|
||||||
|
// HURT a un costat o l'altre: el resolver no s'activa — el HURT
|
||||||
|
// surt projectat sense control i els dos es travessen. Coincideix
|
||||||
|
// amb la regla d'`Player::collidesWithWorld`.
|
||||||
|
if (a->isHurt() || b->isHurt()) { return; }
|
||||||
|
|
||||||
// Aire ↔ tierra: el saltador es travessa net del caminant. Sense
|
// Aire ↔ tierra: el saltador es travessa net del caminant. Sense
|
||||||
// resolver, sense head-on, sense push. Els dos continuen amb la
|
// resolver, sense head-on, sense push. Els dos continuen amb la
|
||||||
// velocitat que portaven. Important: això NO és el filtre per
|
// velocitat que portaven. Important: això NO és el filtre per
|
||||||
|
|||||||
Reference in new issue
Block a user