mirror of
https://github.com/OpenDiablo2/OpenDiablo2
synced 2024-09-30 07:06:18 -04:00
fc87b2be7a
* Remove weapons, armor, misc, itemCommon, itemTyps datadict singletons - removed loader calls from d2app - removed the HeroObjects singleton from `d2core/d2inventory` - added an InventoryItemFactory in d2inventory - package-level functions that use data records are now methods of the InventoryItemFactory - renamed ItemGenerator in d2item to ItemFactory - package-level functions that use records are now methods of ItemFactory - d2map.MapEntityFactory now has an item factory instance for creating items - fixed a bug in unique item record loader where it loaded an empty record - added a PlayerStateFactory for creating a player state (uses the asset manager) - updated the test inventory/equipment code in d2player to handle errors from the ItemFactory - character select and character creation screens have a player state and inventory item factory - updated item tests to use the item factory * minor edit * Removed d2datadict.Experience singleton added a HeroStatsFactory, much like the other factories. The factory gets an asset manager reference in order to use data records. * removed d2datadict.AutoMagic singleton * removed d2datadict.AutoMap singleton * removed d2datadict.BodyLocations singleton * removed d2datadict.Books singleton * Removed singletons for level records - removed loader calls in d2app - changed type references from d2datadict to d2records - added a `MapGenerator` in d2mapgen which uses thew asset manager and map engine - package-level map generation functions are now MapGenerator methods - `d2datadict.GetLevelDetails(id int)` is now a method of the RecordManager * remove SkillCalc and MissileCalc singletons * Removed CharStats and ItemStatCost singletons - added an ItemStatFactory which uses the asset manager to create stats - package-level functions for stats in d2item are now StatFactory methods - changed type references from d2datadict to d2records - `d2player.GetAllPlayerStates` is now a method of the `PlayerStateFactory` * Removed DkillDesc and Skills singletons from d2datadict - removed loader calls from d2app - diablo2stats.Stat instances are given a reference to the factory for doing record lookups * update the stats test to use mock a asset manager and stat factory * fixed diablo2stats tests and diablo2item tests * removed CompCodes singleton from d2datadict * remove cubemain singleton from d2datadict * removed DifficultyLevels singleton from d2datadict * removed ElemTypes singleton from d2datadict * removed events.go loader from d2datadict (was unused) * removed Gems singleton from d2datadict * removed Hireling and Inventory singletons from d2datadict * removed MagicPrefix and MagicSuffix singletons from d2datadict * removed ItemRatios singleton from d2datadict * removed Missiles singleton from d2datadict * removed MonModes singleton * Removed all monster and npc singletons from d2datadict - MapStamp instances now get a reference to their factory for doing record lookups * removed SoundEntry and SoundEnviron singletons from d2datadict
207 lines
4.4 KiB
Go
207 lines
4.4 KiB
Go
package d2audio
|
|
|
|
import (
|
|
"log"
|
|
"math/rand"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
|
|
|
|
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
|
|
)
|
|
|
|
type envState int
|
|
|
|
const (
|
|
envAttack = 0
|
|
envSustain = 1
|
|
envRelease = 2
|
|
envStopped = 3
|
|
)
|
|
|
|
const volMax float64 = 255
|
|
const originalFPS float64 = 25
|
|
|
|
// A Sound that can be started and stopped
|
|
type Sound struct {
|
|
effect d2interface.SoundEffect
|
|
entry *d2records.SoundDetailsRecord
|
|
volume float64
|
|
vTarget float64
|
|
vRate float64
|
|
state envState
|
|
// panning float64 // lets forget about this for now
|
|
}
|
|
|
|
func (s *Sound) update(elapsed float64) {
|
|
// attack
|
|
if s.state == envAttack {
|
|
s.volume += s.vRate * elapsed
|
|
if s.volume > s.vTarget {
|
|
s.volume = s.vTarget
|
|
s.state = envSustain
|
|
}
|
|
|
|
s.effect.SetVolume(s.volume)
|
|
}
|
|
|
|
// release
|
|
if s.state == envRelease {
|
|
s.volume -= s.vRate * elapsed
|
|
if s.volume < 0 {
|
|
s.effect.Stop()
|
|
s.volume = 0
|
|
s.state = envStopped
|
|
}
|
|
|
|
s.effect.SetVolume(s.volume)
|
|
}
|
|
}
|
|
|
|
// SetPan sets the stereo pan, range -1 to 1
|
|
func (s *Sound) SetPan(pan float64) {
|
|
s.effect.SetPan(pan)
|
|
}
|
|
|
|
// Play the sound
|
|
func (s *Sound) Play() {
|
|
log.Println("starting sound", s.entry.Handle)
|
|
s.effect.Play()
|
|
|
|
if s.entry.FadeIn != 0 {
|
|
s.effect.SetVolume(0)
|
|
s.volume = 0
|
|
s.state = envAttack
|
|
s.vTarget = float64(s.entry.Volume) / volMax
|
|
s.vRate = s.vTarget / (float64(s.entry.FadeIn) / originalFPS)
|
|
} else {
|
|
s.volume = float64(s.entry.Volume) / volMax
|
|
s.effect.SetVolume(s.volume)
|
|
s.state = envSustain
|
|
}
|
|
}
|
|
|
|
// Stop the sound, only required for looping sounds
|
|
func (s *Sound) Stop() {
|
|
if s.entry.FadeOut != 0 {
|
|
s.state = envRelease
|
|
s.vTarget = 0
|
|
s.vRate = s.volume / (float64(s.entry.FadeOut) / originalFPS)
|
|
} else {
|
|
s.state = envStopped
|
|
s.volume = 0
|
|
s.effect.SetVolume(s.volume)
|
|
s.effect.Stop()
|
|
}
|
|
}
|
|
|
|
// SoundEngine provides functions for playing sounds
|
|
type SoundEngine struct {
|
|
asset *d2asset.AssetManager
|
|
provider d2interface.AudioProvider
|
|
timer float64
|
|
accTime float64
|
|
sounds map[*Sound]struct{}
|
|
}
|
|
|
|
// NewSoundEngine creates a new sound engine
|
|
func NewSoundEngine(provider d2interface.AudioProvider,
|
|
asset *d2asset.AssetManager, term d2interface.Terminal) *SoundEngine {
|
|
r := SoundEngine{
|
|
asset: asset,
|
|
provider: provider,
|
|
sounds: map[*Sound]struct{}{},
|
|
timer: 1,
|
|
}
|
|
|
|
_ = term.BindAction("playsoundid", "plays the sound for a given id", func(id int) {
|
|
r.PlaySoundID(id)
|
|
})
|
|
|
|
_ = term.BindAction("playsound", "plays the sound for a given handle string", func(handle string) {
|
|
r.PlaySoundHandle(handle)
|
|
})
|
|
|
|
_ = term.BindAction("activesounds", "list currently active sounds", func() {
|
|
for s := range r.sounds {
|
|
log.Println(s)
|
|
}
|
|
})
|
|
|
|
_ = term.BindAction("killsounds", "kill active sounds", func() {
|
|
for s := range r.sounds {
|
|
s.Stop()
|
|
}
|
|
})
|
|
|
|
return &r
|
|
}
|
|
|
|
// Advance updates sound engine state, triggering events and envelopes
|
|
func (s *SoundEngine) Advance(elapsed float64) {
|
|
s.timer -= elapsed
|
|
s.accTime += elapsed
|
|
|
|
if s.timer < 0 {
|
|
for sound := range s.sounds {
|
|
sound.update(s.accTime)
|
|
|
|
// Clean up finished non-looping effects
|
|
if !sound.effect.IsPlaying() {
|
|
delete(s.sounds, sound)
|
|
}
|
|
|
|
// Clean up stopped looping effects
|
|
if sound.state == envStopped {
|
|
delete(s.sounds, sound)
|
|
}
|
|
}
|
|
|
|
s.timer = 0.2
|
|
s.accTime = 0
|
|
}
|
|
}
|
|
|
|
// Reset stop all sounds and reset state
|
|
func (s *SoundEngine) Reset() {
|
|
for snd := range s.sounds {
|
|
snd.effect.Stop()
|
|
delete(s.sounds, snd)
|
|
}
|
|
}
|
|
|
|
// PlaySoundID plays a sound by sounds.txt index, returning the sound here is kinda ugly
|
|
// now we could have a situation where someone holds onto the sound after the sound engine is done with it
|
|
// someone needs to be in charge of deciding when to stopping looping sounds though...
|
|
func (s *SoundEngine) PlaySoundID(id int) *Sound {
|
|
if id == 0 {
|
|
return nil
|
|
}
|
|
|
|
entry := s.asset.Records.SelectSoundByIndex(id)
|
|
|
|
if entry.GroupSize > 0 {
|
|
entry = s.asset.Records.SelectSoundByIndex(entry.Index + rand.Intn(entry.GroupSize))
|
|
}
|
|
|
|
effect, _ := s.provider.LoadSound(entry.FileName, entry.Loop, entry.MusicVol)
|
|
|
|
snd := Sound{
|
|
entry: entry,
|
|
effect: effect,
|
|
}
|
|
|
|
s.sounds[&snd] = struct{}{}
|
|
|
|
snd.Play()
|
|
|
|
return &snd
|
|
}
|
|
|
|
// PlaySoundHandle plays a sound by sounds.txt handle
|
|
func (s *SoundEngine) PlaySoundHandle(handle string) *Sound {
|
|
sound := s.asset.Records.Sound.Details[handle].Index
|
|
return s.PlaySoundID(sound)
|
|
}
|