From 3d3a55af9d225e15d1a23bc6cd6101bbb3b082f4 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Wed, 25 Mar 2020 02:12:17 +0000 Subject: [PATCH] Break flow out of ordoor --- internal/ordoor/flow/flow.go | 187 +++++++++++++++++++ internal/ordoor/flow/options.go | 114 ++++++++++++ internal/ordoor/interfaces.go | 307 -------------------------------- internal/ordoor/ordoor.go | 37 ++-- internal/ui/window.go | 14 +- 5 files changed, 338 insertions(+), 321 deletions(-) create mode 100644 internal/ordoor/flow/flow.go create mode 100644 internal/ordoor/flow/options.go delete mode 100644 internal/ordoor/interfaces.go diff --git a/internal/ordoor/flow/flow.go b/internal/ordoor/flow/flow.go new file mode 100644 index 0000000..debdc93 --- /dev/null +++ b/internal/ordoor/flow/flow.go @@ -0,0 +1,187 @@ +package flow + +import ( + "errors" + + "github.com/hajimehoshi/ebiten" + + "code.ur.gs/lupine/ordoor/internal/assetstore" + "code.ur.gs/lupine/ordoor/internal/config" + "code.ur.gs/lupine/ordoor/internal/ui" +) + +// type Flow is responsible for wiring up UI elements to each other and ensuring +// they behave as expected. This includes forward / back buttons to switch +// between screens, loading and saving options, launching a scenario, etc +type Flow struct { + assets *assetstore.AssetStore + config *config.Config + current *ui.Driver + drivers map[driverName]*ui.Driver + + exit error +} + +type driverName string + +const ( + // Names of all the drivers + main driverName = "main" + levelPly driverName = "levelPly" + singles driverName = "singles" + randomMap driverName = "randomMap" + newGame driverName = "newGame" + loadGame driverName = "loadGame" + options driverName = "options" + kbd driverName = "keyboard" +) + +var ( + ErrExit = errors.New("exiting gracefully") + + driverNames = []driverName{ + main, levelPly, singles, randomMap, newGame, loadGame, options, kbd, + } + + // Constants used for sliders + + h3Slider = map[int]int{1: 8, 2: 56, 3: 110, 4: 120} + + v10Slider = map[int]int{ + 0: 0, + 10: 9, 20: 18, 30: 27, 40: 36, 50: 45, + 60: 54, 70: 63, 80: 72, 90: 81, 100: 90, + } + + h9Slider = map[int]int{ + 0: 0, + 10: 10, 20: 20, 30: 30, 40: 40, + 50: 50, 60: 60, 70: 70, 80: 80, + } +) + +func New(assets *assetstore.AssetStore, config *config.Config) (*Flow, error) { + out := &Flow{ + assets: assets, + config: config, + drivers: make(map[driverName]*ui.Driver, len(driverNames)), + } + + // Load all the drivers upfront + for _, name := range driverNames { + driver, err := buildDriver(assets, name) + if err != nil { + return nil, err + } + + out.drivers[name] = driver + } + + // Initial load of the config into the options UI + if err := out.configIntoOptions(); err != nil { + return nil, err + } + + out.linkDrivers() + out.setDriverNow(main) + + return out, nil +} + +func (f *Flow) Update(screenX, screenY int) error { + if f.exit != nil { + return f.exit + } + + return f.current.Update(screenX, screenY) +} + +func (f *Flow) Draw(screen *ebiten.Image) error { + if f.exit != nil { + return f.exit + } + + return f.current.Draw(screen) +} + +func (f *Flow) setDriver(name driverName) func() { + return func() { + f.setDriverNow(name) + } +} + +func (f *Flow) setDriverNow(name driverName) { + f.current = f.drivers[name] +} + +func (f *Flow) setExit() { + f.exit = ErrExit +} + +func (f *Flow) linkDrivers() { + // Main interface + f.onClick(main, "2.1", f.setDriver(newGame)) // New game + f.onClick(main, "2.2", f.setDriver(loadGame)) // Load game + f.setFreeze(main, "2.3", true) // Multiplayer - disable for now + f.onClick(main, "2.4", f.setDriver(options)) // Options + f.onClick(main, "2.5", f.setExit) // Quit + + // New game + f.onClick(newGame, "2.1", f.setDriver(levelPly)) // New campaign button + f.onClick(newGame, "2.2", f.setDriver(singles)) // Single scenario button + f.onClick(newGame, "2.3", f.setDriver(randomMap)) // Random scenario button + f.onClick(newGame, "2.4", f.setDriver(main)) // Back button + + // Load game + f.onClick(loadGame, "3.3", f.setDriver(main)) // Cancel button + + // Options + f.linkOptions() + + // Level of play select + f.onClick(levelPly, "2.5", f.setDriver(newGame)) // Back button + + // Single scenario setup + f.onClick(singles, "4.11", f.setDriver(newGame)) // Back button + + // Random map setup + f.onClick(randomMap, "2.19", f.setDriver(newGame)) // Back button +} + +func (f *Flow) configureSlider(driver driverName, id string, steps map[int]int) { + if f.exit != nil { + return + } + + f.exit = f.drivers[driver].ConfigureSlider(id, steps) +} + +func (f *Flow) onClick(driver driverName, id string, fn func()) { + if f.exit != nil { + return + } + + f.exit = f.drivers[driver].OnClick(id, fn) +} + +func (f *Flow) setFreeze(driver driverName, id string, value bool) { + if f.exit != nil { + return + } + + f.exit = f.drivers[driver].SetFreeze(id, value) +} + +func buildDriver(assets *assetstore.AssetStore, name driverName) (*ui.Driver, error) { + menu, err := assets.Menu(string(name)) + if err != nil { + return nil, err + } + + driver, err := ui.NewDriver(menu) + if err != nil { + return nil, err + } + + return driver, nil +} diff --git a/internal/ordoor/flow/options.go b/internal/ordoor/flow/options.go new file mode 100644 index 0000000..8a2cb0d --- /dev/null +++ b/internal/ordoor/flow/options.go @@ -0,0 +1,114 @@ +package flow + +import ( + "log" +) + +func (f *Flow) linkOptions() { + // Main options + f.onClick(options, "2.8", f.setDriver(kbd)) // Keyboard settings button + + f.configureSlider(options, "2.9", h3Slider) // Resolution slider + f.configureSlider(options, "2.10", v10Slider) // Music volume slider + f.configureSlider(options, "2.11", v10Slider) // SFX volume slider + + f.onClick(options, "2.12", f.acceptOptions()) // OK button + f.onClick(options, "2.24", f.cancelOptions()) // Cancel button + + f.configureSlider(options, "2.26", h9Slider) // Unit speed slider + f.configureSlider(options, "2.27", h9Slider) // Animation speed slider + + // Keyboard settings + // TODO: implement keybindings save/load behaviour + f.onClick(kbd, "3.1", f.setDriver(options)) // Done button + f.onClick(kbd, "3.2", f.setDriver(options)) // Cancel button + f.onClick(kbd, "3.4", func() {}) // Reset to defaults button +} + +// FIXME: exiting is a bit OTT. Perhaps display "save failed"? +func (f *Flow) acceptOptions() func() { + return func() { + if err := f.optionsIntoConfig(); err != nil { + log.Printf("Saving options to config failed: %v", err) + f.exit = err + } else { + f.setDriverNow(main) + } + } +} + +// FIXME: again, exiting is OTT. We're just resetting the state of +// the interface to the values in config. +func (f *Flow) cancelOptions() func() { + return func() { + if err := f.configIntoOptions(); err != nil { + log.Printf("Saving options to config failed: %v", err) + f.exit = err + } else { + f.setDriverNow(main) + } + } +} + +func (f *Flow) configIntoOptions() error { + var err error + + cfg := &f.config.Options + optionsUI := f.drivers[options] + + try(optionsUI.SetValueBool("2.1", cfg.PlayMovies), &err) + try(optionsUI.SetValueBool("2.1", cfg.Animations), &err) + try(optionsUI.SetValueBool("2.3", cfg.PlayMusic), &err) + try(optionsUI.SetValueBool("2.4", cfg.CombatVoices), &err) + try(optionsUI.SetValueBool("2.5", cfg.ShowGrid), &err) + try(optionsUI.SetValueBool("2.6", cfg.ShowPaths), &err) + try(optionsUI.SetValueBool("2.7", cfg.PointSaving), &err) + try(optionsUI.SetValueInt("2.9", cfg.ResolutionIndex()), &err) + try(optionsUI.SetValueInt("2.10", cfg.MusicVolume), &err) + try(optionsUI.SetValueInt("2.11", cfg.SFXVolume), &err) + try(optionsUI.SetValueBool("2.25", cfg.AutoCutLevel), &err) + try(optionsUI.SetValueInt("2.26", cfg.UnitSpeed), &err) + try(optionsUI.SetValueInt("2.27", cfg.AnimSpeed), &err) + + return err +} + +func (f *Flow) optionsIntoConfig() error { + var resIdx int // needs handling manually + var err error + + cfg := &f.config.Options + optionsUI := f.drivers[options] + + try(optionsUI.ValueBool("2.1", &cfg.PlayMovies), &err) + try(optionsUI.ValueBool("2.2", &cfg.Animations), &err) + try(optionsUI.ValueBool("2.3", &cfg.PlayMusic), &err) + try(optionsUI.ValueBool("2.4", &cfg.CombatVoices), &err) + try(optionsUI.ValueBool("2.5", &cfg.ShowGrid), &err) + try(optionsUI.ValueBool("2.6", &cfg.ShowPaths), &err) + try(optionsUI.ValueBool("2.7", &cfg.PointSaving), &err) + try(optionsUI.ValueInt("2.9", &resIdx), &err) + try(optionsUI.ValueInt("2.10", &cfg.MusicVolume), &err) + try(optionsUI.ValueInt("2.11", &cfg.SFXVolume), &err) + try(optionsUI.ValueBool("2.25", &cfg.AutoCutLevel), &err) + try(optionsUI.ValueInt("2.26", &cfg.UnitSpeed), &err) + try(optionsUI.ValueInt("2.27", &cfg.AnimSpeed), &err) + + if err != nil { + return err + } + + cfg.SetResolutionIndex(resIdx) + + if err := f.config.Save(); err != nil { + return err + } + + return nil +} + +func try(result error, into *error) { + if *into == nil { + *into = result + } +} diff --git a/internal/ordoor/interfaces.go b/internal/ordoor/interfaces.go deleted file mode 100644 index 6b93fcd..0000000 --- a/internal/ordoor/interfaces.go +++ /dev/null @@ -1,307 +0,0 @@ -package ordoor - -import ( - "log" - - "code.ur.gs/lupine/ordoor/internal/ui" -) - -func try(result error, into *error) { - if *into == nil { - *into = result - } -} - -// These are UI interfaces covering the game entrypoint - -func (o *Ordoor) mainDriver() (*ui.Driver, error) { - // Start in the "main" menu - main, err := o.buildDriver("main") - if err != nil { - return nil, err - } - - newGame, err := o.newGameDriver(main) - if err != nil { - return nil, err - } - - loadGame, err := o.loadGameDriver(main) - if err != nil { - return nil, err - } - - options, err := o.optionsDriver(main) - if err != nil { - return nil, err - } - - // TODO: clicking these buttons should load other interfaces - try(main.OnClick("2.1", func() { o.driver = newGame }), &err) // New game - try(main.OnClick("2.2", func() { o.driver = loadGame }), &err) // Load game - try(main.SetFreeze("2.3", true), &err) // Multiplayer - disable for now - try(main.OnClick("2.4", func() { o.driver = options }), &err) // Options - try(main.OnClick("2.5", func() { o.nextState = StateExit }), &err) // Quit - - return main, err -} - -func (o *Ordoor) newGameDriver(main *ui.Driver) (*ui.Driver, error) { - newGame, err := o.buildDriver("newgame") - if err != nil { - return nil, err - } - - levelPly, err := o.levelPlyDriver(newGame) - if err != nil { - return nil, err - } - - singles, err := o.singlesDriver(newGame) - if err != nil { - return nil, err - } - - randomMap, err := o.randomMapDriver(newGame) - if err != nil { - return nil, err - } - - try(newGame.OnClick("2.1", func() { o.driver = levelPly }), &err) // New campaign button - try(newGame.OnClick("2.2", func() { o.driver = singles }), &err) // Single scenario button - try(newGame.OnClick("2.3", func() { o.driver = randomMap }), &err) // Random scenario button - try(newGame.OnClick("2.4", func() { o.driver = main }), &err) // Back button - - if err != nil { - return nil, err - } - - return newGame, nil -} - -func (o *Ordoor) loadGameDriver(main *ui.Driver) (*ui.Driver, error) { - loadGame, err := o.buildDriver("loadgame") - if err != nil { - return nil, err - } - - try(loadGame.OnClick("3.3", func() { o.driver = main }), &err) // Cancel button - - if err != nil { - return nil, err - } - - return loadGame, nil -} - -// Options needs to know how to go back to main -func (o *Ordoor) optionsDriver(main *ui.Driver) (*ui.Driver, error) { - options, err := o.buildDriver("options") - if err != nil { - return nil, err - } - - kbd, err := o.keyboardDriver(options) - if err != nil { - return nil, err - } - - if err := o.configIntoOptions(options); err != nil { - return nil, err - } - - h3Slider := map[int]int{1: 8, 2: 56, 3: 110, 4: 120} - v10Slider := map[int]int{ - 0: 0, - 10: 9, 20: 18, 30: 27, 40: 36, 50: 45, - 60: 54, 70: 63, 80: 72, 90: 81, 100: 90, - } - h9Slider := map[int]int{ - 0: 0, - 10: 10, 20: 20, 30: 30, 40: 40, - 50: 50, 60: 60, 70: 70, 80: 80, - } - - try(options.OnClick("2.8", func() { o.driver = kbd }), &err) // Keyboard settings button - - try(options.ConfigureSlider("2.9", h3Slider), &err) // Resolution slider - try(options.ConfigureSlider("2.10", v10Slider), &err) // Music volume slider - try(options.ConfigureSlider("2.11", v10Slider), &err) // SFX volume slider - try(options.OnClick("2.12", acceptOptionsFn(o, main, options)), &err) - // 13...23 are "hypertext" - try(options.OnClick("2.24", cancelOptionsFn(o, main, options)), &err) - try(options.ConfigureSlider("2.26", h9Slider), &err) // Unit speed slider - try(options.ConfigureSlider("2.27", h9Slider), &err) // Animation speed slider - // Sample of unit speed animation is 2,28 - // Sample of effect speed animation is 2,29 - - // 30...35 are "hypertext" - - return options, err -} - -// "Level of play menu when starting a new campaign -func (o *Ordoor) levelPlyDriver(newGame *ui.Driver) (*ui.Driver, error) { - levelPly, err := o.buildDriver("levelply") - if err != nil { - return nil, err - } - - try(levelPly.OnClick("2.5", func() { o.driver = newGame }), &err) // Back button - - if err != nil { - return nil, err - } - - return levelPly, nil -} - -func (o *Ordoor) singlesDriver(newGame *ui.Driver) (*ui.Driver, error) { - singles, err := o.buildDriver("singles") - if err != nil { - return nil, err - } - - try(singles.OnClick("4.11", func() { o.driver = newGame }), &err) // Back button - - if err != nil { - return nil, err - } - - return singles, nil -} - -func (o *Ordoor) randomMapDriver(newGame *ui.Driver) (*ui.Driver, error) { - randomMap, err := o.buildDriver("randommap") - if err != nil { - return nil, err - } - - try(randomMap.OnClick("2.19", func() { o.driver = newGame }), &err) // Back button - - if err != nil { - return nil, err - } - - return randomMap, nil -} - -func (o *Ordoor) keyboardDriver(options *ui.Driver) (*ui.Driver, error) { - kbd, err := o.buildDriver("keyboard") - if err != nil { - return nil, err - } - - // TODO: implement keybindings save/load behaviour - try(kbd.OnClick("3.1", func() { o.driver = options }), &err) // Done button - try(kbd.OnClick("3.2", func() { o.driver = options }), &err) // Cancel button - try(kbd.OnClick("3.4", func() {}), &err) // Reset to defaults button - - if err != nil { - return nil, err - } - - return kbd, nil -} - -// FIXME: exiting is a bit OTT. Perhaps display "save failed"? -func acceptOptionsFn(o *Ordoor, main, options *ui.Driver) func() { - return func() { - if err := o.optionsIntoConfig(options); err != nil { - log.Printf("Saving options to config failed: %v", err) - o.nextState = StateExit - } else { - o.driver = main - } - } -} - -// FIXME: again, exiting is OTT. We're just resetting the state of -// the interface to the values in config. -func cancelOptionsFn(o *Ordoor, main, options *ui.Driver) func() { - return func() { - if err := o.configIntoOptions(options); err != nil { - log.Printf("Saving options to config failed: %v", err) - o.nextState = StateExit - } else { - o.driver = main - } - } -} - -func (o *Ordoor) configIntoOptions(options *ui.Driver) error { - cfg := &o.config.Options - var err error - - try(options.SetValueBool("2.1", cfg.PlayMovies), &err) - try(options.SetValueBool("2.1", cfg.Animations), &err) - try(options.SetValueBool("2.3", cfg.PlayMusic), &err) - try(options.SetValueBool("2.4", cfg.CombatVoices), &err) - try(options.SetValueBool("2.5", cfg.ShowGrid), &err) - try(options.SetValueBool("2.6", cfg.ShowPaths), &err) - try(options.SetValueBool("2.7", cfg.PointSaving), &err) - try(options.SetValueInt("2.9", cfg.ResolutionIndex()), &err) - try(options.SetValueInt("2.10", cfg.MusicVolume), &err) - try(options.SetValueInt("2.11", cfg.SFXVolume), &err) - try(options.SetValueBool("2.25", cfg.AutoCutLevel), &err) - try(options.SetValueInt("2.26", cfg.UnitSpeed), &err) - try(options.SetValueInt("2.27", cfg.AnimSpeed), &err) - - return err -} - -func (o *Ordoor) optionsIntoConfig(options *ui.Driver) error { - cfg := &o.config.Options - var resIdx int // needs handling manually - var err error - - try(options.ValueBool("2.1", &cfg.PlayMovies), &err) - try(options.ValueBool("2.2", &cfg.Animations), &err) - try(options.ValueBool("2.3", &cfg.PlayMusic), &err) - try(options.ValueBool("2.4", &cfg.CombatVoices), &err) - try(options.ValueBool("2.5", &cfg.ShowGrid), &err) - try(options.ValueBool("2.6", &cfg.ShowPaths), &err) - try(options.ValueBool("2.7", &cfg.PointSaving), &err) - try(options.ValueInt("2.9", &resIdx), &err) - try(options.ValueInt("2.10", &cfg.MusicVolume), &err) - try(options.ValueInt("2.11", &cfg.SFXVolume), &err) - try(options.ValueBool("2.25", &cfg.AutoCutLevel), &err) - try(options.ValueInt("2.26", &cfg.UnitSpeed), &err) - try(options.ValueInt("2.27", &cfg.AnimSpeed), &err) - - if err != nil { - return err - } - - cfg.SetResolutionIndex(resIdx) - - if err := o.config.Save(); err != nil { - return err - } - - // TODO: emit events, rather than just modifying state here - if o.music != nil && o.music.IsPlaying() != cfg.PlayMusic { - if cfg.PlayMusic { - o.music.Rewind() - o.music.Play() - } else { - o.music.Pause() - } - } - - return nil -} - -func (o *Ordoor) buildDriver(name string) (*ui.Driver, error) { - menu, err := o.assets.Menu(name) - if err != nil { - return nil, err - } - - driver, err := ui.NewDriver(menu) - if err != nil { - return nil, err - } - - return driver, nil -} diff --git a/internal/ordoor/ordoor.go b/internal/ordoor/ordoor.go index d9e4faf..0d4b6cb 100644 --- a/internal/ordoor/ordoor.go +++ b/internal/ordoor/ordoor.go @@ -14,13 +14,13 @@ import ( "code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/config" + "code.ur.gs/lupine/ordoor/internal/ordoor/flow" "code.ur.gs/lupine/ordoor/internal/ui" ) type gameState int const ( - StateInitial gameState = 0 StateInterface gameState = 1 StateExit gameState = 666 ) @@ -39,7 +39,7 @@ type Ordoor struct { nextState gameState // Relevant to interface state - driver *ui.Driver + flow *flow.Flow } func Run(configFile string, overrideX, overrideY int) error { @@ -60,7 +60,7 @@ func Run(configFile string, overrideX, overrideY int) error { ordoor := &Ordoor{ assets: assets, config: cfg, - state: StateInitial, + state: StateInterface, nextState: StateInterface, } @@ -79,6 +79,10 @@ func Run(configFile string, overrideX, overrideY int) error { ordoor.win = win + if err := ordoor.setupFlow(); err != nil { + return fmt.Errorf("failed to setup UI flow: %v", err) + } + if err := ordoor.Run(); err != nil { return fmt.Errorf("Run returned %v", err) } @@ -93,7 +97,7 @@ func (o *Ordoor) Run() error { } err := o.win.Run() - if err == errExit { + if err == flow.ErrExit { log.Printf("Exit requested") return nil } @@ -128,14 +132,15 @@ func (o *Ordoor) PlayMusic(name string) error { return nil } -func (o *Ordoor) setupInterface() error { +func (o *Ordoor) setupFlow() error { o.PlayMusic("music_interface") - main, err := o.mainDriver() + + flow, err := flow.New(o.assets, o.config) if err != nil { return err } - o.driver = main + o.flow = flow return nil } @@ -145,10 +150,6 @@ func (o *Ordoor) Update(screenX, screenY int) error { if o.state != o.nextState { log.Printf("State transition: %v -> %v", o.state, o.nextState) switch o.nextState { - case StateInterface: // Setup, move state to interface - if err := o.setupInterface(); err != nil { - return err - } case StateExit: { return errExit @@ -161,9 +162,19 @@ func (o *Ordoor) Update(screenX, screenY int) error { // State transition is finished, hooray o.state = o.nextState + // Ensure music is doing the right thing + if o.music != nil && o.music.IsPlaying() != o.config.Options.PlayMusic { + if o.config.Options.PlayMusic { + o.music.Rewind() + o.music.Play() + } else { + o.music.Pause() + } + } + switch o.state { case StateInterface: - return o.driver.Update(screenX, screenY) + return o.flow.Update(screenX, screenY) default: return fmt.Errorf("Unknown state: %v", o.state) } @@ -172,7 +183,7 @@ func (o *Ordoor) Update(screenX, screenY int) error { func (o *Ordoor) Draw(screen *ebiten.Image) error { switch o.state { case StateInterface: - return o.driver.Draw(screen) + return o.flow.Draw(screen) default: return fmt.Errorf("Unknown state: %v", o.state) } diff --git a/internal/ui/window.go b/internal/ui/window.go index cb6bd8b..1ac2d6c 100644 --- a/internal/ui/window.go +++ b/internal/ui/window.go @@ -5,6 +5,7 @@ import ( "fmt" "log" "os" + "runtime/debug" "runtime/pprof" "github.com/hajimehoshi/ebiten" @@ -67,7 +68,18 @@ func (w *Window) Layout(_, _ int) (int, int) { return w.xRes, w.yRes } -func (w *Window) Update(screen *ebiten.Image) error { +func (w *Window) Update(screen *ebiten.Image) (outErr error) { + // Ebiten does not like it if we panic inside its main loop + defer func() { + if panicErr := recover(); panicErr != nil { + if w.debug { + debug.PrintStack() + } + + outErr = fmt.Errorf("Panic: %v", panicErr) + } + }() + if err := w.game.Update(screen.Size()); err != nil { return err }