diff --git a/data/entities/torch/torch.yaml b/data/entities/torch/torch.yaml index 3afa6f4..89a83dc 100644 --- a/data/entities/torch/torch.yaml +++ b/data/entities/torch/torch.yaml @@ -22,8 +22,12 @@ animations: # anells). Amb `flicker: true` el radi efectiu oscil·la per frame # (suma de dues sinusoides amb fase derivada de la posició) per a # que el cercle "respiri" com una flama; els paràmetres del flicker -# també viuen al config global. Omet el bloc per fer una antorxa -# "apagada". +# també viuen al config global. `offset: [x, y]` desplaça el centre +# del cercle: la flama del sprite no està al centre del bounding box +# sinó cap a dalt, així que pujem la llum 4 px (Y creix cap avall) +# per a que el halo "neixi" de la flama, no del peu. Omet el bloc +# per fer una antorxa "apagada". light: radius: 40 flicker: true + offset: [0, -4] diff --git a/source/game/entities/entity.cpp b/source/game/entities/entity.cpp index 1e4902e..219c1c4 100644 --- a/source/game/entities/entity.cpp +++ b/source/game/entities/entity.cpp @@ -64,12 +64,13 @@ auto Entity::getCollisionRect() const -> SDL_FRect { } auto Entity::getLightCenter() const -> SDL_FPoint { - // Centre del rect de render. Per a entitats que es desplacen - // visualment sobre la seva posició lògica (p.ex. el Player saltant), - // la subclasse override aquest mètode per restar el desplaçament. + // Centre del rect de render + offset configurat al YAML. Per a + // entitats que es desplacen visualment sobre la seva posició lògica + // (p.ex. el Player saltant), la subclasse override aquest mètode + // per restar el desplaçament — i ha d'incloure l'offset igualment. return { - .x = pixel_rect_.x + (pixel_rect_.w * 0.5F), - .y = pixel_rect_.y + (pixel_rect_.h * 0.5F), + .x = pixel_rect_.x + (pixel_rect_.w * 0.5F) + static_cast(light_offset_x_), + .y = pixel_rect_.y + (pixel_rect_.h * 0.5F) + static_cast(light_offset_y_), }; } diff --git a/source/game/entities/entity.hpp b/source/game/entities/entity.hpp index c1c1b68..d3e7ff8 100644 --- a/source/game/entities/entity.hpp +++ b/source/game/entities/entity.hpp @@ -280,6 +280,22 @@ class Entity { light_color_b_ = b; } + // Desplaçament en píxels del centre del cercle de llum respecte el + // centre visual del sprite (que és el que retornaria per defecte + // `getLightCenter`). Útil quan el "punt de llum" iconogràfic no + // coincideix amb el centre del bounding box — per exemple, la flama + // d'una antorxa està a la part de dalt del sprite i interessa pujar + // l'emissor 3-4 px per a que el cercle de llum surti de la flama, + // no del peu. Y creix cap avall al canvas, així que `y: -3` puja + // la llum 3 píxels. Cadena de resolució idèntica al color: tipus → + // sala → spawn. + [[nodiscard]] auto getLightOffsetX() const -> int { return light_offset_x_; } + [[nodiscard]] auto getLightOffsetY() const -> int { return light_offset_y_; } + void setLightOffset(int x, int y) { + light_offset_x_ = x; + light_offset_y_ = y; + } + // Identificador estable per al flicker. Cada Entity rep un valor // únic a la construcció (contador estàtic) i el conserva fins a la // mort — així el patró del flicker no "deriva" quan l'entitat es @@ -288,11 +304,13 @@ class Entity { // un problema. [[nodiscard]] auto getLightSeed() const -> int { return light_seed_; } - // Centre de la llum en píxels del canvas. Per defecte és el centre del - // `pixel_rect_` (que és la caixa de render: sprite top-left + mida). - // Virtual perquè entitats amb desplaçament visual sobre la posició - // lògica (p.ex. el Player en l'aire, que rebaixa visualment `pos_.y` - // amb `jump_z_`) puguin retornar el centre que realment es veu. + // Centre de la llum en píxels del canvas. Per defecte és el centre + // del `pixel_rect_` (caixa de render) més l'offset configurat al + // YAML (`light.offset: [x, y]`). Virtual perquè entitats amb + // desplaçament visual sobre la posició lògica (p.ex. el Player en + // l'aire, que rebaixa visualment `pos_.y` amb `jump_z_`) puguin + // retornar el centre que realment es veu — l'override n'és + // responsable d'incloure l'offset (com fa `Player::getLightCenter`). [[nodiscard]] virtual auto getLightCenter() const -> SDL_FPoint; protected: @@ -319,6 +337,8 @@ class Entity { std::uint8_t light_color_r_{255}; std::uint8_t light_color_g_{255}; std::uint8_t light_color_b_{255}; + int light_offset_x_{0}; + int light_offset_y_{0}; bool dead_{false}; SDL_FlipMode flip_{SDL_FLIP_NONE}; EventBus* bus_{nullptr}; diff --git a/source/game/entities/entity_manager_loadlevel.cpp b/source/game/entities/entity_manager_loadlevel.cpp index f063961..1d341b4 100644 --- a/source/game/entities/entity_manager_loadlevel.cpp +++ b/source/game/entities/entity_manager_loadlevel.cpp @@ -157,12 +157,14 @@ namespace { return c["height"].get_value(); } - // Bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b] }` + // Bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }` // de la plantilla. Si manca, el radi és 0 i l'entitat no entra a la // pasada d'il·luminació. `flicker` activa la modulació "tipus foc" // del radi (només antorxes l'haurien d'usar). `color` només té - // efecte en mode additive — defecte blanc (255,255,255). La forma - // del gradient (3 anells) i els paràmetres del flicker viuen a + // efecte en mode additive — defecte blanc (255,255,255). `offset` + // desplaça el centre del cercle de llum respecte el centre del + // sprite (default (0, 0); Y creix cap avall). La forma del gradient + // (3 anells) i els paràmetres del flicker viuen a // data/config/lighting.yaml — no cal declarar-los per entitat. struct LightTemplate { int radius{0}; @@ -170,6 +172,8 @@ namespace { std::uint8_t color_r{255}; std::uint8_t color_g{255}; std::uint8_t color_b{255}; + int offset_x{0}; + int offset_y{0}; }; // Helper compartit entre les tres capes (tipus, sala via Map, spawn) @@ -182,6 +186,16 @@ namespace { return true; } + // Helper paral·lel a `readColorTriplet` per a `offset: [x, y]` — + // píxels enters, sense clamp (offsets negatius són legítims; Y + // creix cap avall). + auto readOffsetPair(const fkyaml::node& seq, int& x, int& y) -> bool { + if (!seq.is_sequence() || seq.size() != 2) { return false; } + x = seq[0].get_value(); + y = seq[1].get_value(); + return true; + } + auto readLight(const fkyaml::node& tmpl) -> LightTemplate { if (!tmpl.contains("light")) { return {}; } const auto& l = tmpl["light"]; @@ -189,6 +203,7 @@ namespace { if (l.contains("radius")) { out.radius = l["radius"].get_value(); } if (l.contains("flicker")) { out.flicker = l["flicker"].get_value(); } if (l.contains("color")) { readColorTriplet(l["color"], out.color_r, out.color_g, out.color_b); } + if (l.contains("offset")) { readOffsetPair(l["offset"], out.offset_x, out.offset_y); } return out; } @@ -203,7 +218,7 @@ namespace { auto resolveLightWithRoom(const std::string& type, LightTemplate type_default) -> LightTemplate { const auto* room = Map::getRoomEmitterOverride(type); if (room == nullptr) { return type_default; } - if (!room->enabled) { return {.radius = 0, .flicker = false, .color_r = 255, .color_g = 255, .color_b = 255}; } + if (!room->enabled) { return LightTemplate{}; } LightTemplate out = type_default; if (room->radius.has_value()) { out.radius = *room->radius; } if (room->flicker.has_value()) { out.flicker = *room->flicker; } @@ -212,17 +227,21 @@ namespace { out.color_g = (*room->color)[1]; out.color_b = (*room->color)[2]; } + if (room->offset.has_value()) { + out.offset_x = (*room->offset)[0]; + out.offset_y = (*room->offset)[1]; + } return out; } // Aplica l'override per-spawn (si existeix) per damunt del que ja // viu a l'entitat (defaults del tipus + override de sala). Tres formes // a l'entry del YAML de nivell: - // `light: false` → apaga aquesta instància - // `light: true` → informatiu (cau a capes inferiors) - // `light: { radius, flicker, color: [r,g,b] }` → camps presents sobreescriuen; - // els absents respecten el que - // ja duia l'entitat + // `light: false` → apaga aquesta instància + // `light: true` → informatiu (cau a capes inferiors) + // `light: { radius, flicker, color: [r,g,b], offset: [x,y] }` → camps presents sobreescriuen; + // els absents respecten el que + // ja duia l'entitat void applySpawnLightOverride(Entity& entity, const fkyaml::node& spawn) { if (!spawn.contains("light")) { return; } const auto& l = spawn["light"]; @@ -242,6 +261,11 @@ namespace { std::uint8_t b{}; if (readColorTriplet(l["color"], r, g, b)) { entity.setLightColor(r, g, b); } } + if (l.contains("offset")) { + int x{}; + int y{}; + if (readOffsetPair(l["offset"], x, y)) { entity.setLightOffset(x, y); } + } } // Bloc `collision: { size: [w, h], base_size: [bw, bh]?, offset: [x, y]? }` @@ -374,6 +398,7 @@ namespace { entity->setLightRadius(LIGHT.radius); entity->setLightFlicker(LIGHT.flicker); entity->setLightColor(LIGHT.color_r, LIGHT.color_g, LIGHT.color_b); + entity->setLightOffset(LIGHT.offset_x, LIGHT.offset_y); anchor_out = readAnchor(tmpl); return entity; } diff --git a/source/game/entities/player.cpp b/source/game/entities/player.cpp index 4c64062..83548a4 100644 --- a/source/game/entities/player.cpp +++ b/source/game/entities/player.cpp @@ -153,6 +153,8 @@ void Player::readLightBlock(const fkyaml::node& yaml) { light_color_r = 255; light_color_g = 255; light_color_b = 255; + light_offset_x = 0; + light_offset_y = 0; if (!yaml.contains("light")) { return; } const auto& l = yaml["light"]; if (l.contains("radius")) { light_radius = l["radius"].get_value(); } @@ -167,6 +169,15 @@ void Player::readLightBlock(const fkyaml::node& yaml) { light_color_b = static_cast(std::clamp(c[2].get_value(), 0, 255)); } } + // `offset: [x, y]` opcional, píxels enters. Default (0, 0) → centre + // del sprite. Y creix cap avall, així que `y: -3` puja la llum 3 px. + if (l.contains("offset")) { + const auto& o = l["offset"]; + if (o.is_sequence() && o.size() == 2) { + light_offset_x = o[0].get_value(); + light_offset_y = o[1].get_value(); + } + } } void Player::readCollisionBlock(const fkyaml::node& yaml, const std::string& yaml_path) { @@ -1492,9 +1503,10 @@ auto Player::getLightCenter() const -> SDL_FPoint { // Sprite top-left = (pos_.x, pos_.y). El render desplaça verticalment // `jump_z_` píxels durant el salt; aplicar el mateix offset aquí fa // que la llum "salti" amb el jugador en lloc de quedar-se al sòl. + // Sumem també l'offset estàtic configurat al YAML del personatge. return { - .x = getPosX() + (static_cast(sprite_w) * 0.5F), - .y = getPosY() - jump_z_ + (static_cast(sprite_h) * 0.5F), + .x = getPosX() + (static_cast(sprite_w) * 0.5F) + static_cast(light_offset_x), + .y = getPosY() - jump_z_ + (static_cast(sprite_h) * 0.5F) + static_cast(light_offset_y), }; } diff --git a/source/game/entities/player.hpp b/source/game/entities/player.hpp index ed6a1c1..72a310c 100644 --- a/source/game/entities/player.hpp +++ b/source/game/entities/player.hpp @@ -125,17 +125,21 @@ class Player : public AnimatedSprite { inline static int body_height = 0; // Radi de la llum emesa pel jugador (en píxels del canvas), llegit - // del bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b] }` + // del bloc opcional `light: { radius: N, flicker: bool, color: [r,g,b], offset: [x,y] }` // del YAML. 0 = no emet. Es declara estàtic com la resta de configs // del jugador (immunity, jump, ...): tots els jugadors del mateix YAML // comparteixen el valor. `light_flicker` activa la modulació "tipus // foc" del radi; `light_color_*` són el tint quan la sala usa mode - // additive (en subtractive s'ignora). + // additive (en subtractive s'ignora); `light_offset_*` desplaça el + // centre del cercle respecte el centre del sprite (Y creix avall: + // `y: -3` puja la llum 3 px). inline static int light_radius = 0; inline static bool light_flicker = false; inline static std::uint8_t light_color_r = 255; inline static std::uint8_t light_color_g = 255; inline static std::uint8_t light_color_b = 255; + inline static int light_offset_x = 0; + inline static int light_offset_y = 0; // Ha de cridar-se abans d'instanciar Player; GameScene ho fa al ctor. static void loadFrom(const std::string& yaml_path); @@ -154,6 +158,8 @@ class Player : public AnimatedSprite { [[nodiscard]] static auto getLightColorR() -> std::uint8_t { return light_color_r; } [[nodiscard]] static auto getLightColorG() -> std::uint8_t { return light_color_g; } [[nodiscard]] static auto getLightColorB() -> std::uint8_t { return light_color_b; } + [[nodiscard]] static auto getLightOffsetX() -> int { return light_offset_x; } + [[nodiscard]] static auto getLightOffsetY() -> int { return light_offset_y; } // Seed estable per al flicker: reusem `id_` (0 o 1 en multijugador // local) amb un offset gran per evitar col·lisions amb el contador // d'Entity (que arrenca des de 1). Així P1 i P2 oscil·len en fases diff --git a/source/game/map/map.cpp b/source/game/map/map.cpp index 281fcb2..cedef2b 100644 --- a/source/game/map/map.cpp +++ b/source/game/map/map.cpp @@ -66,6 +66,18 @@ namespace Map { }; } + // Parser comú d'una 2-tupla d'enters (offset de llum). Retorna + // nullopt si el node no és una seqüència de 2 elements. Sense + // clamp: els offsets negatius són legítims (Y creix cap avall, + // així que `y: -3` puja la llum 3 px). + auto parseIntPair(const fkyaml::node& node) -> std::optional> { + if (!node.is_sequence() || node.size() != 2) { return std::nullopt; } + return std::array{ + node[0].get_value(), + node[1].get_value(), + }; + } + // Llegeix el bloc opcional `lighting:` del YAML d'una sala i // actualitza `light_level`, `lighting_mode` i el mapping // `g_room_emitters`. Idempotent: cada `Map::loadFrom` reseteja @@ -102,6 +114,7 @@ namespace Map { if (val.contains("radius")) { ov.radius = val["radius"].get_value(); } if (val.contains("flicker")) { ov.flicker = val["flicker"].get_value(); } if (val.contains("color")) { ov.color = parseColorTriplet(val["color"]); } + if (val.contains("offset")) { ov.offset = parseIntPair(val["offset"]); } } g_room_emitters[KEY] = ov; } diff --git a/source/game/map/map.hpp b/source/game/map/map.hpp index 39c5732..e1f40d0 100644 --- a/source/game/map/map.hpp +++ b/source/game/map/map.hpp @@ -95,6 +95,10 @@ namespace Map { // del tipus (default blanc 255,255,255 si el tipus no en declara). // La forma YAML és `color: [r, g, b]` (bytes 0..255). std::optional> color; + // Offset del centre del cercle de llum respecte el centre del + // sprite (píxels enters; Y creix avall). `nullopt` → conservar + // l'offset del tipus (default [0, 0]). Forma YAML: `offset: [x, y]`. + std::optional> offset; }; // Consulta l'override de sala per a un tipus d'entitat (clau singular, diff --git a/source/game/scenes/game_scene.cpp b/source/game/scenes/game_scene.cpp index 34f3785..2ee7264 100644 --- a/source/game/scenes/game_scene.cpp +++ b/source/game/scenes/game_scene.cpp @@ -255,6 +255,10 @@ namespace { Player::light_color_g = (*room->color)[1]; Player::light_color_b = (*room->color)[2]; } + if (room->offset.has_value()) { + Player::light_offset_x = (*room->offset)[0]; + Player::light_offset_y = (*room->offset)[1]; + } } } // namespace