diff --git a/.gitignore b/.gitignore index e8b2308..aea3f71 100644 --- a/.gitignore +++ b/.gitignore @@ -3,8 +3,9 @@ fonts/ maps/ bin/ dist/ +rtp/ dev-assets/guidebook/venv -dev-assets/guidebook/compiled/pages +dev-assets/guidebook/site/ wasm/assets/ *.wasm *.doodad @@ -15,3 +16,4 @@ docker/fedora screenshot-*.png map-*.json pkg/wallpaper/*.png + diff --git a/Building.md b/Building.md index 6ffe704..0f075f2 100644 --- a/Building.md +++ b/Building.md @@ -84,10 +84,10 @@ Dependencies are Go, SDL2 and SDL2_ttf: ```bash # Fedora -sudo dnf -y install golang SDL2-devel SDL2_ttf-devel +sudo dnf -y install golang SDL2-devel SDL2_ttf-devel SDL2_mixer-devel # Ubuntu and Debian -sudo apt -y install golang libsdl2-dev libsdl2-ttf-dev +sudo apt -y install golang libsdl2-dev libsdl2-ttf-dev libsdl2-mixer-devel ``` ## Mac OS diff --git a/cmd/doodle/main.go b/cmd/doodle/main.go index f6e459f..74f2e03 100644 --- a/cmd/doodle/main.go +++ b/cmd/doodle/main.go @@ -12,6 +12,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/branding" + "git.kirsle.net/apps/doodle/pkg/sound" "git.kirsle.net/go/render/sdl" "github.com/urfave/cli" @@ -95,6 +96,9 @@ func main() { panic(err) } + // Preload all sound effects. + sound.PreloadAll() + game := doodle.New(c.Bool("debug"), engine) game.SetupEngine() if c.Bool("guitest") { diff --git a/dev-assets/doodads/buttons/button.js b/dev-assets/doodads/buttons/button.js index ceac9e7..c9c8237 100644 --- a/dev-assets/doodads/buttons/button.js +++ b/dev-assets/doodads/buttons/button.js @@ -2,6 +2,7 @@ function main() { console.log("%s initialized!", Self.Title); var timer = 0; + var pressed = false; Events.OnCollide(function(e) { if (!e.Settled) { @@ -13,7 +14,12 @@ function main() { return; } - Message.Publish("power", true); + if (!pressed) { + Sound.Play("button-down.wav") + Message.Publish("power", true); + pressed = true; + } + if (timer > 0) { clearTimeout(timer); @@ -21,9 +27,11 @@ function main() { Self.ShowLayer(1); timer = setTimeout(function() { + Sound.Play("button-up.wav") Self.ShowLayer(0); Message.Publish("power", false); timer = 0; + pressed = false; }, 200); }); } diff --git a/dev-assets/doodads/buttons/sticky.js b/dev-assets/doodads/buttons/sticky.js index 9d1893e..9dac758 100644 --- a/dev-assets/doodads/buttons/sticky.js +++ b/dev-assets/doodads/buttons/sticky.js @@ -8,6 +8,7 @@ function main() { if (powered && pressed) { Self.ShowLayer(0); pressed = false; + Sound.Play("button-up.wav") Message.Publish("power", false); } }) @@ -26,6 +27,7 @@ function main() { return; } + Sound.Play("button-down.wav") Self.ShowLayer(1); pressed = true; Message.Publish("power", true); diff --git a/dev-assets/doodads/crumbly-floor/crumbly-floor.js b/dev-assets/doodads/crumbly-floor/crumbly-floor.js index 509101b..97c2be0 100644 --- a/dev-assets/doodads/crumbly-floor/crumbly-floor.js +++ b/dev-assets/doodads/crumbly-floor/crumbly-floor.js @@ -43,6 +43,7 @@ function main() { Self.PlayAnimation("shake", function() { state = stateFalling; Self.PlayAnimation("fall", function() { + Sound.Play("crumbly-break.wav") state = stateFallen; Self.ShowLayerNamed("fallen"); diff --git a/dev-assets/doodads/doors/colored-door.js b/dev-assets/doodads/doors/colored-door.js index f3e12fc..1967449 100644 --- a/dev-assets/doodads/doors/colored-door.js +++ b/dev-assets/doodads/doors/colored-door.js @@ -32,6 +32,7 @@ function main() { if (unlocked) { Self.ShowLayer(enterSide < 0 ? layer.right : layer.left); opened = true; + Sound.Play("door-open.wav") return; } @@ -45,11 +46,13 @@ function main() { if (e.Settled) { unlocked = true; Self.ShowLayer(enterSide < 0 ? layer.right : layer.left); + Sound.Play("unlock.wav") } } }); Events.OnLeave(function(e) { Self.ShowLayer(layer.closed); + // Sound.Play("door-close.wav") // Reset collision state. opened = false; diff --git a/dev-assets/doodads/doors/electric-door.js b/dev-assets/doodads/doors/electric-door.js index 1642890..43056c6 100644 --- a/dev-assets/doodads/doors/electric-door.js +++ b/dev-assets/doodads/doors/electric-door.js @@ -17,12 +17,14 @@ function main() { } animating = true; + Sound.Play("electric-door.wav") Self.PlayAnimation("open", function() { opened = true; animating = false; }); } else { animating = true; + Sound.Play("electric-door.wav") Self.PlayAnimation("close", function() { opened = false; animating = false; diff --git a/dev-assets/doodads/doors/keys.js b/dev-assets/doodads/doors/keys.js index 159069e..00cc6d7 100644 --- a/dev-assets/doodads/doors/keys.js +++ b/dev-assets/doodads/doors/keys.js @@ -3,6 +3,7 @@ function main() { Events.OnCollide(function(e) { if (e.Settled) { + Sound.Play("item-get.wav") e.Actor.AddItem(Self.Filename, 0); Self.Destroy(); } diff --git a/dev-assets/doodads/switches/switch.js b/dev-assets/doodads/switches/switch.js index c36b2de..c62689a 100644 --- a/dev-assets/doodads/switches/switch.js +++ b/dev-assets/doodads/switches/switch.js @@ -19,6 +19,7 @@ function main() { } if (collide === false) { + Sound.Play("button-down.wav") state = !state; Message.Publish("power", state); showState(state); diff --git a/dev-assets/guidebook/pages/DoodadScripts.md b/dev-assets/guidebook/docs/custom-doodads/scripts.md similarity index 100% rename from dev-assets/guidebook/pages/DoodadScripts.md rename to dev-assets/guidebook/docs/custom-doodads/scripts.md diff --git a/pkg/scripting/js_api.go b/pkg/scripting/js_api.go index 54a57fa..5323619 100644 --- a/pkg/scripting/js_api.go +++ b/pkg/scripting/js_api.go @@ -6,6 +6,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/physics" "git.kirsle.net/apps/doodle/pkg/shmem" + "git.kirsle.net/apps/doodle/pkg/sound" "git.kirsle.net/go/render" ) @@ -25,6 +26,11 @@ func NewJSProxy(vm *VM) JSProxy { "error": log.Error, }, + // Audio API. + "Sound": map[string]interface{}{ + "Play": sound.PlaySound, + }, + // Type constructors. "RGBA": render.RGBA, "Point": render.NewPoint, diff --git a/pkg/sound/preload.go b/pkg/sound/preload.go new file mode 100644 index 0000000..b51546d --- /dev/null +++ b/pkg/sound/preload.go @@ -0,0 +1,25 @@ +package sound + +import ( + "io/ioutil" + "path/filepath" +) + +// PreloadAll looks in the SoundRoot and MusicRoot folders and preloads all +// supported files into the caches. +func PreloadAll() { + if engine == nil || !Enabled { + return + } + + // Preload sound effects. + if files, err := ioutil.ReadDir(SoundRoot); err == nil { + for _, file := range files { + if filepath.Ext(file.Name()) != ".wav" { + continue + } + + LoadSound(file.Name()) + } + } +} diff --git a/pkg/sound/sound.go b/pkg/sound/sound.go new file mode 100644 index 0000000..45de411 --- /dev/null +++ b/pkg/sound/sound.go @@ -0,0 +1,114 @@ +// Package sound provides audio functions for Doodle. +package sound + +import ( + "path/filepath" + "sync" + + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/go/audio/sdl" + "github.com/veandco/go-sdl2/mix" +) + +// Globals. +var ( + // If enabled is false, all sound functions are no-ops. + Enabled bool + + // Root folder on disk where sound and music files should live. + SoundRoot = filepath.Join("rtp", "sfx") + MusicRoot = filepath.Join("rtp", "music") + + // Cache of loaded music and sound effects. + music = map[string]*sdl.Track{} + sounds = map[string]*sdl.Track{} + mu sync.RWMutex + + engine *sdl.Engine +) + +// Initialize SDL2 Audio at startup. +func init() { + eng, err := sdl.New(mix.INIT_MP3 | mix.INIT_OGG) + if err != nil { + log.Error("sound.init(): error initializing SDL2 audio: %s", err) + return + } + + err = eng.Setup() + if err != nil { + log.Error("sound.init(): error setting up SDL2 audio: %s", err) + return + } + + engine = eng + Enabled = true +} + +// LoadMusic loads filename from the MusicRoot into the global music cache. +// If the music is already loaded, does nothing. +func LoadMusic(filename string) *sdl.Track { + if engine == nil || !Enabled { + return nil + } + + // Check if the music is already loaded. + mu.RLock() + mus, ok := music[filename] + mu.RUnlock() + if ok { + return mus + } + + // Load the music in. + track, err := engine.LoadMusic(filepath.Join(MusicRoot, filename)) + if err != nil { + log.Error("sound.LoadMusic: failed to load file %s: %s", filename, err) + return nil + } + + mu.Lock() + music[filename] = &track + mu.Unlock() + + return &track +} + +// LoadSound loads filename from the SoundRoot into the global SFX cache. +// If the sound is already loaded, does nothing. +func LoadSound(filename string) *sdl.Track { + if engine == nil || !Enabled { + return nil + } + + // Check if the music is already loaded. + mu.RLock() + sfx, ok := sounds[filename] + mu.RUnlock() + if ok { + return sfx + } + + // Load the sound in. + log.Info("Loading sound: %s", filename) + track, err := engine.LoadSound(filepath.Join(SoundRoot, filename)) + if err != nil { + log.Error("sound.LoadSound: failed to load file %s: %s", filename, err) + return nil + } + + mu.Lock() + sounds[filename] = &track + mu.Unlock() + + return &track +} + +// PlaySound plays the named sound. +func PlaySound(filename string) { + log.Debug("Play sound: %s", filename) + sound := LoadSound(filename) + if sound != nil { + sound.Play(1) + } +}