From bcee07e8f74374fbcc908a4527cfb0945b62c2f9 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Mon, 23 Mar 2020 00:33:29 +0000 Subject: [PATCH] Make animations work in the options screen --- cmd/ordoor/main.go | 8 ++- cmd/view-obj/main.go | 10 +++- internal/menus/menus.go | 10 ++++ internal/ordoor/interfaces.go | 98 ++++++++++++++++++++--------------- internal/ordoor/ordoor.go | 17 ++++-- internal/ui/animation.go | 15 ++++++ internal/ui/interface.go | 15 +++--- internal/ui/noninteractive.go | 20 +++++++ internal/ui/setup_handlers.go | 42 ++++++++++----- internal/ui/static_element.go | 15 ------ internal/ui/widget.go | 8 +-- 11 files changed, 172 insertions(+), 86 deletions(-) create mode 100644 internal/ui/animation.go create mode 100644 internal/ui/noninteractive.go delete mode 100644 internal/ui/static_element.go diff --git a/cmd/ordoor/main.go b/cmd/ordoor/main.go index 30bd176..fff94a0 100644 --- a/cmd/ordoor/main.go +++ b/cmd/ordoor/main.go @@ -1,19 +1,25 @@ package main import ( + "flag" "log" "os" "code.ur.gs/lupine/ordoor/internal/ordoor" ) +var ( + winX = flag.Int("win-x", 0, "Pre-scaled window X dimension override") + winY = flag.Int("win-y", 0, "Pre-scaled window Y dimension override") +) + func main() { configFile := "config.toml" if len(os.Args) == 2 { configFile = os.Args[1] } - if err := ordoor.Run(configFile); err != nil { + if err := ordoor.Run(configFile, *winX, *winY); err != nil { log.Fatalf(err.Error()) } diff --git a/cmd/view-obj/main.go b/cmd/view-obj/main.go index eb5afbc..5768302 100644 --- a/cmd/view-obj/main.go +++ b/cmd/view-obj/main.go @@ -24,6 +24,7 @@ var ( type env struct { obj *assetstore.Object + spr *assetstore.Sprite step int state state @@ -92,10 +93,17 @@ func main() { func (e *env) Update(screenX, screenY int) error { if e.step == 0 || e.lastState != e.state { + sprite, err := e.obj.Sprite(e.state.spriteIdx) + if err != nil { + return err + } + e.spr = sprite + log.Printf( - "new state: sprite=%d/%d zoom=%.2f, origin=%+v", + "new state: sprite=%d/%d bounds=%+#v zoom=%.2f, origin=%+v", e.state.spriteIdx, e.obj.NumSprites, + e.spr.Rect, e.state.zoom, e.state.origin, ) diff --git a/internal/menus/menus.go b/internal/menus/menus.go index 2fb225a..3ae677b 100644 --- a/internal/menus/menus.go +++ b/internal/menus/menus.go @@ -233,3 +233,13 @@ func (m *Menu) Internationalize(replacer Replacer) { record.Internationalize(replacer) } } + +func (r *Record) Path() string { + var path []string + + for rec := r; rec != nil; rec = rec.Parent { + path = append([]string{strconv.Itoa(rec.Id)}, path...) + } + + return strings.Join(path, ".") +} diff --git a/internal/ordoor/interfaces.go b/internal/ordoor/interfaces.go index 5b8932a..e0d33cb 100644 --- a/internal/ordoor/interfaces.go +++ b/internal/ordoor/interfaces.go @@ -28,11 +28,11 @@ func (o *Ordoor) ifaceMain() (*ui.Interface, error) { } // TODO: clicking these buttons should load other interfaces - try(wireupClick(main, func() {}, 2, 1), &err) // New game - try(wireupClick(main, func() {}, 2, 2), &err) // Load game - try(disableWidget(main, 2, 3), &err) // Multiplayer - disable for now - try(wireupClick(main, func() { o.iface = options }, 2, 4), &err) // Options - try(wireupClick(main, func() { o.nextState = StateExit }, 2, 5), &err) // Quit + try(wireupClick(main, func() {}, "2.1"), &err) // New game + try(wireupClick(main, func() {}, "2.2"), &err) // Load game + try(disableWidget(main, "2.3"), &err) // Multiplayer - disable for now + try(wireupClick(main, func() { o.iface = options }, "2.4"), &err) // Options + try(wireupClick(main, func() { o.nextState = StateExit }, "2.5"), &err) // Quit return main, err } @@ -49,10 +49,10 @@ func (o *Ordoor) ifaceOptions(main *ui.Interface) (*ui.Interface, error) { } // TODO: load current options state into UI - try(wireupClick(options, func() {}, 2, 8), &err) // Keyboard settings button - // Resolution slider is 2,9 - // Music volume slider is 2,10 - // Sound FX volume slider is 2,11 + try(wireupClick(options, func() {}, "2.8"), &err) // Keyboard settings button + // Resolution slider is 2.9 + // Music volume slider is 2.10 + // Sound FX volume slider is 2.11 // Accept button try(wireupClick( @@ -65,7 +65,7 @@ func (o *Ordoor) ifaceOptions(main *ui.Interface) (*ui.Interface, error) { o.iface = main } }, - 2, 12, + "2.12", ), &err) // 13...23 are "hypertext" @@ -84,7 +84,7 @@ func (o *Ordoor) ifaceOptions(main *ui.Interface) (*ui.Interface, error) { o.iface = main } }, - 2, 24, + "2.24", ), &err) // Unit speed slider is 2,26 // Looping effect speed slider is 2,27 @@ -100,14 +100,14 @@ func (o *Ordoor) configIntoOptions(options *ui.Interface) error { cfg := &o.config.Options var err error - try(setWidgetValueBool(options, cfg.PlayMovies, 2, 1), &err) - try(setWidgetValueBool(options, cfg.Animations, 2, 2), &err) - try(setWidgetValueBool(options, cfg.PlayMusic, 2, 3), &err) - try(setWidgetValueBool(options, cfg.CombatVoices, 2, 4), &err) - try(setWidgetValueBool(options, cfg.ShowGrid, 2, 5), &err) - try(setWidgetValueBool(options, cfg.ShowPaths, 2, 6), &err) - try(setWidgetValueBool(options, cfg.PointSaving, 2, 7), &err) - try(setWidgetValueBool(options, cfg.AutoCutLevel, 2, 25), &err) + try(setWidgetValueBool(options, cfg.PlayMovies, "2.1"), &err) + try(setWidgetValueBool(options, cfg.Animations, "2.2"), &err) + try(setWidgetValueBool(options, cfg.PlayMusic, "2.3"), &err) + try(setWidgetValueBool(options, cfg.CombatVoices, "2.4"), &err) + try(setWidgetValueBool(options, cfg.ShowGrid, "2.5"), &err) + try(setWidgetValueBool(options, cfg.ShowPaths, "2.6"), &err) + try(setWidgetValueBool(options, cfg.PointSaving, "2.7"), &err) + try(setWidgetValueBool(options, cfg.AutoCutLevel, "2.25"), &err) return err } @@ -116,20 +116,34 @@ func (o *Ordoor) optionsIntoConfig(options *ui.Interface) error { cfg := &o.config.Options var err error - try(getWidgetValueBool(options, &cfg.PlayMovies, 2, 1), &err) - try(getWidgetValueBool(options, &cfg.Animations, 2, 2), &err) - try(getWidgetValueBool(options, &cfg.PlayMusic, 2, 3), &err) - try(getWidgetValueBool(options, &cfg.CombatVoices, 2, 4), &err) - try(getWidgetValueBool(options, &cfg.ShowGrid, 2, 5), &err) - try(getWidgetValueBool(options, &cfg.ShowPaths, 2, 6), &err) - try(getWidgetValueBool(options, &cfg.PointSaving, 2, 7), &err) - try(getWidgetValueBool(options, &cfg.AutoCutLevel, 2, 25), &err) + try(getWidgetValueBool(options, &cfg.PlayMovies, "2.1"), &err) + try(getWidgetValueBool(options, &cfg.Animations, "2.2"), &err) + try(getWidgetValueBool(options, &cfg.PlayMusic, "2.3"), &err) + try(getWidgetValueBool(options, &cfg.CombatVoices, "2.4"), &err) + try(getWidgetValueBool(options, &cfg.ShowGrid, "2.5"), &err) + try(getWidgetValueBool(options, &cfg.ShowPaths, "2.6"), &err) + try(getWidgetValueBool(options, &cfg.PointSaving, "2.7"), &err) + try(getWidgetValueBool(options, &cfg.AutoCutLevel, "2.25"), &err) if err != nil { return err } - return o.config.Save() + 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) buildInterface(name string) (*ui.Interface, error) { @@ -146,8 +160,8 @@ func (o *Ordoor) buildInterface(name string) (*ui.Interface, error) { return iface, nil } -func findWidget(iface *ui.Interface, spec ...int) (*ui.Widget, error) { - widget, err := iface.Widget(spec...) +func findWidget(iface *ui.Interface, spec string) (*ui.Widget, error) { + widget, err := iface.Widget(spec) if err != nil { return nil, fmt.Errorf("Couldn't find widget %v:%+v", iface.Name, spec) } @@ -155,8 +169,8 @@ func findWidget(iface *ui.Interface, spec ...int) (*ui.Widget, error) { return widget, nil } -func getWidgetValue(iface *ui.Interface, spec ...int) (string, error) { - widget, err := findWidget(iface, spec...) +func getWidgetValue(iface *ui.Interface, spec string) (string, error) { + widget, err := findWidget(iface, spec) if err != nil { return "", err } @@ -164,8 +178,8 @@ func getWidgetValue(iface *ui.Interface, spec ...int) (string, error) { return widget.Value, nil } -func setWidgetValue(iface *ui.Interface, value string, spec ...int) error { - widget, err := findWidget(iface, spec...) +func setWidgetValue(iface *ui.Interface, value string, spec string) error { + widget, err := findWidget(iface, spec) if err != nil { return err } @@ -175,8 +189,8 @@ func setWidgetValue(iface *ui.Interface, value string, spec ...int) error { return nil } -func getWidgetValueBool(iface *ui.Interface, into *bool, spec ...int) error { - vStr, err := getWidgetValue(iface, spec...) +func getWidgetValueBool(iface *ui.Interface, into *bool, spec string) error { + vStr, err := getWidgetValue(iface, spec) if err != nil { return err } @@ -185,17 +199,17 @@ func getWidgetValueBool(iface *ui.Interface, into *bool, spec ...int) error { return nil } -func setWidgetValueBool(iface *ui.Interface, value bool, spec ...int) error { +func setWidgetValueBool(iface *ui.Interface, value bool, spec string) error { vStr := "0" if value { vStr = "1" } - return setWidgetValue(iface, vStr, spec...) + return setWidgetValue(iface, vStr, spec) } -func wireupClick(iface *ui.Interface, f func(), spec ...int) error { - widget, err := findWidget(iface, spec...) +func wireupClick(iface *ui.Interface, f func(), spec string) error { + widget, err := findWidget(iface, spec) if err != nil { return err } @@ -209,8 +223,8 @@ func wireupClick(iface *ui.Interface, f func(), spec ...int) error { return nil } -func disableWidget(iface *ui.Interface, spec ...int) error { - widget, err := findWidget(iface, spec...) +func disableWidget(iface *ui.Interface, spec string) error { + widget, err := findWidget(iface, spec) if err != nil { return err } diff --git a/internal/ordoor/ordoor.go b/internal/ordoor/ordoor.go index 5db6913..91831a3 100644 --- a/internal/ordoor/ordoor.go +++ b/internal/ordoor/ordoor.go @@ -42,7 +42,7 @@ type Ordoor struct { iface *ui.Interface } -func Run(configFile string) error { +func Run(configFile string, overrideX, overrideY int) error { cfg, err := config.Load(configFile) if err != nil { return fmt.Errorf("Couldn't load config file: %v", err) @@ -64,7 +64,15 @@ func Run(configFile string) error { nextState: StateInterface, } - win, err := ui.NewWindow(ordoor, "Ordoor", cfg.Options.XRes, cfg.Options.YRes) + x, y := cfg.Options.XRes, cfg.Options.YRes + if overrideX > 0 { + x = overrideX + } + if overrideY > 0 { + y = overrideY + } + + win, err := ui.NewWindow(ordoor, "Ordoor", x, y) if err != nil { return fmt.Errorf("Failed to create window: %v", err) } @@ -112,7 +120,10 @@ func (o *Ordoor) PlayMusic(name string) error { return fmt.Errorf("Failed to generate music player for %v: %v", name, err) } o.music = player - player.Play() + + if o.config.Options.PlayMusic { + player.Play() + } return nil } diff --git a/internal/ui/animation.go b/internal/ui/animation.go new file mode 100644 index 0000000..cdb270e --- /dev/null +++ b/internal/ui/animation.go @@ -0,0 +1,15 @@ +package ui + +import ( + "github.com/hajimehoshi/ebiten" +) + +type animation []*ebiten.Image + +func (a animation) image(step int) *ebiten.Image { + if len(a) == 0 { + return nil + } + + return a[step%len(a)] +} diff --git a/internal/ui/interface.go b/internal/ui/interface.go index 823ed6d..e24b2be 100644 --- a/internal/ui/interface.go +++ b/internal/ui/interface.go @@ -4,7 +4,6 @@ import ( "fmt" "image" "log" - "reflect" // For DeepEqual "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" @@ -23,7 +22,7 @@ import ( type Interface struct { Name string menu *assetstore.Menu - static []*staticElement + static []*noninteractive ticks int widgets []*Widget } @@ -44,14 +43,14 @@ func NewInterface(menu *assetstore.Menu) (*Interface, error) { } // Find a widget by its hierarchical ID path -func (i *Interface) Widget(path ...int) (*Widget, error) { +func (i *Interface) Widget(path string) (*Widget, error) { for _, widget := range i.widgets { - if reflect.DeepEqual(path, widget.path) { + if path == widget.path { return widget, nil } } - return nil, fmt.Errorf("Couldn't find widget %#+v", path) + return nil, fmt.Errorf("Couldn't find widget %v", path) } func (i *Interface) Update(screenX, screenY int) error { @@ -82,9 +81,9 @@ func (i *Interface) Draw(screen *ebiten.Image) error { do := &ebiten.DrawImageOptions{GeoM: geo} for _, s := range i.static { - if s.image != nil { - do.GeoM.Translate(geo.Apply(float64(s.bounds.Min.X), float64(s.bounds.Min.X))) - if err := screen.DrawImage(s.image, do); err != nil { + if image := s.image(i.ticks); image != nil { + do.GeoM.Translate(geo.Apply(float64(s.bounds.Min.X), float64(s.bounds.Min.Y))) + if err := screen.DrawImage(image, do); err != nil { return err } do.GeoM = geo diff --git a/internal/ui/noninteractive.go b/internal/ui/noninteractive.go new file mode 100644 index 0000000..c18648f --- /dev/null +++ b/internal/ui/noninteractive.go @@ -0,0 +1,20 @@ +package ui + +import ( + "github.com/hajimehoshi/ebiten" + "image" +) + +// A non-interactive element is not a widget; it merely displays some pixels and +// may optionally have a tooltip for display within bounds. +// +// For non-animated non-interactive elements, just give them a single frame. +type noninteractive struct { + bounds image.Rectangle + frames animation + tooltip string +} + +func (n *noninteractive) image(step int) *ebiten.Image { + return n.frames.image(step) +} diff --git a/internal/ui/setup_handlers.go b/internal/ui/setup_handlers.go index 9dc778d..aa207ab 100644 --- a/internal/ui/setup_handlers.go +++ b/internal/ui/setup_handlers.go @@ -17,7 +17,7 @@ var setupHandlers = map[menus.MenuType]func(i *Interface, r *menus.Record) error menus.TypeOverlay: nil, // FIXME: What's it for? menus.TypeHypertext: handleHypertext, menus.TypeCheckbox: handleCheckbox, - menus.TypeAnimationSample: nil, // FIXME: handle this + menus.TypeAnimationSample: handleAnimation, menus.TypeMainButton: handleMainButton, menus.TypeSlider: nil, // FIXME: handle this } @@ -35,9 +35,9 @@ func handleStatic(i *Interface, record *menus.Record) error { return err } - static := &staticElement{ + static := &noninteractive{ bounds: sprite.Rect, - image: sprite.Image, + frames: animation{sprite.Image}, tooltip: record.Desc, } @@ -54,9 +54,9 @@ func handleHypertext(i *Interface, record *menus.Record) error { return err } - static := &staticElement{ + static := &noninteractive{ bounds: sprite.Rect, - image: nil, + frames: nil, tooltip: record.Desc, } @@ -106,6 +106,29 @@ func handleCheckbox(i *Interface, record *menus.Record) error { return nil } +// An animation is a non-interactive element that displays something in a loop +func handleAnimation(i *Interface, record *menus.Record) error { + sprite, err := i.menu.Sprite(record.SpriteId[0]) + if err != nil { + return err + } + + frames, err := i.menu.Images(record.SpriteId[0], record.DrawType) + if err != nil { + return err + } + + ani := &noninteractive{ + bounds: sprite.Rect, + frames: animation(frames), + tooltip: record.Desc, + } + + i.static = append(i.static, ani) + + return nil +} + func handleButton(i *Interface, record *menus.Record) error { spriteId := record.SpriteId[0] widget, err := i.widgetFromRecord(record, spriteId) @@ -179,7 +202,7 @@ func handleMainButton(i *Interface, record *menus.Record) error { widget.mouseButtonDownImage = pressed.Image widget.disabledImage = disabled.Image - widget.hoverAnimation = hovers + widget.hoverAnimation = animation(hovers) i.widgets = append(i.widgets, widget) @@ -194,15 +217,10 @@ func (i *Interface) widgetFromRecord(record *menus.Record, spriteId int) (*Widge return nil, err } - var path []int - for r := record; r != nil; r = r.Parent { - path = append([]int{r.Id}, path...) - } - widget := &Widget{ Bounds: sprite.Rect, Tooltip: record.Desc, - path: path, + path: record.Path(), record: record, sprite: sprite, } diff --git a/internal/ui/static_element.go b/internal/ui/static_element.go deleted file mode 100644 index 033e952..0000000 --- a/internal/ui/static_element.go +++ /dev/null @@ -1,15 +0,0 @@ -package ui - -import ( - "image" - - "github.com/hajimehoshi/ebiten" -) - -// A static element is not a widget; it merely displays some pixels and may -// optionally have a tooltip for display within bounds -type staticElement struct { - bounds image.Rectangle - image *ebiten.Image - tooltip string -} diff --git a/internal/ui/widget.go b/internal/ui/widget.go index c7fbbbd..27b4e3e 100644 --- a/internal/ui/widget.go +++ b/internal/ui/widget.go @@ -30,7 +30,7 @@ type Widget struct { disabledImage *ebiten.Image // These are expected to have the same dimensions as the Bounds - hoverAnimation []*ebiten.Image + hoverAnimation animation hoverState bool // FIXME: We assume right mouse button isn't needed here @@ -38,7 +38,7 @@ type Widget struct { mouseButtonDownImage *ebiten.Image mouseButtonState bool - path []int + path string record *menus.Record sprite *assetstore.Sprite @@ -97,8 +97,8 @@ func (w *Widget) Image(aniStep int) (*ebiten.Image, error) { return w.mouseButtonDownImage, nil } - if w.hoverState && len(w.hoverAnimation) > 0 { - return w.hoverAnimation[(aniStep)%len(w.hoverAnimation)], nil + if w.hoverState && w.hoverAnimation != nil { + return w.hoverAnimation.image(aniStep), nil } if w.valueToImage != nil {