Merge branch 'feat/player-immunity'
This commit is contained in:
commit
165499b9d4
7 files changed
+199
-1
No files matched your search
@@ -0,0 +1,35 @@
|
||||
JASC-PAL
|
||||
0100
|
||||
32
|
||||
0 0 0
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
217 217 217
|
||||
@@ -131,3 +131,15 @@ whip:
|
||||
death:
|
||||
respawn_delay_ms: 500
|
||||
lives: 3
|
||||
|
||||
# Immunitat post-spawn: durant `duration_ms` ms, els hazards no
|
||||
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
||||
# caiguda no és dany convencional). Visualment es manifesta amb un
|
||||
# parpadeig que alterna la paleta normal del sprite amb la paleta
|
||||
# `palette` (basename de `data/player/<name>.pal`) cada
|
||||
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
|
||||
# mecànica queda desactivada i el comportament és l'antic.
|
||||
immunity:
|
||||
duration_ms: 3000
|
||||
blink_period_ms: 100
|
||||
palette: immune
|
||||
@@ -131,3 +131,15 @@ whip:
|
||||
death:
|
||||
respawn_delay_ms: 500
|
||||
lives: 3
|
||||
|
||||
# Immunitat post-spawn: durant `duration_ms` ms, els hazards no
|
||||
# poden ferir ni matar el jugador (els forats sí que el maten — la
|
||||
# caiguda no és dany convencional). Visualment es manifesta amb un
|
||||
# parpadeig que alterna la paleta normal del sprite amb la paleta
|
||||
# `palette` (basename de `data/player/<name>.pal`) cada
|
||||
# `blink_period_ms` ms. Si `duration_ms` és 0 (o el bloc manca), la
|
||||
# mecànica queda desactivada i el comportament és l'antic.
|
||||
immunity:
|
||||
duration_ms: 3000
|
||||
blink_period_ms: 100
|
||||
palette: immune
|
||||
@@ -64,6 +64,10 @@ class Surface {
|
||||
|
||||
void setPalette(const Palette& palette) { palette_ = palette; }
|
||||
[[nodiscard]] auto getPaletteColor(Uint8 index) const -> Uint32 { return palette_[index]; }
|
||||
// Accés de lectura a la paleta sencera. L'usen consumidors que volen
|
||||
// fer save/restore (p.ex. el blink d'immunitat del Player, que aplica
|
||||
// una paleta plana abans del render i restaura la normal després).
|
||||
[[nodiscard]] auto getPalette() const -> const Palette& { return palette_; }
|
||||
|
||||
static void initializeSubPalette(SubPalette& palette) { std::iota(palette.begin(), palette.end(), 0); }
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include "core/input/input_state.hpp"
|
||||
#include "core/rendering/canvas.hpp"
|
||||
#include "core/rendering/screen.hpp"
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "core/resources/palette_loader.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "core/resources/resource_helper.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
@@ -75,6 +77,7 @@ Player::JumpConfig Player::jump_config;
|
||||
Player::HurtConfig Player::hurt_config;
|
||||
Player::WhipConfig Player::whip_config;
|
||||
Player::DeathConfig Player::death_config;
|
||||
Player::ImmunityConfig Player::immunity_config;
|
||||
|
||||
void Player::loadFrom(const std::string& yaml_path) {
|
||||
auto bytes = Resource::Helper::loadFile(yaml_path);
|
||||
@@ -131,6 +134,11 @@ void Player::loadFrom(const std::string& yaml_path) {
|
||||
// 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);
|
||||
|
||||
// Bloc opcional `immunity:` — finestra de gràcia post-spawn. Si
|
||||
// `duration_ms` és 0 (o el bloc manca) la mecànica queda
|
||||
// desactivada i els hazards toquen al primer frame, com abans.
|
||||
readImmunityBlock(yaml);
|
||||
}
|
||||
|
||||
void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) {
|
||||
@@ -205,6 +213,20 @@ void Player::readDeathBlock(const fkyaml::node& yaml) {
|
||||
if (d.contains("lives")) { death_config.initial_lives = d["lives"].get_value<int>(); }
|
||||
}
|
||||
|
||||
void Player::readImmunityBlock(const fkyaml::node& yaml) {
|
||||
immunity_config = ImmunityConfig{};
|
||||
if (!yaml.contains("immunity")) { return; }
|
||||
const auto& im = yaml["immunity"];
|
||||
if (im.contains("duration_ms")) { immunity_config.duration_ms = im["duration_ms"].get_value<float>(); }
|
||||
if (im.contains("blink_period_ms")) { immunity_config.blink_period_ms = im["blink_period_ms"].get_value<int>(); }
|
||||
// Paleta del blink: el basename val tant de clau al Resource::Cache
|
||||
// com de path (`data/player/<key>.pal`). Mateix patró que els sons.
|
||||
if (im.contains("palette")) {
|
||||
immunity_config.palette_key = im["palette"].get_value<std::string>();
|
||||
Resource::Cache::get().loadPalette(immunity_config.palette_key, "data/player/" + immunity_config.palette_key + ".pal");
|
||||
}
|
||||
}
|
||||
|
||||
void Player::readWhipBlock(const fkyaml::node& yaml) {
|
||||
whip_config = WhipConfig{};
|
||||
if (!yaml.contains("whip")) { return; }
|
||||
@@ -372,6 +394,12 @@ Player::Player(const std::string& anim_key, int id, float x, float y, const Cont
|
||||
// GameScene el reescriu via `setSpawnPoint` quan canvia de sala.
|
||||
spawn_x_ = x;
|
||||
spawn_y_ = y;
|
||||
// Cachegem la paleta normal del sprite ara que la Surface ja està
|
||||
// carregada. El render del blink d'immunitat la restaura després
|
||||
// d'aplicar la paleta plana — fer-ho via membre evita dependre de
|
||||
// l'estat momentani de la Surface (que pot ser compartida entre
|
||||
// jugadors del mateix personatge).
|
||||
if (surface_ != nullptr) { normal_palette_ = surface_->getPalette(); }
|
||||
}
|
||||
|
||||
void Player::update(float delta_time) {
|
||||
@@ -469,6 +497,16 @@ void Player::tick(float delta_time) {
|
||||
// que GameScene el reactiva (respawn / spawnFromAir).
|
||||
if (isOutOfPlay()) { return; }
|
||||
|
||||
// Decrement del timer d'immunitat. Independent de l'estat: si el
|
||||
// jugador mor durant la immunitat (només via forat, que no es gateja),
|
||||
// el `respawn` rearma una nova finestra. Si la finestra acaba mentre
|
||||
// és AIRBORNE/HURT, no passa res — el gate dels hazards passa a
|
||||
// quedar obert al primer frame post-zero.
|
||||
if (immunity_remaining_ms_ > 0.0F) {
|
||||
immunity_remaining_ms_ -= delta_time * 1000.0F;
|
||||
immunity_remaining_ms_ = std::max(immunity_remaining_ms_, 0.0F);
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -886,6 +924,11 @@ auto Player::chooseHurtDirection(Cardinal preferred) const -> Cardinal {
|
||||
}
|
||||
|
||||
void Player::enterHurt(Cardinal preferred) {
|
||||
// Gate d'immunitat: tots els callers de dany (whip friendly fire,
|
||||
// caiguda damunt, futurs hazards) passen per ací. Una sola línia
|
||||
// cobreix la finestra de gràcia post-spawn.
|
||||
if (isImmune()) { return; }
|
||||
|
||||
// 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; }
|
||||
@@ -1140,6 +1183,13 @@ void Player::applyMovement(float delta_time) {
|
||||
}
|
||||
|
||||
void Player::enterDying(DeathCause cause) {
|
||||
// Gate d'immunitat amb bypass per a HOLE: la caiguda per agujero NO
|
||||
// és dany convencional sinó "sortida lògica del món", així que els
|
||||
// forats han de matar fins i tot el jugador immune. Qualsevol altra
|
||||
// causa (mòmia letal via `player_killed`, etc.) queda neutralitzada
|
||||
// mentre dura la finestra de gràcia.
|
||||
if (cause != DeathCause::HOLE && isImmune()) { return; }
|
||||
|
||||
// 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
|
||||
@@ -1311,6 +1361,25 @@ void Player::render() {
|
||||
});
|
||||
}
|
||||
|
||||
// Blink d'immunitat: cada `blink_period_ms` ms, el render alterna
|
||||
// entre la paleta normal i la paleta plana. La fase es deriva del
|
||||
// timer restant — al spawn (timer = duration) toca paleta plana
|
||||
// perquè el feedback sigui immediat. La Surface pot ser compartida
|
||||
// entre dos jugadors del mateix personatge: com que el render del
|
||||
// joc és seqüencial, el save/restore al voltant del render del
|
||||
// sprite és segur — quan acaba aquesta funció, la Surface torna a
|
||||
// tenir la paleta normal i la pròxima crida pot fer el seu propi
|
||||
// swap independentment.
|
||||
const bool BLINK_ON =
|
||||
immunity_remaining_ms_ > 0.0F &&
|
||||
immunity_config.blink_period_ms > 0 &&
|
||||
!immunity_config.palette_key.empty() &&
|
||||
surface_ != nullptr &&
|
||||
(static_cast<int>(immunity_remaining_ms_) / immunity_config.blink_period_ms) % 2 == 0;
|
||||
if (BLINK_ON) {
|
||||
surface_->setPalette(Resource::Cache::get().getPalette(immunity_config.palette_key));
|
||||
}
|
||||
|
||||
if (jump_z_ == 0.0F) {
|
||||
AnimatedSprite::render();
|
||||
} else {
|
||||
@@ -1320,6 +1389,8 @@ void Player::render() {
|
||||
pos_.y = SAVED_Y;
|
||||
}
|
||||
|
||||
if (BLINK_ON) { surface_->setPalette(normal_palette_); }
|
||||
|
||||
if (CLIP_HOLE) { Rendering::popClip(); }
|
||||
}
|
||||
|
||||
@@ -1486,6 +1557,12 @@ void Player::respawn(float x, float y) {
|
||||
setCurrentAnimation("stand_south");
|
||||
setPosX(x);
|
||||
setPosY(y);
|
||||
// Immunitat post-spawn: armem la finestra de gràcia amb la durada del
|
||||
// YAML. Cobreix l'aparició inicial (`spawnFromAir` → `respawn`) i el
|
||||
// respawn post-mort (`respawnFromDying` → `respawn`). Si el bloc
|
||||
// `immunity:` manca al YAML, `duration_ms` és 0 i `setImmunity` és
|
||||
// no-op — comportament idèntic al d'abans.
|
||||
setImmunity(immunity_config.duration_ms);
|
||||
}
|
||||
|
||||
void Player::disable() {
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "core/rendering/sprite/animated_sprite.hpp"
|
||||
#include "core/rendering/surface.hpp"
|
||||
#include "external/fkyaml_node.hpp"
|
||||
#include "game/map/map.hpp"
|
||||
|
||||
@@ -311,6 +312,25 @@ class Player : public AnimatedSprite {
|
||||
// game over. Públic per a HUD i tests.
|
||||
[[nodiscard]] auto getLivesRemaining() const -> int { return lives_remaining_; }
|
||||
|
||||
// Activa la immunitat per `duration_ms` ms. Mentre `isImmune()` és
|
||||
// cert, `enterHurt` i `enterDying(GENERIC)` són no-op — els hazards no
|
||||
// poden tocar el jugador. La caiguda per forats (`enterDying(HOLE)`)
|
||||
// segueix funcionant: els forats no són dany convencional, són una
|
||||
// sortida lògica del món. Idempotent: una segona crida reescriu el
|
||||
// timer (no acumula). Visualment, el render alterna la paleta del
|
||||
// sprite amb una paleta plana cada `immunity_config.blink_period_ms`
|
||||
// ms. Si `duration_ms <= 0` és no-op (manera de cancel·lar
|
||||
// explícitament la immunitat seria `setImmunity(0)` — equivalent al
|
||||
// comportament natural quan el timer s'esgota).
|
||||
void setImmunity(float duration_ms) {
|
||||
if (duration_ms > 0.0F) { immunity_remaining_ms_ = duration_ms; }
|
||||
}
|
||||
|
||||
// Cert mentre el jugador té immunitat activa. Comprovat per
|
||||
// `enterHurt` i `enterDying` per filtrar els hazards. La causa HOLE
|
||||
// ignora aquest gate (vegeu `enterDying`).
|
||||
[[nodiscard]] auto isImmune() const -> bool { return immunity_remaining_ms_ > 0.0F; }
|
||||
|
||||
// Reset complet a l'estat inicial: posició a (x, y), velocitats a 0,
|
||||
// estat GROUNDED, qualsevol flag de control reinicialitzat. Cridat
|
||||
// per GameScene a la transició ROOM_OUTRO → ROOM_INTRO. Idempotent
|
||||
@@ -664,6 +684,30 @@ class Player : public AnimatedSprite {
|
||||
// l'animació) i `initial_lives` es manté al default (3).
|
||||
static void readDeathBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Configuració de la immunitat post-spawn llegida del bloc `immunity:`
|
||||
// del YAML. `duration_ms` és el temps per defecte que dura cada
|
||||
// immunitat (l'invoca `respawn`). `blink_period_ms` és el mig-cicle del
|
||||
// parpadeig visual: cada `blink_period_ms` ms, el render alterna
|
||||
// entre la paleta normal i la paleta `palette_key`. Si `duration_ms`
|
||||
// és 0 (o el bloc manca) la immunitat queda desactivada i el
|
||||
// comportament és l'antic.
|
||||
struct ImmunityConfig {
|
||||
float duration_ms{0.0F};
|
||||
int blink_period_ms{0};
|
||||
// Clau al Resource::Cache de la paleta plana usada al blink (basename
|
||||
// del .pal a `data/player/`). Buida = no hi ha alternança visual,
|
||||
// només la finestra lògica d'immunitat (que sense feedback visual
|
||||
// pot ser confusa, però no és incorrecte).
|
||||
std::string palette_key;
|
||||
};
|
||||
static ImmunityConfig immunity_config;
|
||||
|
||||
// Llig el bloc `immunity:` del YAML i n'omple `immunity_config`. Si
|
||||
// hi ha `palette`, registra la paleta corresponent al Resource::Cache
|
||||
// (path: `data/player/<palette_key>.pal`). Mateix patró que els altres
|
||||
// readers — `loadFrom` el crida al final.
|
||||
static void readImmunityBlock(const fkyaml::node& yaml);
|
||||
|
||||
// Activa l'estat ATTACKING en la direcció cardinal indicada (la
|
||||
// direcció cap on mira el jugador) i publica `whip_spawn` al bus
|
||||
// perquè el GameScene cree el `WhipAttack` corresponent. Posa el
|
||||
@@ -743,6 +787,20 @@ class Player : public AnimatedSprite {
|
||||
// LANDING_STUN i tot l'input queda bloquejat.
|
||||
float landing_stun_remaining_ms_{0.0F};
|
||||
|
||||
// Temps restant d'immunitat (ms). Decreix cada frame al `tick`,
|
||||
// independentment de l'estat — la immunitat és ortogonal al state
|
||||
// machine. Mentre > 0, `enterHurt` i `enterDying(GENERIC)` són no-op.
|
||||
// L'arma `setImmunity()` (cridada per `respawn` amb la durada del
|
||||
// YAML), i el render alterna paleta cada `immunity_config.blink_period_ms`.
|
||||
float immunity_remaining_ms_{0.0F};
|
||||
|
||||
// Còpia de la paleta normal del sprite (extreta de `surface_->getPalette()`
|
||||
// al ctor). El render del blink l'usa per restaurar la paleta després
|
||||
// d'aplicar la versió plana cada mig-cicle. Cachejada perquè la
|
||||
// Surface pot ser compartida entre dos jugadors del mateix personatge
|
||||
// i no és segur consultar-la entre swaps.
|
||||
Palette normal_palette_{};
|
||||
|
||||
// Estat anterior del botó de salt per fer detecció de flanc (saltem
|
||||
// només a la transició no-premut → premut, no mentre es manté). Es
|
||||
// manté un per teclat i un per mando perquè poden conviure.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace Texts {
|
||||
constexpr const char* WINDOW_TITLE = "© 2026 Aventures en Egipte ARCADE — JailDesigner";
|
||||
constexpr const char* COPYRIGHT = "© 2026 JailDesigner";
|
||||
constexpr const char* VERSION = "0.32.0"; // Single source of truth — Makefile el grep
|
||||
constexpr const char* VERSION = "0.33.0"; // Single source of truth — Makefile el grep
|
||||
} // namespace Texts
|
||||
|
||||
namespace GameCanvas {
|
||||
|
||||
Reference in new issue
Block a user