feat(player): mort i respawn (DYING + death_spinning + lives counter)
This commit is contained in:
1 parent
6397f9d4eb
commit
9f0a566ffb
11 files changed
+377
-7
No files matched your search
@@ -69,6 +69,7 @@ Player::InertiaConfig Player::inertia_config;
|
||||
Player::JumpConfig Player::jump_config;
|
||||
Player::HurtConfig Player::hurt_config;
|
||||
Player::WhipConfig Player::whip_config;
|
||||
Player::DeathConfig Player::death_config;
|
||||
|
||||
void Player::loadFrom(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
@@ -120,6 +121,11 @@ void Player::loadFrom(const std::string& yaml_path) {
|
||||
// jugador no atacarà efectivament (ATTACKING dura 0 ms i el flanc
|
||||
// d'input es perd).
|
||||
readWhipBlock(yaml);
|
||||
|
||||
// Bloc opcional `death:` — pausa de respawn i comptador inicial de
|
||||
// vides. Si manca, el respawn és immediat i les vides queden a 3
|
||||
// per defecte (no és un valor crític: avui no hi ha game over).
|
||||
readDeathBlock(yaml);
|
||||
}
|
||||
|
||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
||||
@@ -186,6 +192,14 @@ void Player::readHurtBlock(const fkyaml::node& yaml) {
|
||||
}
|
||||
}
|
||||
|
||||
void Player::readDeathBlock(const fkyaml::node& yaml) {
|
||||
death_config = DeathConfig{};
|
||||
if (!yaml.contains("death")) { return; }
|
||||
const auto& d = yaml["death"];
|
||||
if (d.contains("respawn_delay_ms")) { death_config.respawn_delay_ms = d["respawn_delay_ms"].get_value<int>(); }
|
||||
if (d.contains("lives")) { death_config.initial_lives = d["lives"].get_value<int>(); }
|
||||
}
|
||||
|
||||
void Player::readWhipBlock(const fkyaml::node& yaml) {
|
||||
whip_config = WhipConfig{};
|
||||
if (!yaml.contains("whip")) { return; }
|
||||
@@ -343,6 +357,13 @@ Player::Player(const std::string& anim_key, int id, float x, float y, const Cont
|
||||
// prem cap tecla.
|
||||
facing_ = Facing::SOUTH;
|
||||
setCurrentAnimation("stand_south");
|
||||
// Comptador d'intents inicialitzat amb el valor del YAML (default 3
|
||||
// si el bloc `death:` manca). Fora del cicle DYING no toca a ningú.
|
||||
lives_remaining_ = death_config.initial_lives;
|
||||
// Spawn point inicial: la posició del ctor val per defecte; el
|
||||
// GameScene el reescriu via `setSpawnPoint` quan canvia de sala.
|
||||
spawn_x_ = x;
|
||||
spawn_y_ = y;
|
||||
}
|
||||
|
||||
void Player::update(float delta_time) {
|
||||
@@ -378,9 +399,13 @@ auto Player::collidesWithWorld(const SDL_FRect& box) const -> bool {
|
||||
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_HURT, MY_Z](const Player* p) {
|
||||
const bool MY_DYING = (state_ == State::DYING);
|
||||
return std::ranges::any_of(*all_players_, [this, &PROPOSED_HIT, MY_AIR, MY_HURT, MY_DYING, MY_Z](const Player* p) {
|
||||
if (p == nullptr || p == this) { return false; }
|
||||
if (p->isOutOfPlay()) { return false; }
|
||||
// DYING és intangible per a PvP: el jugador mort no actua de
|
||||
// mur, i un altre jugador en DYING no pot empentar tampoc.
|
||||
if (MY_DYING || p->isDying()) { 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
|
||||
@@ -436,6 +461,25 @@ void Player::tick(float delta_time) {
|
||||
// que GameScene el reactiva (respawn / spawnFromAir).
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// DYING: animació `death_spinning` + pausa de respawn. Ignorem tot
|
||||
// l'input (ni moviment, ni salt, ni atac). El moviment queda
|
||||
// congelat al lloc de la mort; `applyMovement` avança l'animació i
|
||||
// detecta la fi per disparar el respawn. Mantenim els flancs de
|
||||
// botó actualitzats perquè una tecla sostinguda no es comporte com
|
||||
// a flanc nou en sortir del cicle.
|
||||
if (state_ == State::DYING) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
motion_time_ms_ = 0.0F;
|
||||
setVelX(0.0F);
|
||||
setVelY(0.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
// Reset per frame: el resolver del GameScene el tornarà a posar a
|
||||
// true si aquest frame hi ha push actiu. Així quan el push acaba
|
||||
// (un deixa d'empentar, l'altre comença a caminar), l'animació torna
|
||||
@@ -877,7 +921,9 @@ void Player::resolveLandingOverlap() {
|
||||
if (other == nullptr || other == this) { continue; }
|
||||
// Salta jugadors fora del joc: un caigut sobre un slot DISABLED
|
||||
// (o sobre un EXITED encara al vector) no els ha de projectar.
|
||||
if (other->isOutOfPlay()) { continue; }
|
||||
// També DYING — un jugador en cicle de mort no rep cap més
|
||||
// efecte fins que respawneja.
|
||||
if (other->isOutOfPlay() || other->isDying()) { continue; }
|
||||
// Test amb la hit-box (8×8 de contacte precís), no la base-box.
|
||||
// Coherent amb la resta de col·lisions actor-actor del joc:
|
||||
// momies, fuetades futures, resolver PvP… tots usen hit-box. Un
|
||||
@@ -925,6 +971,11 @@ void Player::applyMovement(float delta_time) {
|
||||
resolveLandingOverlap();
|
||||
}
|
||||
|
||||
if (state_ == State::DYING) {
|
||||
applyDyingMovement(delta_time);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state_ == State::HURT) {
|
||||
applyHurtMovement(delta_time);
|
||||
return;
|
||||
@@ -996,6 +1047,67 @@ void Player::applyMovement(float delta_time) {
|
||||
if (state_ == State::GROUNDED) { publishTileEnteredIfChanged(); }
|
||||
}
|
||||
|
||||
void Player::enterDying() {
|
||||
// Idempotent. Un segon contacte de momia mentre el jugador ja està
|
||||
// en DYING no reinicia l'animació ni el comptador. Tampoc disparem
|
||||
// el cicle si el jugador està fora de joc (EXITED al passar la
|
||||
// porta, DISABLED durant les transicions de sala).
|
||||
if (state_ == State::DYING || isOutOfPlay()) { return; }
|
||||
|
||||
state_ = State::DYING;
|
||||
intended_vx_ = 0.0F;
|
||||
intended_vy_ = 0.0F;
|
||||
input_vx_ = 0.0F;
|
||||
input_vy_ = 0.0F;
|
||||
motion_time_ms_ = 0.0F;
|
||||
being_pushed_ = false;
|
||||
jump_z_ = 0.0F;
|
||||
jump_vz_ = 0.0F;
|
||||
landing_stun_remaining_ms_ = 0.0F;
|
||||
attacking_remaining_ms_ = 0.0F;
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
pending_cardinal_ = Cardinal::NONE;
|
||||
intermediate_cardinal_ = Cardinal::NONE;
|
||||
last_input_cardinal_ = Cardinal::NONE;
|
||||
setVelX(0.0F);
|
||||
setVelY(0.0F);
|
||||
|
||||
death_animation_complete_ = false;
|
||||
respawn_remaining_ms_ = 0.0F;
|
||||
setCurrentAnimation("death_spinning");
|
||||
}
|
||||
|
||||
void Player::applyDyingMovement(float delta_time) {
|
||||
// Avancem l'animació; el sprite no es mou (la posició queda
|
||||
// congelada al punt de la mort). `animationIsCompleted()` flanqueja
|
||||
// un cop la roda arriba al darrer frame (loop:false). Llavors
|
||||
// descomptem `respawn_delay_ms` mantenint el sprite al darrer frame
|
||||
// i, en arribar a 0, disparem el respawn.
|
||||
animate(delta_time);
|
||||
if (!death_animation_complete_ && animationIsCompleted()) {
|
||||
death_animation_complete_ = true;
|
||||
respawn_remaining_ms_ = static_cast<float>(death_config.respawn_delay_ms);
|
||||
}
|
||||
if (death_animation_complete_) {
|
||||
respawn_remaining_ms_ -= delta_time * 1000.0F;
|
||||
if (respawn_remaining_ms_ <= 0.0F) {
|
||||
respawnFromDying();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Player::respawnFromDying() {
|
||||
// Vides: decreix sempre. Pot anar a negatius — no hi ha game over.
|
||||
--lives_remaining_;
|
||||
death_animation_complete_ = false;
|
||||
respawn_remaining_ms_ = 0.0F;
|
||||
// `respawn` reseteja l'estat (GROUNDED, facing SOUTH, animació
|
||||
// `stand_south`) i mou el sprite a les coords del spawn que el
|
||||
// GameScene ha assignat. No fem `spawnFromAir` per disseny: el
|
||||
// respawn post-mort és sec, sense cinemàtica de caiguda.
|
||||
respawn(spawn_x_, spawn_y_);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -1133,6 +1245,12 @@ auto Player::statePrefix(State state, bool moving, bool in_attack_swing) -> cons
|
||||
return "hurt";
|
||||
case State::ATTACKING:
|
||||
return in_attack_swing ? "attack" : "recover";
|
||||
case State::DYING:
|
||||
// L'animació concreta (`death_spinning`) la fixa `enterDying`;
|
||||
// `applyDyingMovement` no torna a cridar `setCurrentAnimation`,
|
||||
// així que aquest valor no s'usa mai. El retornem només per
|
||||
// exhaustivitat del switch.
|
||||
return "death";
|
||||
case State::GROUNDED:
|
||||
case State::EXITED:
|
||||
case State::DISABLED:
|
||||
@@ -1199,10 +1317,16 @@ void Player::respawn(float x, float y) {
|
||||
jump_z_ = 0.0F;
|
||||
jump_vz_ = 0.0F;
|
||||
landing_stun_remaining_ms_ = 0.0F;
|
||||
attacking_remaining_ms_ = 0.0F;
|
||||
turning_remaining_ms_ = 0.0F;
|
||||
pending_cardinal_ = Cardinal::NONE;
|
||||
intermediate_cardinal_ = Cardinal::NONE;
|
||||
last_input_cardinal_ = Cardinal::NONE;
|
||||
// Flags del cicle DYING (que `respawnFromDying` també neteja, però
|
||||
// si algú crida `respawn` directament des d'un altre estat, han de
|
||||
// quedar coherents per al pròxim cicle de mort).
|
||||
death_animation_complete_ = false;
|
||||
respawn_remaining_ms_ = 0.0F;
|
||||
// Reseteig del facing al d'spawn original (mirant cap a la càmera):
|
||||
// sense això, un EXITED que entrava per la porta amb facing NORTH
|
||||
// re-aparix d'esquena al pròxim cicle. Mateixa lògica que el ctor.
|
||||
|
||||
Reference in new issue
Block a user