feat(player): estat ATTACKING amb edge-detect, animacions i spawn del whip via bus
This commit is contained in:
1 parent
5db3bb0bd0
commit
d208ba02df
7 files changed
+288
-1
No files matched your search
@@ -68,6 +68,7 @@ Player::TurnConfig Player::turn_config;
|
||||
Player::InertiaConfig Player::inertia_config;
|
||||
Player::JumpConfig Player::jump_config;
|
||||
Player::HurtConfig Player::hurt_config;
|
||||
Player::WhipConfig Player::whip_config;
|
||||
|
||||
void Player::loadFrom(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
@@ -114,6 +115,11 @@ void Player::loadFrom(const std::string& yaml_path) {
|
||||
// Bloc opcional `hurt:` — estat post-impacte. Si manca o el speed
|
||||
// és 0, l'estat HURT mai s'activa (cas degenerat).
|
||||
readHurtBlock(yaml);
|
||||
|
||||
// Bloc opcional `whip:` — paràmetres del látigo. Si manca, el
|
||||
// jugador no atacarà efectivament (ATTACKING dura 0 ms i el flanc
|
||||
// d'input es perd).
|
||||
readWhipBlock(yaml);
|
||||
}
|
||||
|
||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
||||
@@ -175,6 +181,16 @@ void Player::readHurtBlock(const fkyaml::node& yaml) {
|
||||
if (h.contains("brake_time_ms")) { hurt_config.brake_time_ms = h["brake_time_ms"].get_value<int>(); }
|
||||
}
|
||||
|
||||
void Player::readWhipBlock(const fkyaml::node& yaml) {
|
||||
whip_config = WhipConfig{};
|
||||
if (!yaml.contains("whip")) { return; }
|
||||
const auto& w = yaml["whip"];
|
||||
if (w.contains("range_tiles")) { whip_config.range_tiles = w["range_tiles"].get_value<float>(); }
|
||||
if (w.contains("swing_ms")) { whip_config.swing_ms = w["swing_ms"].get_value<int>(); }
|
||||
if (w.contains("cooldown_ms")) { whip_config.cooldown_ms = w["cooldown_ms"].get_value<int>(); }
|
||||
if (w.contains("damage")) { whip_config.damage = w["damage"].get_value<int>(); }
|
||||
}
|
||||
|
||||
void Player::applyCardinal(Cardinal c) {
|
||||
switch (c) {
|
||||
case Cardinal::NORTH:
|
||||
@@ -440,6 +456,7 @@ void Player::tick(float delta_time) {
|
||||
// durant el stun: així si l'usuari l'ha estat prement abans, no
|
||||
// se'l consumeix com a salt nou en sortir del stun.
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -449,11 +466,36 @@ void Player::tick(float delta_time) {
|
||||
// pulsació quan torne a terra es detecte com a flanc nou.
|
||||
if (state_ == State::AIRBORNE) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
setVelX(intended_vx_);
|
||||
setVelY(intended_vy_);
|
||||
return;
|
||||
}
|
||||
|
||||
// En ATTACKING el jugador queda bloquejat tot el `swing_ms +
|
||||
// cooldown_ms`: ni input direccional, ni salt, ni atac, ni canvi
|
||||
// de facing. El timer descompta amb `delta_time * 1000` per
|
||||
// mantenir la convenció de delta_ms de la resta d'estats temporals.
|
||||
// Mantenim els flancs dels botons actualitzats perquè una
|
||||
// tecla mantinguda no es comporte com a flanc nou en sortir.
|
||||
if (state_ == State::ATTACKING) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
attacking_remaining_ms_ -= delta_time * 1000.0F;
|
||||
if (attacking_remaining_ms_ <= 0.0F) {
|
||||
attacking_remaining_ms_ = 0.0F;
|
||||
state_ = State::GROUNDED;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.
|
||||
@@ -461,6 +503,7 @@ void Player::tick(float delta_time) {
|
||||
// estats), que també gestiona el xoc contra murs.
|
||||
if (state_ == State::HURT) {
|
||||
(void)consumeJumpEdge();
|
||||
(void)consumeAttackEdge();
|
||||
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;
|
||||
@@ -535,6 +578,18 @@ void Player::tick(float delta_time) {
|
||||
// només disparen amb intenció explícita del jugador.
|
||||
updateFacingAndTurn(delta_time, input_vx_, input_vy_);
|
||||
|
||||
// Detecció de flanc del botó d'atac. Es processa ABANS del salt
|
||||
// perquè ATTACKING i AIRBORNE són mútuament excloents i, davant un
|
||||
// mateix-frame, l'atac té prioritat (millor castigar la pulsació
|
||||
// doble per excés que perdre l'atac per un toc accidental al salt).
|
||||
// En entrar a ATTACKING, descartem el flanc del salt per no que
|
||||
// queda penjat per a quan es desbloque.
|
||||
if (consumeAttackEdge() && whip_config.swing_ms > 0) {
|
||||
enterAttacking(currentFacingCardinal());
|
||||
(void)consumeJumpEdge();
|
||||
return;
|
||||
}
|
||||
|
||||
// Detecció de flanc del botó de salt. Es processa DESPRÉS d'aplicar
|
||||
// el facing perquè el sprite del salt usa el facing del moment del
|
||||
// despegue. `being_pushed_` no ho pot bloquejar ací (encara no s'ha
|
||||
@@ -565,6 +620,92 @@ auto Player::consumeJumpEdge() -> bool {
|
||||
return edge;
|
||||
}
|
||||
|
||||
auto Player::consumeAttackEdge() -> bool {
|
||||
// Mateix patró que `consumeJumpEdge` però per al botó d'atac. Mando:
|
||||
// SDL_GAMEPAD_BUTTON_WEST (X a Xbox / Quadrat a PlayStation), oposat
|
||||
// a SOUTH per evitar topades semàntiques.
|
||||
const bool* keys = InputState::getKeyboardState();
|
||||
const bool KEY_NOW = keys[controls_.attack];
|
||||
bool edge = (KEY_NOW && !attack_held_keyboard_);
|
||||
attack_held_keyboard_ = KEY_NOW;
|
||||
|
||||
SDL_Gamepad* pad = GamepadManager::get().getForPlayer(id_);
|
||||
const bool PAD_NOW = (pad != nullptr) && InputState::getGamepadButton(pad, SDL_GAMEPAD_BUTTON_WEST);
|
||||
if (PAD_NOW && !attack_held_gamepad_) { edge = true; }
|
||||
attack_held_gamepad_ = PAD_NOW;
|
||||
|
||||
return edge;
|
||||
}
|
||||
|
||||
auto Player::currentFacingCardinal() const -> Cardinal {
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return Cardinal::NORTH;
|
||||
case Facing::DOWN:
|
||||
return Cardinal::SOUTH;
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
// Lateral: el flip discrimina E/W. Sense flip = mira a la dreta
|
||||
// (convenció estable a tots els assets actuals).
|
||||
return (getFlip() == SDL_FLIP_HORIZONTAL) ? Cardinal::WEST : Cardinal::EAST;
|
||||
}
|
||||
|
||||
void Player::enterAttacking(Cardinal facing) {
|
||||
state_ = State::ATTACKING;
|
||||
attacking_remaining_ms_ = static_cast<float>(whip_config.swing_ms + whip_config.cooldown_ms);
|
||||
|
||||
// Garantim que facing/flip i cardinal queden coherents amb la
|
||||
// direcció triada (per si l'atac s'ha disparat amb facing residual,
|
||||
// p.ex. just després d'un canvi d'input).
|
||||
applyCardinal(facing);
|
||||
|
||||
// Geometria de spawn de la hitbox: alineada al rectangle de
|
||||
// col·lisió del jugador, no al sprite (que té perspectiva falsa amb
|
||||
// el cap dalt). La hitbox és `range_px × tile_size` (E/W) o
|
||||
// `tile_size × range_px` (N/S); el GameScene la consumeix per
|
||||
// crear el WhipAttack amb la mida adequada per direcció.
|
||||
const auto BOX = getBaseBox();
|
||||
const float RANGE_PX = whip_config.range_tiles * static_cast<float>(Map::tile_size);
|
||||
float spawn_x = 0.0F;
|
||||
float spawn_y = 0.0F;
|
||||
int dir_int = 0;
|
||||
switch (facing) {
|
||||
case Cardinal::EAST:
|
||||
spawn_x = BOX.x + BOX.w;
|
||||
spawn_y = BOX.y;
|
||||
dir_int = 3;
|
||||
break;
|
||||
case Cardinal::WEST:
|
||||
spawn_x = BOX.x - RANGE_PX;
|
||||
spawn_y = BOX.y;
|
||||
dir_int = 4;
|
||||
break;
|
||||
case Cardinal::NORTH:
|
||||
spawn_x = BOX.x;
|
||||
spawn_y = BOX.y - RANGE_PX;
|
||||
dir_int = 1;
|
||||
break;
|
||||
case Cardinal::SOUTH:
|
||||
spawn_x = BOX.x;
|
||||
spawn_y = BOX.y + BOX.h;
|
||||
dir_int = 2;
|
||||
break;
|
||||
case Cardinal::NONE:
|
||||
return;
|
||||
}
|
||||
|
||||
if (bus_ != nullptr) {
|
||||
bus_->publish("whip_spawn", WhipSpawnPayload{
|
||||
.owner = this,
|
||||
.direction = dir_int,
|
||||
.spawn_x = spawn_x,
|
||||
.spawn_y = spawn_y,
|
||||
.range_px = RANGE_PX,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void Player::startJump() {
|
||||
// No saltem si l'estat no ho permet. `being_pushed_` no és segur de
|
||||
// consultar ací (es pot haver marcat o no, segons l'ordre del frame),
|
||||
@@ -788,6 +929,15 @@ void Player::applyMovement(float delta_time) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (state_ == State::ATTACKING) {
|
||||
// Cap moviment durant ATTACKING. Animem (perquè avance la
|
||||
// seqüència `attack_*`) i actualitzem la clau d'animació segons
|
||||
// la sub-fase (swing vs cooldown).
|
||||
animate(delta_time);
|
||||
setCurrentAnimation(animationName(false));
|
||||
return;
|
||||
}
|
||||
|
||||
// El resolver del GameScene pot haver escalat `intended_vx_/vy_`
|
||||
// (cas push: velocitat * push_factor) o haver-les anul·lat (cas
|
||||
// head-on). Consumim-les aquí com a velocitat definitiva del frame.
|
||||
@@ -989,6 +1139,33 @@ auto Player::animationName(bool moving) const -> const char* {
|
||||
}
|
||||
return "hurt";
|
||||
}
|
||||
// ATTACKING té dues sub-fases: durant `swing_ms` el sprite és
|
||||
// `attack_*` (animació del braç llançant el látigo); durant
|
||||
// `cooldown_ms` posterior tornem al `stand_*` de la direcció. Fem
|
||||
// la decisió comparant el remaining_ms_ amb el cooldown configurat.
|
||||
if (state_ == State::ATTACKING) {
|
||||
const bool IN_SWING = (attacking_remaining_ms_ > static_cast<float>(whip_config.cooldown_ms));
|
||||
if (IN_SWING) {
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return "attack_up";
|
||||
case Facing::DOWN:
|
||||
return "attack_down";
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
return "attack";
|
||||
}
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return "stand_up";
|
||||
case Facing::DOWN:
|
||||
return "stand_down";
|
||||
case Facing::SIDE:
|
||||
break;
|
||||
}
|
||||
return "stand";
|
||||
}
|
||||
switch (facing_) {
|
||||
case Facing::UP:
|
||||
return moving ? "walk_up" : "stand_up";
|
||||
|
||||
Reference in new issue
Block a user