feat(audio): so del sarcòfag amb pitch aleatori per obertura
This commit is contained in:
1 parent
3e31a88f78
commit
932d195eb0
11 files changed
+112
-11
No files matched your search
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 848 B After Width: | Height: | Size: 1.6 KiB |
@@ -20,11 +20,23 @@ flags:
|
||||
interactable: true
|
||||
animations:
|
||||
closed: { frames: [0], fps: 1, loop: true }
|
||||
opening: { frames: [0], fps: 6, loop: false }
|
||||
open: { frames: [0], fps: 1, loop: true }
|
||||
opening: { frames: [1, 2], fps: 6, loop: false }
|
||||
open: { frames: [3], fps: 1, loop: true }
|
||||
behaviour:
|
||||
states: [CLOSED, OPENING, OPEN]
|
||||
initial_state: CLOSED
|
||||
on_perimeter_complete: OPENING
|
||||
on_opening_end: OPEN
|
||||
emits_event_on_open: sarcophagus_opened
|
||||
# Sons opcionals. `opening` es dispara un sol cop en la transició
|
||||
# CLOSED → OPENING (quan el jugador completa el perímetre). Si la clau
|
||||
# manca, no sona res. El basename val tant de clau al Resource::Cache
|
||||
# com de path (`Defaults::Paths::AUDIO_SOUNDS_DIR + key`).
|
||||
#
|
||||
# `opening_pitch_spread`: variació uniforme del ratio de reproducció
|
||||
# centrada a 1.0. Amb 0.05 cada obertura sona entre 0.95× i 1.05× la
|
||||
# velocitat normal (puja/baixa també el to, efecte arcade clàssic).
|
||||
# 0 ⇒ sempre 1.0 (sense variació).
|
||||
sound:
|
||||
opening: open_sarc.wav
|
||||
opening_pitch_spread: 0.5
|
||||
@@ -136,6 +136,22 @@ void Audio::playSound(Ja::Sound* sound, Group group) {
|
||||
}
|
||||
}
|
||||
|
||||
// Variant amb velocitat (i to) escalats. Apliquem el ratio al canal
|
||||
// just retornat per `playSound`: així el `SDL_AudioStream` recent creat
|
||||
// processa tot el sample amb el ratio des del primer pull del callback.
|
||||
// Si l'engine torna -1 (sense canal lliure) o el so no existeix, no fem
|
||||
// la crida al ratio — sense efectes col·laterals.
|
||||
// NOLINTNEXTLINE(readability-make-member-function-const)
|
||||
void Audio::playSound(const std::string& name, Group group, float speed) {
|
||||
if (!sound_enabled_) { return; }
|
||||
auto* sound = AudioResource::getSound(name);
|
||||
if (sound == nullptr) { return; }
|
||||
const int CH = engine_->playSound(sound, 0, static_cast<int>(group));
|
||||
if (CH >= 0 && speed != 1.0F) {
|
||||
engine_->setChannelSpeed(CH, speed);
|
||||
}
|
||||
}
|
||||
|
||||
// Reprodueix un so processat per un eco definit a sounds.yaml. Si el preset no
|
||||
// existeix o l'engine retorna -1 (cap de canals d'efecte plé), cau a playSound
|
||||
// sec — l'usuari sent el so encara que la cua no s'apliqui.
|
||||
|
||||
@@ -85,6 +85,12 @@ class Audio {
|
||||
// --- Control de sons ---
|
||||
void playSound(const std::string& name, Group group = Group::GAME); // Reproduir so puntual per nom (muta globals de Ja)
|
||||
void playSound(Ja::Sound* sound, Group group = Group::GAME); // Reproduir so puntual per punter (muta globals de Ja)
|
||||
// Reprodueix un so amb la velocitat (i to) escalats per `speed`:
|
||||
// 1.0 = normal, 0.95 ≈ -5% (més greu i lent), 1.05 ≈ +5% (més
|
||||
// agut i ràpid). Mateixa semàntica que `setMusicSpeed`. Útil per a
|
||||
// variacions subtils que eviten la fatiga d'escoltar el mateix
|
||||
// sample idèntic (p.ex. obertures de sarcòfag, picks d'ítems).
|
||||
void playSound(const std::string& name, Group group, float speed);
|
||||
// Reprodueix un so processat per un efecte definit a data/config/sounds.yaml
|
||||
// (preset_name busca a SoundEffectsConfig). Si el preset no existeix
|
||||
// o el motor està al cap de canals amb efecte, fa fallback a playSound
|
||||
|
||||
@@ -430,6 +430,12 @@ namespace Ja {
|
||||
return channel;
|
||||
}
|
||||
|
||||
void Engine::setChannelSpeed(const int channel, const float ratio) {
|
||||
if (channel < 0 || channel >= MAX_SIMULTANEOUS_CHANNELS) { return; }
|
||||
if (channels_[channel].stream == nullptr) { return; }
|
||||
SDL_SetAudioStreamFrequencyRatio(channels_[channel].stream, ratio);
|
||||
}
|
||||
|
||||
void Engine::pauseChannel(const int channel) {
|
||||
forEachTargetChannel(channel, [](Channel& ch) {
|
||||
if (ch.state == ChannelState::PLAYING) {
|
||||
|
||||
@@ -192,6 +192,14 @@ namespace Ja {
|
||||
// --- So ---
|
||||
auto playSound(Sound* sound, int loop = 0, int group = 0) -> int;
|
||||
auto playSoundOnChannel(Sound* sound, int channel, int loop = 0, int group = 0) -> int;
|
||||
// Ajusta la velocitat de reproducció d'un canal actiu via
|
||||
// `SDL_SetAudioStreamFrequencyRatio`. 1.0 = normal. Igual que a
|
||||
// `setMusicSpeed`, puja/baixa el to junt amb la velocitat
|
||||
// (efecte "chipmunk"); per a SFX curts arcade és el que volem.
|
||||
// No-op si el canal no està actiu. Cridar-lo just després de
|
||||
// `playSound`/`playSoundOnChannel` perquè el ratio cobreixi
|
||||
// tota la reproducció.
|
||||
void setChannelSpeed(int channel, float ratio);
|
||||
// Reproducció amb so processat per un efecte. Retorna el canal
|
||||
// assignat o -1 si no queden slots d'efecte (MAX_EFFECT_CHANNELS).
|
||||
// El sound original només s'usa per consultar el spec/buffer; el
|
||||
|
||||
@@ -1,14 +1,26 @@
|
||||
#include "game/entities/concrete/sarcophagus.hpp"
|
||||
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "core/audio/audio.hpp"
|
||||
#include "core/resources/resource_cache.hpp"
|
||||
#include "game/entities/entity_factory.hpp"
|
||||
#include "game/entities/entity_manager.hpp"
|
||||
#include "game/entities/event_bus.hpp"
|
||||
#include "utils/defaults.hpp"
|
||||
|
||||
namespace {
|
||||
|
||||
// RNG local al mòdul, mateix patró que `mummy.cpp`: llavor fixa per
|
||||
// facilitar la depuració. Si algun dia volem aleatoritat real entre
|
||||
// partides, prou amb llegir-la de std::random_device.
|
||||
auto rng() -> std::mt19937& {
|
||||
static std::mt19937 k_rng_{0xCAFEU};
|
||||
return k_rng_;
|
||||
}
|
||||
|
||||
auto buildAnimation(const fkyaml::node& yaml, const std::string& key) -> AnimationComponent {
|
||||
std::vector<int> frames = {0};
|
||||
float fps = 1.0F;
|
||||
@@ -57,6 +69,20 @@ Sarcophagus::Sarcophagus(const fkyaml::node& yaml)
|
||||
interactable_ = true;
|
||||
readTileSize(yaml, tile_size_w_, tile_size_h_);
|
||||
anim_ = anim_closed_;
|
||||
|
||||
// Sons opcionals (bloc `sound:` del YAML). El basename val tant de
|
||||
// clau al Resource::Cache com de path dins `Defaults::Paths::AUDIO_SOUNDS_DIR`.
|
||||
// Mateix patró que `mummy.hurt.sound` i `player.jump.sound`.
|
||||
if (yaml.contains("sound")) {
|
||||
const auto& s = yaml["sound"];
|
||||
if (s.contains("opening")) {
|
||||
opening_sound_key_ = s["opening"].get_value<std::string>();
|
||||
Resource::Cache::get().loadSound(opening_sound_key_, std::string{Defaults::Paths::AUDIO_SOUNDS_DIR} + opening_sound_key_);
|
||||
}
|
||||
if (s.contains("opening_pitch_spread")) {
|
||||
opening_pitch_spread_ = s["opening_pitch_spread"].get_value<float>();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Sarcophagus::place(int anchor_col, int anchor_row, const BlockedPredicate& is_blocked) {
|
||||
@@ -122,6 +148,18 @@ void Sarcophagus::notifyTilePainted(int col, int row) {
|
||||
state_ = State::OPENING;
|
||||
anim_ = anim_opening_;
|
||||
if (anim_.has_value()) { anim_->reset(); }
|
||||
if (!opening_sound_key_.empty() && Audio::get() != nullptr) {
|
||||
// Variació de pitch/velocitat per evitar la fatiga d'escoltar
|
||||
// el mateix sample idèntic a cada obertura. Si l'spread és 0
|
||||
// (default), reproduïm amb ratio 1.0; en cas contrari sortegem
|
||||
// uniformement [1 - spread, 1 + spread].
|
||||
if (opening_pitch_spread_ > 0.0F) {
|
||||
std::uniform_real_distribution<float> dist(1.0F - opening_pitch_spread_, 1.0F + opening_pitch_spread_);
|
||||
Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME, dist(rng()));
|
||||
} else {
|
||||
Audio::get()->playSound(opening_sound_key_, Audio::Group::GAME);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "external/fkyaml_node.hpp"
|
||||
@@ -83,5 +84,12 @@ class Sarcophagus : public Interactable {
|
||||
AnimationComponent anim_opening_;
|
||||
AnimationComponent anim_open_;
|
||||
|
||||
// Clau (basename del fitxer) al Resource::Cache del so que es dispara
|
||||
// a la transició CLOSED → OPENING. Buit ⇒ no sona res.
|
||||
std::string opening_sound_key_;
|
||||
// Variació uniforme del ratio de reproducció al voltant de 1.0. Cada
|
||||
// obertura sorteja [1 - spread, 1 + spread]. 0 ⇒ ratio fix a 1.0.
|
||||
float opening_pitch_spread_{0.0F};
|
||||
|
||||
bool opened_event_emitted_{false};
|
||||
};
|
||||
@@ -18,6 +18,9 @@ auto Audio::get() -> Audio* {
|
||||
void Audio::playSound(const std::string& /*name*/, Audio::Group /*group*/) {
|
||||
}
|
||||
|
||||
void Audio::playSound(const std::string& /*name*/, Audio::Group /*group*/, float /*speed*/) {
|
||||
}
|
||||
|
||||
namespace Resource {
|
||||
|
||||
auto Cache::get() -> Cache& {
|
||||
|
||||
@@ -166,9 +166,11 @@ namespace {
|
||||
REQUIRE(sarc->getVisitedCount() == 8);
|
||||
REQUIRE(sarc->getState() == Sarcophagus::State::OPENING);
|
||||
|
||||
// Un update amb l'animació `opening` d'1 frame loop=false completa
|
||||
// la transició a OPEN.
|
||||
sarc->update(0.0F);
|
||||
// Tickem prou per consumir l'animació `opening` (4 frames @ 6fps =
|
||||
// ~0.67s). Quan completa, la transició OPENING → OPEN es dispara.
|
||||
for (int i = 0; i < 16 && sarc->getState() == Sarcophagus::State::OPENING; ++i) {
|
||||
sarc->update(0.1F);
|
||||
}
|
||||
REQUIRE(sarc->getState() == Sarcophagus::State::OPEN);
|
||||
}
|
||||
|
||||
@@ -208,12 +210,14 @@ namespace {
|
||||
forceOpen(s1);
|
||||
forceOpen(s2);
|
||||
|
||||
// Un primer tick: cada sarc transiciona OPENING → OPEN i publica;
|
||||
// l'handler del manager compta i, en arribar al total, crida
|
||||
// door->open() — que en aquest mateix tick (la iteració sobre
|
||||
// entities continua) farà transitar la porta OPENING → OPEN perquè
|
||||
// l'animació és d'1 frame loop=false.
|
||||
manager.update(0.0F);
|
||||
// Tickem prou per consumir l'animació `opening` dels sarcs (4
|
||||
// frames @ 6fps) i la de la porta. Cada sarc transiciona
|
||||
// OPENING → OPEN i publica; l'handler del manager compta i, en
|
||||
// arribar al total, crida `door->open()`, que també necessita
|
||||
// tickar fins acabar la seua animació d'obertura.
|
||||
for (int i = 0; i < 20 && door_ptr->getState() != Door::State::OPEN; ++i) {
|
||||
manager.update(0.1F);
|
||||
}
|
||||
REQUIRE(manager.openedSarcophagusCount() == 2);
|
||||
REQUIRE(door_ptr->getState() == Door::State::OPEN);
|
||||
|
||||
|
||||
Reference in new issue
Block a user