package flow import ( "errors" "fmt" "log" "strings" "github.com/hajimehoshi/ebiten" "code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/config" "code.ur.gs/lupine/ordoor/internal/data" "code.ur.gs/lupine/ordoor/internal/scenario" "code.ur.gs/lupine/ordoor/internal/ship" "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 generic *data.Generic // Some screens can be returned to from more than one place. Where this is // the case, instead of hardcoding it, we'll store an entry in here so we // know where we're going back to // // FIXME: this really suggests wiring everything up at the start is wrong. returns map[driverName]driverName // If we're currently playing a scenario, it it placed here scenario *scenario.Scenario ship *ship.Ship exit error } var ( ErrExit = errors.New("exiting gracefully") // 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, ship *ship.Ship) (*Flow, error) { generic, err := assets.Generic() if err != nil { return nil, fmt.Errorf("Failed to read generic data: %v", err) } out := &Flow{ assets: assets, config: config, generic: generic, drivers: make(map[driverName]*ui.Driver, len(driverNames)), returns: make(map[driverName]driverName), ship: ship, } // Load all the drivers upfront for _, name := range driverNames { driver, err := buildDriver(assets, name) if err != nil { return nil, err } out.drivers[name] = driver } out.linkDrivers() out.reset() return out, out.exit } func (f *Flow) SetScenario(scenario *scenario.Scenario) { f.current = f.drivers[mainGame] f.scenario = scenario } func (f *Flow) Update(screenX, screenY int) error { if f.exit != nil { return f.exit } // Keybindings for map control // FIXME: this needs a big rethink if f.current != nil && f.scenario != nil && !f.current.IsInDialogue() { step := 32 if ebiten.IsKeyPressed(ebiten.KeyLeft) { f.scenario.Viewpoint.X -= step } if ebiten.IsKeyPressed(ebiten.KeyRight) { f.scenario.Viewpoint.X += step } if ebiten.IsKeyPressed(ebiten.KeyUp) { f.scenario.Viewpoint.Y -= step } if ebiten.IsKeyPressed(ebiten.KeyDown) { f.scenario.Viewpoint.Y += step } } if f.scenario != nil { if err := f.scenario.Update(screenX, screenY); err != nil { return err } } if f.current != nil { if err := f.current.Update(screenX, screenY); err != nil { return err } } return nil } func (f *Flow) Draw(screen *ebiten.Image) error { if f.exit != nil { return f.exit } if f.scenario != nil { if err := f.scenario.Draw(screen); err != nil { return err } } if f.current != nil { if err := f.current.Draw(screen); err != nil { return err } } return nil } func (f *Flow) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) { if f.current != nil { return f.current.Cursor() } // FIXME: we should get a cursor from current all the time. return nil, nil, nil } func (f *Flow) linkDrivers() { // linkMain f.onClick(main, "2.1", f.setReturningDriver(main, newGame)) // New game f.onClick(main, "2.2", f.setReturningDriver(main, loadGame)) // Load game f.setFreeze(main, "2.3", true) // Multiplayer - disable for now f.onClick(main, "2.4", f.setReturningDriver(main, options)) // Options f.onClick(main, "2.5", f.setExit) // Quit // Now link immediate children. They will link their children, and so on f.linkNewGame() f.linkLoadGame() // TODO: link multiplayer f.linkOptions() } func maybeErr(driver driverName, err error) error { if err != nil { return fmt.Errorf("%v: %v", driver, err) } return nil } 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 = maybeErr(driver, f.drivers[driver].OnClick(id, fn)) } func (f *Flow) setFreeze(driver driverName, id string, value bool) { if f.exit != nil { return } f.exit = maybeErr(driver, f.drivers[driver].SetFreeze(id, value)) } func (f *Flow) setValueBool(driver driverName, id string, value bool) { if f.exit != nil { return } f.exit = f.drivers[driver].SetValueBool(id, value) } func (f *Flow) valueBool(driver driverName, id string) bool { if f.exit != nil { return false } var value bool f.exit = f.drivers[driver].ValueBool(id, &value) return value } func (f *Flow) playNextScenario(from driverName) func() { return func() { log.Printf("Loading scenario: %v", f.ship.NextScenario) // TODO: we *could* load scenario assets in a separate assetstore to // make it easier to chuck them away at the end? scenario, err := scenario.NewScenario(f.assets, f.ship.NextScenario) if err != nil { f.exit = err return } f.setReturningDriverNow(from, mainGame) f.scenario = scenario } } func (f *Flow) setActive(driver driverName, id string, value bool) func() { return func() { if f.exit != nil { return } f.exit = maybeErr(driver, f.setActiveNow(driver, id, value)) } } func (f *Flow) setActiveNow(driver driverName, id string, value bool) error { return f.drivers[driver].SetActive(locator(driver, id), value) } func (f *Flow) toggleActive(driver driverName, id string) func() { return func() { if f.exit != nil { return } f.exit = maybeErr(driver, f.drivers[driver].ToggleActive(locator(driver, id))) } } func (f *Flow) showDialogue(driver driverName, id string) func() { return func() { if f.exit != nil { return } f.exit = maybeErr(driver, f.drivers[driver].ShowDialogue(locator(driver, id))) } } func (f *Flow) hideDialogue(driver driverName) func() { return f.drivers[driver].HideDialogue } func (f *Flow) withScenario(then func()) func() { return func() { if f.scenario != nil { then() } } } func (f *Flow) reset() { if f.exit != nil { return } f.setDriverNow(main) // Back to the main interface // Wipe out any returns that may exist f.returns = make(map[driverName]driverName) // FIXME: these should really happen via data binding. f.resetLevelPlyInventorySelect() f.exit = f.configIntoOptions() } func (f *Flow) setExit() { f.exit = ErrExit } // TODO: convert all to locators func locator(driver driverName, id string) string { return fmt.Sprintf("%v:%v", strings.ToLower(string(driver)), id) }