Open source Star Ruler 2 source code!
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
const float fadeInTime = 1000.f;
|
||||
const float fadeOutTime = 3000.f;
|
||||
|
||||
//Delay (seconds) to avoid playing music after the track ends
|
||||
float silenceTime = 0;
|
||||
|
||||
//Calculated volume for the music tracks
|
||||
double musicVolume = 1.0;
|
||||
|
||||
class Layer {
|
||||
Movement@ movement;
|
||||
Sound@ track;
|
||||
float currentVolume = 1;
|
||||
float volumeGoal = 1;
|
||||
float fadeTime = 0;
|
||||
|
||||
string name;
|
||||
|
||||
Layer(Movement@ moveTrack, const string& file, float Volume = 1.f, bool loop = false) {
|
||||
name = file;
|
||||
@movement = moveTrack;
|
||||
|
||||
//Sounds don't play if the volume is 0
|
||||
if(soundVolume <= 0)
|
||||
return;
|
||||
|
||||
@track = playTrack(file, loop, true);
|
||||
currentVolume = Volume;
|
||||
volumeGoal = Volume;
|
||||
if(track !is null) {
|
||||
track.volume = Volume;
|
||||
}
|
||||
else {
|
||||
error("Could not load track " + file);
|
||||
}
|
||||
}
|
||||
|
||||
void volumeFade(float goal, float seconds) {
|
||||
volumeGoal = goal;
|
||||
fadeTime = seconds;
|
||||
}
|
||||
|
||||
void stop() {
|
||||
if(track !is null)
|
||||
track.stop();
|
||||
@track = null;
|
||||
@movement = null;
|
||||
}
|
||||
|
||||
void play() {
|
||||
if(track !is null)
|
||||
track.paused = false;
|
||||
}
|
||||
|
||||
void updateVolume() {
|
||||
track.volume = currentVolume * musicVolume;
|
||||
}
|
||||
|
||||
bool update(double time) {
|
||||
if(track is null)
|
||||
return true;
|
||||
|
||||
if(currentVolume != volumeGoal) {
|
||||
if(time >= fadeTime) {
|
||||
currentVolume = volumeGoal;
|
||||
fadeTime = 0;
|
||||
}
|
||||
else {
|
||||
currentVolume += (volumeGoal - currentVolume) * (time/fadeTime);
|
||||
fadeTime -= time;
|
||||
}
|
||||
|
||||
track.volume = currentVolume * musicVolume;
|
||||
}
|
||||
|
||||
if(track.playing) {
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
@track = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
array<string> tracks;
|
||||
int lastTrack = -1;
|
||||
string get_nextTrack() {
|
||||
int nextIndex;
|
||||
do {
|
||||
nextIndex = randomi(0,tracks.length-1);
|
||||
} while(nextIndex == lastTrack && tracks.length != 1);
|
||||
|
||||
lastTrack = nextIndex;
|
||||
return tracks[nextIndex];
|
||||
}
|
||||
|
||||
Namespace musicVars;
|
||||
array<Arrangement@> arrangements, queued;
|
||||
array<Layer@> layers;
|
||||
StatTracker winTracker(stat::ShipsDestroyed, "ships_destroyed"), lossTracker(stat::ShipsLost, "ships_lost");
|
||||
|
||||
class Movement {
|
||||
string name;
|
||||
string track;
|
||||
bool loop = false;
|
||||
Formula@ volume;
|
||||
|
||||
int volVar, timeVar, playingVar;
|
||||
|
||||
Movement(const string& Name) {
|
||||
name = Name;
|
||||
volVar = musicVars.lookup(name + ".volume", true);
|
||||
timeVar = musicVars.lookup(name + ".time", true);
|
||||
playingVar = musicVars.lookup(name + ".playing", true);
|
||||
}
|
||||
};
|
||||
|
||||
class Arrangement {
|
||||
array<Movement@> movements;
|
||||
double playOrder = 0;
|
||||
string name;
|
||||
|
||||
int opCmp(const Arrangement& other) {
|
||||
if(playOrder < other.playOrder)
|
||||
return -1;
|
||||
else if(playOrder == other.playOrder)
|
||||
return 0;
|
||||
else
|
||||
return 1;
|
||||
}
|
||||
|
||||
void prepare() {
|
||||
//Load up all tracks, then start them when they've all been loaded
|
||||
for(uint i = 0; i < movements.length; ++i) {
|
||||
Movement@ movement = movements[i];
|
||||
layers.insertLast(Layer(movement, movement.track, 0, movement.loop));
|
||||
musicVars.setConstant(movement.playingVar, 1.0);
|
||||
}
|
||||
|
||||
for(uint i = 0; i < layers.length; ++i)
|
||||
layers[i].play();
|
||||
}
|
||||
};
|
||||
|
||||
void init() {
|
||||
if(!soundEnabled)
|
||||
return;
|
||||
|
||||
//Load arrangements
|
||||
ReadFile file(resolve("data/music/orchestra.txt"));
|
||||
|
||||
Arrangement@ arrange;
|
||||
Movement@ movement;
|
||||
|
||||
string key, value;
|
||||
while(file++) {
|
||||
key = file.key;
|
||||
value = file.value;
|
||||
|
||||
if(key == "Arrangement") {
|
||||
@arrange = Arrangement();
|
||||
arrange.name = value;
|
||||
arrangements.insertLast(arrange);
|
||||
}
|
||||
else if(arrange !is null) {
|
||||
if(key == "Era") {
|
||||
if(value == "Industrial")
|
||||
arrange.playOrder = randomd(0.75, 1.25);
|
||||
else if(value == "Imperial")
|
||||
arrange.playOrder = randomd(1.2, 1.5);
|
||||
else
|
||||
arrange.playOrder = randomd(0.0, 1.0);
|
||||
}
|
||||
else if(key == "Movement") {
|
||||
@movement = Movement(value);
|
||||
arrange.movements.insertLast(movement);
|
||||
}
|
||||
else if(movement !is null) {
|
||||
if(key == "Track") {
|
||||
movement.track = value;
|
||||
}
|
||||
else if(key == "Volume") {
|
||||
@movement.volume = Formula(value);
|
||||
}
|
||||
else if(key == "Loop") {
|
||||
movement.loop = toBool(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//If we're starting a new-ish game, play the music in a semi-ordered way
|
||||
if(gameTime < 30.0 * 60.0) {
|
||||
queued = arrangements;
|
||||
queued.sortAsc();
|
||||
uint remove = min(uint(max(double(queued.length) * gameTime / (30.0 / 60.0), 0.0)), queued.length);
|
||||
for(uint i = 0; i < remove; ++i)
|
||||
queued.removeAt(0);
|
||||
}
|
||||
|
||||
if(soundVolume > 0)
|
||||
playNewArrangement();
|
||||
}
|
||||
|
||||
Arrangement@ prevArrangement;
|
||||
void randomize() {
|
||||
queued = arrangements;
|
||||
for(uint i = 0, cnt = queued.length; i < cnt; ++i) {
|
||||
uint o = randomi(i, cnt-1);
|
||||
if(o != i) {
|
||||
auto@ a = queued[i];
|
||||
@queued[i] = queued[o];
|
||||
@queued[o] = a;
|
||||
}
|
||||
}
|
||||
|
||||
if(prevArrangement !is null && prevArrangement is queued[0] && queued.length > 1) {
|
||||
uint i = randomi(1, queued.length-1);
|
||||
auto@ a = queued[0];
|
||||
@queued[0] = queued[i];
|
||||
@queued[i] = a;
|
||||
}
|
||||
}
|
||||
|
||||
void playNewArrangement() {
|
||||
if(arrangements.length == 0)
|
||||
return;
|
||||
else if(arrangements.length == 1) {
|
||||
arrangements[0].prepare();
|
||||
return;
|
||||
}
|
||||
else if(queued.length == 0) {
|
||||
randomize();
|
||||
}
|
||||
|
||||
Arrangement@ nextArrangement = queued[0];
|
||||
queued.removeAt(0);
|
||||
nextArrangement.prepare();
|
||||
@prevArrangement = nextArrangement;
|
||||
}
|
||||
|
||||
void deinit() {
|
||||
for(uint i = 0; i < layers.length; ++i)
|
||||
layers[i].stop();
|
||||
}
|
||||
|
||||
double prevMusicVol = 1.0, prevMasterVol = 1.0, prevSFXVol = 1.0;
|
||||
void tick(double time) {
|
||||
if(!soundEnabled)
|
||||
return;
|
||||
|
||||
//Update volume settings
|
||||
if(prevMusicVol != settings::dMusicVolume || prevSFXVol != settings::dSFXVolume || prevMasterVol != settings::dMasterVolume) {
|
||||
if(settings::dSFXVolume < 0.01)
|
||||
musicVolume = settings::dMusicVolume;
|
||||
else
|
||||
musicVolume = settings::dMusicVolume / settings::dSFXVolume;
|
||||
|
||||
for(int i = layers.length - 1; i >= 0; --i)
|
||||
layers[i].updateVolume();
|
||||
prevMusicVol = settings::dMusicVolume;
|
||||
prevMasterVol = settings::dMasterVolume;
|
||||
prevSFXVol = settings::dSFXVolume;
|
||||
}
|
||||
|
||||
if(layers.length == 0) {
|
||||
silenceTime -= time;
|
||||
|
||||
if(soundVolume > 0 && silenceTime <= 0.0) {
|
||||
playNewArrangement();
|
||||
//Silence music for 3 seconds after we attempt to resume the tracks
|
||||
// Mostly, this avoids spamming errors when failing to play trakcs for some reason
|
||||
silenceTime = 3.0;
|
||||
}
|
||||
else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
winTracker.update();
|
||||
lossTracker.update();
|
||||
|
||||
bool activeTrack = false;
|
||||
|
||||
for(int i = layers.length - 1; i >= 0; --i) {
|
||||
Layer@ layer = layers[i];
|
||||
if(layer.update(time)) {
|
||||
musicVars.setConstant(layer.movement.playingVar, 0.0);
|
||||
layers.removeAt(i);
|
||||
}
|
||||
else {
|
||||
if(!layer.movement.loop || layer.currentVolume > 0.01)
|
||||
activeTrack = true;
|
||||
musicVars.setConstant(layer.movement.playingVar, 1.0);
|
||||
musicVars.setConstant(layer.movement.timeVar, double(layer.track.playPosition) / 1000.0);
|
||||
musicVars.setConstant(layer.movement.volVar, layer.currentVolume);
|
||||
layer.volumeFade(layer.movement.volume.evaluate(musicVars), 2);
|
||||
}
|
||||
}
|
||||
|
||||
if(!activeTrack) {
|
||||
for(int i = layers.length - 1; i >= 0; --i)
|
||||
layers[i].stop();
|
||||
layers.length = 0;
|
||||
}
|
||||
}
|
||||
|
||||
class StatTracker {
|
||||
stat::EmpireStat stat;
|
||||
|
||||
string name;
|
||||
int accVar;
|
||||
|
||||
uint lastTime = 0;
|
||||
int lastVal = 0;
|
||||
int accumulation = 0;
|
||||
|
||||
float decay = 0.7;
|
||||
|
||||
StatTracker(stat::EmpireStat Stat, string Name) {
|
||||
stat = Stat;
|
||||
name = Name;
|
||||
accVar = musicVars.lookup(name + ".acc", true);
|
||||
}
|
||||
|
||||
void update() {
|
||||
if(lastTime + 1 >= (systemTime/1000))
|
||||
return;
|
||||
lastTime = systemTime/1000;
|
||||
|
||||
if(accumulation > 0)
|
||||
accumulation = int(ceil(float(accumulation) * decay)) - 1;
|
||||
|
||||
int value = 0;
|
||||
{
|
||||
StatHistory history(playerEmpire, stat);
|
||||
if(history.advance(-1))
|
||||
value = history.intVal;
|
||||
}
|
||||
|
||||
//Accumulate events
|
||||
accumulation = max(accumulation + (value - lastVal),0);
|
||||
|
||||
lastVal = value;
|
||||
|
||||
musicVars.setConstant(accVar, double(accumulation));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user