Implement the main menu for the ordoor binary
In this commit, we also remove code that doesn't properly belong in view-menu
This commit is contained in:
@@ -7,8 +7,6 @@ import (
|
|||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
"github.com/hajimehoshi/ebiten"
|
|
||||||
"github.com/hajimehoshi/ebiten/audio"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -16,19 +14,6 @@ var (
|
|||||||
menuName = flag.String("menu", "", "Name of a menu, e.g. Main")
|
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() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -47,51 +32,12 @@ func main() {
|
|||||||
log.Fatalf("Couldn't load menu %s: %v", *menuName, err)
|
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)
|
iface, err := ui.NewInterface(menu)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Couldn't initialize interface: %v", err)
|
log.Fatalf("Couldn't initialize interface: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if menu.Name == "main" {
|
win, err := ui.NewWindow(iface, "View Menu: "+*menuName)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Couldn't create window: %v", err)
|
log.Fatal("Couldn't create window: %v", err)
|
||||||
}
|
}
|
||||||
@@ -100,14 +46,3 @@ func main() {
|
|||||||
log.Fatal(err)
|
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)
|
|
||||||
}
|
|
||||||
|
52
internal/ordoor/interfaces.go
Normal file
52
internal/ordoor/interfaces.go
Normal file
@@ -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
|
||||||
|
}
|
@@ -5,13 +5,41 @@
|
|||||||
package ordoor
|
package ordoor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"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/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 {
|
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 {
|
func Run(configFile string) error {
|
||||||
@@ -20,14 +48,119 @@ func Run(configFile string) error {
|
|||||||
return fmt.Errorf("Couldn't load config file: %v", err)
|
return fmt.Errorf("Couldn't load config file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ordoor := &Ordoor{
|
assets, err := assetstore.New(cfg.Ordoor.DataDir)
|
||||||
Config: cfg,
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to initialize asset store: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ordoor.PlaySkippableVideo("LOGOS")
|
if _, err := audio.NewContext(48000); err != nil {
|
||||||
ordoor.PlaySkippableVideo("movie1")
|
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
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -6,15 +6,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (o *Ordoor) PlayVideo(name string, skippable bool) {
|
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)
|
log.Printf("Video player not configured, skipping video %v", filename)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
argc := o.Config.VideoPlayer[0]
|
argc := o.config.VideoPlayer[0]
|
||||||
argv := append(o.Config.VideoPlayer[1:])
|
argv := append(o.config.VideoPlayer[1:])
|
||||||
if skippable {
|
if skippable {
|
||||||
argv = append(argv, "--input-conf=skippable.mpv.conf")
|
argv = append(argv, "--input-conf=skippable.mpv.conf")
|
||||||
}
|
}
|
||||||
|
@@ -20,6 +20,7 @@ import (
|
|||||||
// mind. The interface transparently scales them all to the current screen size
|
// mind. The interface transparently scales them all to the current screen size
|
||||||
// to compensate.
|
// to compensate.
|
||||||
type Interface struct {
|
type Interface struct {
|
||||||
|
Name string
|
||||||
menu *assetstore.Menu
|
menu *assetstore.Menu
|
||||||
static []*assetstore.Sprite // Static elements in the interface
|
static []*assetstore.Sprite // Static elements in the interface
|
||||||
ticks int
|
ticks int
|
||||||
@@ -28,6 +29,8 @@ type Interface struct {
|
|||||||
|
|
||||||
func NewInterface(menu *assetstore.Menu) (*Interface, error) {
|
func NewInterface(menu *assetstore.Menu) (*Interface, error) {
|
||||||
iface := &Interface{
|
iface := &Interface{
|
||||||
|
Name: menu.Name,
|
||||||
|
|
||||||
menu: menu,
|
menu: menu,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +62,10 @@ func (i *Interface) Update(screenX, screenY int) error {
|
|||||||
|
|
||||||
// Iterate through all widgets, update mouse state
|
// Iterate through all widgets, update mouse state
|
||||||
for _, widget := range i.widgets {
|
for _, widget := range i.widgets {
|
||||||
|
if widget.disabled {
|
||||||
|
continue // No activity for disabled widgets
|
||||||
|
}
|
||||||
|
|
||||||
mouseIsOver := mousePos.In(widget.Bounds)
|
mouseIsOver := mousePos.In(widget.Bounds)
|
||||||
widget.hovering(mouseIsOver)
|
widget.hovering(mouseIsOver)
|
||||||
widget.mouseButton(mouseIsOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft))
|
widget.mouseButton(mouseIsOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft))
|
||||||
@@ -188,11 +195,17 @@ func (i *Interface) widgetFromRecord(record *menus.Record) (*Widget, error) {
|
|||||||
}
|
}
|
||||||
widget.hoverAnimation = hovers
|
widget.hoverAnimation = hovers
|
||||||
|
|
||||||
sprite, err := i.menu.Sprite(record.Share + 1)
|
pressed, err := i.menu.Sprite(record.Share + 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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
|
return widget, nil
|
||||||
|
@@ -25,6 +25,9 @@ type Widget struct {
|
|||||||
OnMouseClick func()
|
OnMouseClick func()
|
||||||
OnMouseUp func()
|
OnMouseUp func()
|
||||||
|
|
||||||
|
disabled bool
|
||||||
|
disabledImage *ebiten.Image
|
||||||
|
|
||||||
// These are expected to have the same dimensions as the Bounds
|
// These are expected to have the same dimensions as the Bounds
|
||||||
hoverAnimation []*ebiten.Image
|
hoverAnimation []*ebiten.Image
|
||||||
hoverState bool
|
hoverState bool
|
||||||
@@ -39,6 +42,13 @@ type Widget struct {
|
|||||||
sprite *assetstore.Sprite
|
sprite *assetstore.Sprite
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *Widget) Disable() {
|
||||||
|
w.hovering(false)
|
||||||
|
w.mouseButton(false)
|
||||||
|
|
||||||
|
w.disabled = true
|
||||||
|
}
|
||||||
|
|
||||||
func (w *Widget) hovering(value bool) {
|
func (w *Widget) hovering(value bool) {
|
||||||
if w.OnHoverEnter != nil && !w.hoverState && value {
|
if w.OnHoverEnter != nil && !w.hoverState && value {
|
||||||
w.OnHoverEnter()
|
w.OnHoverEnter()
|
||||||
@@ -72,6 +82,10 @@ func (w *Widget) mouseButton(value bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *Widget) Image(aniStep int) (*ebiten.Image, error) {
|
func (w *Widget) Image(aniStep int) (*ebiten.Image, error) {
|
||||||
|
if w.disabled {
|
||||||
|
return w.disabledImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
if w.hoverState && w.mouseButtonState {
|
if w.hoverState && w.mouseButtonState {
|
||||||
return w.mouseButtonDownImage, nil
|
return w.mouseButtonDownImage, nil
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user