From cfa56a0e12c220502e2306934bfe1e571e2ba938 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Sun, 22 Mar 2020 19:12:44 +0000 Subject: [PATCH] Implement the main menu for the ordoor binary In this commit, we also remove code that doesn't properly belong in view-menu --- cmd/view-menu/main.go | 67 +--------------- internal/ordoor/interfaces.go | 52 ++++++++++++ internal/ordoor/ordoor.go | 145 ++++++++++++++++++++++++++++++++-- internal/ordoor/videos.go | 8 +- internal/ui/interface.go | 17 +++- internal/ui/widget.go | 14 ++++ 6 files changed, 225 insertions(+), 78 deletions(-) create mode 100644 internal/ordoor/interfaces.go diff --git a/cmd/view-menu/main.go b/cmd/view-menu/main.go index f459efa..588c315 100644 --- a/cmd/view-menu/main.go +++ b/cmd/view-menu/main.go @@ -7,8 +7,6 @@ import ( "code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/ui" - "github.com/hajimehoshi/ebiten" - "github.com/hajimehoshi/ebiten/audio" ) var ( @@ -16,19 +14,6 @@ var ( menuName = flag.String("menu", "", "Name of a menu, e.g. Main") ) -type env struct { - ui *ui.Interface - - // fonts []*assetstore.Font - // fontObjs []*assetstore.Object - - step int - state state - lastState state -} - -type state struct{} - func main() { flag.Parse() @@ -47,51 +32,12 @@ func main() { log.Fatalf("Couldn't load menu %s: %v", *menuName, err) } - // loadedFonts, err := loadFonts(menu.FontNames...) - // if err != nil { - // log.Fatalf("Failed to load font: %v", err) - // } - iface, err := ui.NewInterface(menu) if err != nil { log.Fatalf("Couldn't initialize interface: %v", err) } - if menu.Name == "main" { - log.Printf("Installing a click handler!") - widget, err := iface.Widget(2, 5) // Menu 2, submenu 5 - if err != nil { - log.Fatalf("Couldn't find widget 2,5: %v", err) - } - widget.OnMouseClick = func() { - os.Exit(0) - } - } - - // Yay sound - if _, err := audio.NewContext(48000); err != nil { - log.Fatalf("Failed to audio: %v", err) - } - music, err := assets.Sound("music_interface") // FIXME: should be a reference to Sounds.dat - if err != nil { - log.Fatalf("Failed to find interface music: %v", err) - } - player, err := music.InfinitePlayer() - if err != nil { - log.Fatalf("Failed to generate music player for interface: %v", err) - } - player.Play() - - state := state{} - env := &env{ - ui: iface, - //objects: menuObjs, - // fonts: loadedFonts, - state: state, - lastState: state, - } - - win, err := ui.NewWindow(env, "View Menu: "+*menuName) + win, err := ui.NewWindow(iface, "View Menu: "+*menuName) if err != nil { log.Fatal("Couldn't create window: %v", err) } @@ -100,14 +46,3 @@ func main() { log.Fatal(err) } } - -func (e *env) Update(screenX, screenY int) error { - e.step += 1 - e.lastState = e.state - - return e.ui.Update(screenX, screenY) -} - -func (e *env) Draw(screen *ebiten.Image) error { - return e.ui.Draw(screen) -} diff --git a/internal/ordoor/interfaces.go b/internal/ordoor/interfaces.go new file mode 100644 index 0000000..ec6831a --- /dev/null +++ b/internal/ordoor/interfaces.go @@ -0,0 +1,52 @@ +package ordoor + +import ( + "fmt" + + "code.ur.gs/lupine/ordoor/internal/ui" +) + +// These are UI interfaces covering the game entrypoint + +func (o *Ordoor) ifaceMain() (*ui.Interface, error) { + // TODO: Start in the "main" menu + menu, err := o.assets.Menu("Main") + if err != nil { + return nil, err + } + + iface, err := ui.NewInterface(menu) + if err != nil { + return nil, err + } + + // TODO: clicking these buttons should load other interfaces + wireupClick(iface, func() {}, 2, 1) // New game + wireupClick(iface, func() {}, 2, 2) // Load game + disableWidget(iface, 2, 3) // Multiplayer. Disable for now. + wireupClick(iface, func() {}, 2, 4) // Options + wireupClick(iface, func() { o.nextState = StateExit }, 2, 5) // Quit + + return iface, nil +} + +func findWidgetOrPanic(iface *ui.Interface, spec ...int) *ui.Widget { + widget, err := iface.Widget(spec...) + if err != nil { + panic(fmt.Sprintf("Couldn't find widget %v:%+v", iface.Name, spec)) + } + + return widget +} + +func wireupClick(iface *ui.Interface, f func(), spec ...int) { + findWidgetOrPanic(iface, spec...).OnMouseClick = f +} + +func disableWidget(iface *ui.Interface, spec ...int) { + findWidgetOrPanic(iface, spec...).Disable() +} + +func (o *Ordoor) ifaceOptions() (*ui.Interface, error) { + return nil, nil +} diff --git a/internal/ordoor/ordoor.go b/internal/ordoor/ordoor.go index 0031d30..b9114cf 100644 --- a/internal/ordoor/ordoor.go +++ b/internal/ordoor/ordoor.go @@ -5,13 +5,41 @@ package ordoor import ( + "errors" "fmt" + "log" + "github.com/hajimehoshi/ebiten" + "github.com/hajimehoshi/ebiten/audio" + + "code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/config" + "code.ur.gs/lupine/ordoor/internal/ui" +) + +type gameState int + +const ( + StateInitial gameState = 0 + StateInterface gameState = 1 + StateExit gameState = 666 +) + +var ( + errExit = errors.New("User-requested exit action") ) type Ordoor struct { - Config *config.Config + assets *assetstore.AssetStore + config *config.Config + music *audio.Player + win *ui.Window + + state gameState + nextState gameState + + // Relevant to interface state + iface *ui.Interface } func Run(configFile string) error { @@ -20,14 +48,119 @@ func Run(configFile string) error { return fmt.Errorf("Couldn't load config file: %v", err) } - ordoor := &Ordoor{ - Config: cfg, + assets, err := assetstore.New(cfg.Ordoor.DataDir) + if err != nil { + return fmt.Errorf("Failed to initialize asset store: %v", err) } - ordoor.PlaySkippableVideo("LOGOS") - ordoor.PlaySkippableVideo("movie1") + if _, err := audio.NewContext(48000); err != nil { + return fmt.Errorf("Failed to set up audio context: %v", err) + } - // TODO: load main interface + ordoor := &Ordoor{ + assets: assets, + config: cfg, + state: StateInitial, + nextState: StateInterface, + } + + win, err := ui.NewWindow(ordoor, "Ordoor") + if err != nil { + return fmt.Errorf("Failed to create window: %v", err) + } + + ordoor.win = win + + if err := ordoor.Run(); err != nil { + return fmt.Errorf("Run returned %v", err) + } return nil } + +func (o *Ordoor) Run() error { + // On startup, play these two videos + o.PlaySkippableVideo("LOGOS") + o.PlaySkippableVideo("movie1") + + err := o.win.Run() + if err == errExit { + return nil + } + + return err +} + +// Only one music track can play at a time. This is handled at the toplevel. +// FIXME: should take references from Sounds.dat +func (o *Ordoor) PlayMusic(name string) error { + if o.music != nil { + if err := o.music.Close(); err != nil { + return fmt.Errorf("Failed to close old music: %v", err) + } + } + + sound, err := o.assets.Sound(name) + if err != nil { + return fmt.Errorf("Failed to find sound %v: %v", name, err) + } + + player, err := sound.InfinitePlayer() + if err != nil { + return fmt.Errorf("Failed to generate music player for %v: %v", name, err) + } + o.music = player + player.Play() + + return nil +} + +func (o *Ordoor) setupInterface() error { + o.PlayMusic("music_interface") + initial, err := o.ifaceMain() + if err != nil { + return err + } + + o.iface = initial + + return nil +} + +func (o *Ordoor) Update(screenX, screenY int) error { + // Perform state transitions + 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 + } + default: + return fmt.Errorf("Unknown state transition: %v -> %v", o.state, o.nextState) + } + } + + // State transition is finished, hooray + o.state = o.nextState + + switch o.state { + case StateInterface: + return o.iface.Update(screenX, screenY) + default: + return fmt.Errorf("Unknown state: %v", o.state) + } +} + +func (o *Ordoor) Draw(screen *ebiten.Image) error { + switch o.state { + case StateInterface: + return o.iface.Draw(screen) + default: + return fmt.Errorf("Unknown state: %v", o.state) + } +} diff --git a/internal/ordoor/videos.go b/internal/ordoor/videos.go index 742a4fe..4b12329 100644 --- a/internal/ordoor/videos.go +++ b/internal/ordoor/videos.go @@ -6,15 +6,15 @@ import ( ) func (o *Ordoor) PlayVideo(name string, skippable bool) { - filename := o.Config.DataFile("SMK/" + name + ".smk") + filename := o.config.DataFile("SMK/" + name + ".smk") - if len(o.Config.VideoPlayer) == 0 { + if len(o.config.VideoPlayer) == 0 { log.Printf("Video player not configured, skipping video %v", filename) return } - argc := o.Config.VideoPlayer[0] - argv := append(o.Config.VideoPlayer[1:]) + argc := o.config.VideoPlayer[0] + argv := append(o.config.VideoPlayer[1:]) if skippable { argv = append(argv, "--input-conf=skippable.mpv.conf") } diff --git a/internal/ui/interface.go b/internal/ui/interface.go index ddb692a..229c4a9 100644 --- a/internal/ui/interface.go +++ b/internal/ui/interface.go @@ -20,6 +20,7 @@ import ( // mind. The interface transparently scales them all to the current screen size // to compensate. type Interface struct { + Name string menu *assetstore.Menu static []*assetstore.Sprite // Static elements in the interface ticks int @@ -28,6 +29,8 @@ type Interface struct { func NewInterface(menu *assetstore.Menu) (*Interface, error) { iface := &Interface{ + Name: menu.Name, + menu: menu, } @@ -59,6 +62,10 @@ func (i *Interface) Update(screenX, screenY int) error { // Iterate through all widgets, update mouse state for _, widget := range i.widgets { + if widget.disabled { + continue // No activity for disabled widgets + } + mouseIsOver := mousePos.In(widget.Bounds) widget.hovering(mouseIsOver) widget.mouseButton(mouseIsOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)) @@ -188,11 +195,17 @@ func (i *Interface) widgetFromRecord(record *menus.Record) (*Widget, error) { } widget.hoverAnimation = hovers - sprite, err := i.menu.Sprite(record.Share + 1) + pressed, err := i.menu.Sprite(record.Share + 1) if err != nil { return nil, err } - widget.mouseButtonDownImage = sprite.Image + widget.mouseButtonDownImage = pressed.Image + + disabled, err := i.menu.Sprite(record.Share + 2) + if err != nil { + return nil, err + } + widget.disabledImage = disabled.Image } return widget, nil diff --git a/internal/ui/widget.go b/internal/ui/widget.go index e0e7f39..0dea1a6 100644 --- a/internal/ui/widget.go +++ b/internal/ui/widget.go @@ -25,6 +25,9 @@ type Widget struct { OnMouseClick func() OnMouseUp func() + disabled bool + disabledImage *ebiten.Image + // These are expected to have the same dimensions as the Bounds hoverAnimation []*ebiten.Image hoverState bool @@ -39,6 +42,13 @@ type Widget struct { sprite *assetstore.Sprite } +func (w *Widget) Disable() { + w.hovering(false) + w.mouseButton(false) + + w.disabled = true +} + func (w *Widget) hovering(value bool) { if w.OnHoverEnter != nil && !w.hoverState && value { w.OnHoverEnter() @@ -72,6 +82,10 @@ func (w *Widget) mouseButton(value bool) { } func (w *Widget) Image(aniStep int) (*ebiten.Image, error) { + if w.disabled { + return w.disabledImage, nil + } + if w.hoverState && w.mouseButtonState { return w.mouseButtonDownImage, nil }