Break flow out of ordoor

This commit is contained in:
2020-03-25 02:12:17 +00:00
parent 4eb4b6e69f
commit 3d3a55af9d
5 changed files with 338 additions and 321 deletions

View File

@@ -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
}

View File

@@ -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
}
}

View File

@@ -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
}

View File

@@ -14,13 +14,13 @@ import (
"code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config" "code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ordoor/flow"
"code.ur.gs/lupine/ordoor/internal/ui" "code.ur.gs/lupine/ordoor/internal/ui"
) )
type gameState int type gameState int
const ( const (
StateInitial gameState = 0
StateInterface gameState = 1 StateInterface gameState = 1
StateExit gameState = 666 StateExit gameState = 666
) )
@@ -39,7 +39,7 @@ type Ordoor struct {
nextState gameState nextState gameState
// Relevant to interface state // Relevant to interface state
driver *ui.Driver flow *flow.Flow
} }
func Run(configFile string, overrideX, overrideY int) error { func Run(configFile string, overrideX, overrideY int) error {
@@ -60,7 +60,7 @@ func Run(configFile string, overrideX, overrideY int) error {
ordoor := &Ordoor{ ordoor := &Ordoor{
assets: assets, assets: assets,
config: cfg, config: cfg,
state: StateInitial, state: StateInterface,
nextState: StateInterface, nextState: StateInterface,
} }
@@ -79,6 +79,10 @@ func Run(configFile string, overrideX, overrideY int) error {
ordoor.win = win 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 { if err := ordoor.Run(); err != nil {
return fmt.Errorf("Run returned %v", err) return fmt.Errorf("Run returned %v", err)
} }
@@ -93,7 +97,7 @@ func (o *Ordoor) Run() error {
} }
err := o.win.Run() err := o.win.Run()
if err == errExit { if err == flow.ErrExit {
log.Printf("Exit requested") log.Printf("Exit requested")
return nil return nil
} }
@@ -128,14 +132,15 @@ func (o *Ordoor) PlayMusic(name string) error {
return nil return nil
} }
func (o *Ordoor) setupInterface() error { func (o *Ordoor) setupFlow() error {
o.PlayMusic("music_interface") o.PlayMusic("music_interface")
main, err := o.mainDriver()
flow, err := flow.New(o.assets, o.config)
if err != nil { if err != nil {
return err return err
} }
o.driver = main o.flow = flow
return nil return nil
} }
@@ -145,10 +150,6 @@ func (o *Ordoor) Update(screenX, screenY int) error {
if o.state != o.nextState { if o.state != o.nextState {
log.Printf("State transition: %v -> %v", o.state, o.nextState) log.Printf("State transition: %v -> %v", o.state, o.nextState)
switch o.nextState { switch o.nextState {
case StateInterface: // Setup, move state to interface
if err := o.setupInterface(); err != nil {
return err
}
case StateExit: case StateExit:
{ {
return errExit return errExit
@@ -161,9 +162,19 @@ func (o *Ordoor) Update(screenX, screenY int) error {
// State transition is finished, hooray // State transition is finished, hooray
o.state = o.nextState 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 { switch o.state {
case StateInterface: case StateInterface:
return o.driver.Update(screenX, screenY) return o.flow.Update(screenX, screenY)
default: default:
return fmt.Errorf("Unknown state: %v", o.state) 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 { func (o *Ordoor) Draw(screen *ebiten.Image) error {
switch o.state { switch o.state {
case StateInterface: case StateInterface:
return o.driver.Draw(screen) return o.flow.Draw(screen)
default: default:
return fmt.Errorf("Unknown state: %v", o.state) return fmt.Errorf("Unknown state: %v", o.state)
} }

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"log" "log"
"os" "os"
"runtime/debug"
"runtime/pprof" "runtime/pprof"
"github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten"
@@ -67,7 +68,18 @@ func (w *Window) Layout(_, _ int) (int, int) {
return w.xRes, w.yRes 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 { if err := w.game.Update(screen.Size()); err != nil {
return err return err
} }