From bfe9fbdf7d4d203bf787ef0eb916de6e94125bd8 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Sun, 22 Mar 2020 02:58:52 +0000 Subject: [PATCH] Start work on menu interactivity. With this commit, we get a ui.Interface and ui.Widget type. The interface monitors hover and mouse click state and tells the widgets about them; the widgets execute code specified by the application when events occur. Next step: have wh40k load the main menu and play sound, etc. --- cmd/view-map/main.go | 6 +- cmd/view-menu/main.go | 158 +++++------------------- cmd/view-minimap/main.go | 6 +- cmd/view-obj/main.go | 6 +- cmd/view-set/main.go | 14 +-- internal/assetstore/assetstore.go | 9 +- internal/assetstore/menu.go | 81 +++++++++++++ internal/menus/menus.go | 23 ---- internal/ui/interface.go | 192 ++++++++++++++++++++++++++++++ internal/ui/widget.go | 84 +++++++++++++ internal/ui/window.go | 53 +++++---- internal/wh40k/wh40k.go | 2 + 12 files changed, 440 insertions(+), 194 deletions(-) create mode 100644 internal/assetstore/menu.go create mode 100644 internal/ui/interface.go create mode 100644 internal/ui/widget.go diff --git a/cmd/view-map/main.go b/cmd/view-map/main.go index 33d2f5e..7b68290 100644 --- a/cmd/view-map/main.go +++ b/cmd/view-map/main.go @@ -69,7 +69,7 @@ func main() { lastState: state, } - win, err := ui.NewWindow("View Map " + *gameMap) + win, err := ui.NewWindow(env, "View Map "+*gameMap) if err != nil { log.Fatal("Couldn't create window: %v", err) } @@ -86,12 +86,12 @@ func main() { win.OnKeyUp(ebiten.Key1+ebiten.Key(i), env.setZIdx(i+1)) } - if err := win.Run(env.Update, env.Draw); err != nil { + if err := win.Run(); err != nil { log.Fatal(err) } } -func (e *env) Update() error { +func (e *env) Update(screenX, screenY int) error { if e.step == 0 || e.lastState != e.state { log.Printf("zoom=%.2f zIdx=%v camPos=%#v", e.state.zoom, e.state.zIdx, e.state.origin) } diff --git a/cmd/view-menu/main.go b/cmd/view-menu/main.go index 33db93c..ccb0850 100644 --- a/cmd/view-menu/main.go +++ b/cmd/view-menu/main.go @@ -2,14 +2,10 @@ package main import ( "flag" - "image" "log" "os" - "path/filepath" "code.ur.gs/lupine/ordoor/internal/assetstore" - "code.ur.gs/lupine/ordoor/internal/data" - "code.ur.gs/lupine/ordoor/internal/menus" "code.ur.gs/lupine/ordoor/internal/ui" "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/audio" @@ -17,12 +13,11 @@ import ( var ( gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation") - menuFile = flag.String("menu", "", "Name of a menu, e.g. Main") + menuName = flag.String("menu", "", "Name of a menu, e.g. Main") ) type env struct { - menu *menus.Menu - objects []*assetstore.Object + ui *ui.Interface // fonts []*assetstore.Font // fontObjs []*assetstore.Object @@ -32,15 +27,12 @@ type env struct { lastState state } -type state struct { - // Redraw the window if these change - winBounds image.Rectangle -} +type state struct{} func main() { flag.Parse() - if *gamePath == "" || *menuFile == "" { + if *gamePath == "" || *menuName == "" { flag.Usage() os.Exit(1) } @@ -50,11 +42,12 @@ func main() { log.Fatal(err) } - menu, err := menus.LoadMenu(*menuFile) + menu, err := assets.Menu(*menuName) if err != nil { - log.Fatalf("Couldn't load menu file %s: %v", *menuFile, err) + log.Fatalf("Couldn't load menu %s: %v", *menuName, err) } + /* TODO: move i18n, fonts into assetstore if i18n, err := data.LoadI18n(filepath.Join(*gamePath, "Data", data.I18nFile)); err != nil { log.Printf("Failed to load i18n data, skipping internationalization: %v", err) } else { @@ -65,15 +58,22 @@ func main() { // if err != nil { // log.Fatalf("Failed to load font: %v", err) // } + */ - var menuObjs []*assetstore.Object - for _, filename := range menu.ObjectFiles { - obj, err := assets.ObjectByPath(filepath.Join(*gamePath, "Menu", filename)) + 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("Failed to load %v: %v", filename, err) + log.Fatalf("Couldn't find widget 2,5: %v", err) + } + widget.OnMouseClick = func() { + os.Exit(0) } - - menuObjs = append(menuObjs, obj) } // Yay sound @@ -92,132 +92,30 @@ func main() { state := state{} env := &env{ - menu: menu, - objects: menuObjs, + ui: iface, + //objects: menuObjs, // fonts: loadedFonts, state: state, lastState: state, } - win, err := ui.NewWindow("View Menu: " + *menuFile) + win, err := ui.NewWindow(env, "View Menu: "+*menuName) if err != nil { log.Fatal("Couldn't create window: %v", err) } - if err := win.Run(env.Update, env.Draw); err != nil { + if err := win.Run(); err != nil { log.Fatal(err) } } -func (e *env) Update() error { - // No behaviour yet - +func (e *env) Update(screenX, screenY int) error { e.step += 1 e.lastState = e.state - return nil -} -const ( - origX = 640.0 - origY = 480.0 -) + return e.ui.Update(screenX, screenY) +} func (e *env) Draw(screen *ebiten.Image) error { - // The menus expect to be drawn to a 640x480 screen. We need to scale and - // project that so it fills the window appropriately. This is a combination - // of translate + zoom - winSize := screen.Bounds().Max - scaleX := float64(winSize.X) / float64(origX) - scaleY := float64(winSize.Y) / float64(origY) - - cam := ebiten.GeoM{} - cam.Scale(scaleX, scaleY) - - for _, record := range e.menu.Records { - if err := e.drawRecordRecursive(record, screen, cam); err != nil { - return err - } - } - - return nil -} - -func (e *env) drawRecordRecursive(record *menus.Record, screen *ebiten.Image, geo ebiten.GeoM) error { - if err := e.drawRecord(record, screen, geo); err != nil { - return err - } - - // Draw all children of this record - for _, child := range record.Children { - if err := e.drawRecordRecursive(child, screen, geo); err != nil { - return err - } - } - - return nil -} - -// If the record has a "share" type, we can work out whether it's -func (e *env) isFocused(record *menus.Record, geo ebiten.GeoM) bool { - if record.Share < 0 { - return false - } - - sprite, err := e.objects[0].Sprite(record.Share) // FIXME: need to handle multiple objects - if err != nil { - return false - } - - invGeo := geo - invGeo.Invert() - - cX, cY := ebiten.CursorPosition() - cursorX, cursorY := invGeo.Apply(float64(cX), float64(cY)) // Undo screen scaling - cursorPoint := image.Pt(int(cursorX), int(cursorY)) - - return cursorPoint.In(sprite.Rect) -} - -func (e *env) drawRecord(record *menus.Record, screen *ebiten.Image, geo ebiten.GeoM) error { - // Draw this record if it's valid to do so. FIXME: lots to learn - - spriteId := record.SelectSprite( - e.step/2, - ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft), - e.isFocused(record, geo), - ) - - if spriteId < 0 { - return nil - } - - // X-CORD and Y-CORD are universally either 0 or -1, so ignore here. - // TODO: maybe 0 overrides in-sprite offset (set below)? - - // FIXME: Need to handle multiple objects - obj := e.objects[0] - sprite, err := obj.Sprite(spriteId) - if err != nil { - return err - } - - // Account for scaling, draw sprite at its specified offset - x, y := geo.Apply(float64(sprite.XOffset), float64(sprite.YOffset)) - - // log.Printf( - // "Drawing id=%v type=%v spriteid=%v x=%v(+%v) y=%v(%+v) desc=%q parent=%p", - // record.Id, record.Type, spriteId, record.X, record.Y, sprite.XOffset, sprite.YOffset, record.Desc, record.Parent, - // ) - - geo.Translate(x, y) - - screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: geo}) - - // FIXME: we probably shouldn't draw everything? - // FIXME: handle multiple fonts - // if len(e.fonts) > 0 && record.Desc != "" { - // e.fonts[0].Output(screen, origOffset, record.Desc) - // } - - return nil + return e.ui.Draw(screen) } diff --git a/cmd/view-minimap/main.go b/cmd/view-minimap/main.go index 9956cb4..2052163 100644 --- a/cmd/view-minimap/main.go +++ b/cmd/view-minimap/main.go @@ -71,7 +71,7 @@ func main() { } env := &env{gameMap: gameMap, set: mapSet, state: state, lastState: state} - win, err := ui.NewWindow("View Map " + *mapFile) + win, err := ui.NewWindow(env, "View Map "+*mapFile) if err != nil { log.Fatal("Couldn't create window: %v", err) } @@ -92,7 +92,7 @@ func main() { win.OnMouseWheel(env.changeZoom) - if err := win.Run(env.Update, env.Draw); err != nil { + if err := win.Run(); err != nil { log.Fatal(err) } } @@ -137,7 +137,7 @@ func (e *env) changeZoom(_, y float64) { e.state.zoom *= math.Pow(1.2, y) } -func (e *env) Update() error { +func (e *env) Update(screenX, screenY int) error { // TODO: show details of clicked-on cell in terminal // Automatically cycle every 500ms when auto-update is on diff --git a/cmd/view-obj/main.go b/cmd/view-obj/main.go index fc9ffb4..670323e 100644 --- a/cmd/view-obj/main.go +++ b/cmd/view-obj/main.go @@ -68,7 +68,7 @@ func main() { lastState: state, } - win, err := ui.NewWindow("View Object: " + *objName) + win, err := ui.NewWindow(env, "View Object: "+*objName) if err != nil { log.Fatal(err) } @@ -82,12 +82,12 @@ func main() { win.OnMouseWheel(env.changeZoom) // The main thread now belongs to ebiten - if err := win.Run(env.Update, env.Draw); err != nil { + if err := win.Run(); err != nil { log.Fatal(err) } } -func (e *env) Update() error { +func (e *env) Update(screenX, screenY int) error { if e.step == 0 || e.lastState != e.state { log.Printf( "new state: sprite=%d/%d zoom=%.2f, origin=%+v", diff --git a/cmd/view-set/main.go b/cmd/view-set/main.go index 668f93b..f824a5d 100644 --- a/cmd/view-set/main.go +++ b/cmd/view-set/main.go @@ -51,11 +51,6 @@ func main() { log.Fatalf("Couldn't load set %s: %v", *setName, err) } - win, err := ui.NewWindow("View Set: " + *setName) - if err != nil { - log.Fatal("Couldn't create window: %v", err) - } - state := state{zoom: 8.0} env := &env{ set: set, @@ -63,6 +58,11 @@ func main() { lastState: state, } + win, err := ui.NewWindow(env, "View Set: "+*setName) + if err != nil { + log.Fatal("Couldn't create window: %v", err) + } + win.OnKeyUp(ebiten.KeyLeft, env.changeObjIdx(-1)) win.OnKeyUp(ebiten.KeyRight, env.changeObjIdx(+1)) @@ -72,12 +72,12 @@ func main() { win.OnMouseWheel(env.changeZoom) // Main thread now belongs to ebiten - if err := win.Run(env.Update, env.Draw); err != nil { + if err := win.Run(); err != nil { log.Fatal(err) } } -func (e *env) Update() error { +func (e *env) Update(screenX, screenY int) error { curObj, err := e.curObject() if err != nil { return err diff --git a/internal/assetstore/assetstore.go b/internal/assetstore/assetstore.go index 0c099b9..ac512e3 100644 --- a/internal/assetstore/assetstore.go +++ b/internal/assetstore/assetstore.go @@ -33,6 +33,7 @@ type AssetStore struct { // These members are used to store things we've already loaded maps map[string]*Map + menus map[string]*Menu objs map[string]*Object sets map[string]*Set sounds map[string]*Sound @@ -81,6 +82,7 @@ func (a *AssetStore) Refresh() error { // Refresh a.entries = newEntryMap a.maps = make(map[string]*Map) + a.menus = make(map[string]*Menu) a.objs = make(map[string]*Object) a.sets = make(map[string]*Set) a.sounds = make(map[string]*Sound) @@ -89,7 +91,12 @@ func (a *AssetStore) Refresh() error { } func (a *AssetStore) lookup(name, ext string, dirs ...string) (string, error) { - filename := canonical(name + "." + ext) + var filename string + if ext != "" { + filename = canonical(name + "." + ext) + } else { + filename = canonical(name) + } for _, dir := range dirs { dir = canonical(dir) diff --git a/internal/assetstore/menu.go b/internal/assetstore/menu.go new file mode 100644 index 0000000..388a5fe --- /dev/null +++ b/internal/assetstore/menu.go @@ -0,0 +1,81 @@ +package assetstore + +import ( + "github.com/hajimehoshi/ebiten" + + "code.ur.gs/lupine/ordoor/internal/menus" +) + +type Menu struct { + assets *AssetStore + obj *Object // TODO: handle multiple objects in the menu + raw *menus.Menu + + Name string +} + +// FIXME: don't expose this +func (m *Menu) Records() []*menus.Record { + return m.raw.Records +} + +func (m *Menu) Images(start, count int) ([]*ebiten.Image, error) { + out := make([]*ebiten.Image, count) + for i := start; i < start+count; i++ { + sprite, err := m.Sprite(i) + if err != nil { + return nil, err + } + + out[i-start] = sprite.Image + } + + return out, nil +} + +func (m *Menu) Sprite(idx int) (*Sprite, error) { + return m.obj.Sprite(idx) +} + +func (a *AssetStore) Menu(name string) (*Menu, error) { + name = canonical(name) + + if menu, ok := a.menus[name]; ok { + return menu, nil + } + + filename, err := a.lookup(name, "mnu", "Menu") + if err != nil { + return nil, err + } + + raw, err := menus.LoadMenu(filename) + if err != nil { + return nil, err + } + + obj, err := a.loadMenuObject(raw) // TODO: multiple objects + if err != nil { + return nil, err + } + + menu := &Menu{ + assets: a, + obj: obj, + raw: raw, + Name: name, + } + + a.menus[name] = menu + return menu, nil +} + +func (a *AssetStore) loadMenuObject(menu *menus.Menu) (*Object, error) { + filename := menu.ObjectFiles[0] + filename, err := a.lookup(filename, "", "Menu") // Extension already present + if err != nil { + return nil, err + } + + return a.ObjectByPath(filename) +} diff --git a/internal/menus/menus.go b/internal/menus/menus.go index 94e9a44..a0f4d02 100644 --- a/internal/menus/menus.go +++ b/internal/menus/menus.go @@ -170,29 +170,6 @@ func (r *Record) Toplevel() *Record { return r } -func (r *Record) SelectSprite(step int, pressed, focused bool) int { - switch r.Type { - case TypeStatic: - return r.SpriteId[0] - case TypeMenu: - return r.SpriteId[0] // Probably -1 - case TypeOverlay: - return r.Share - case TypeMainButton: - // A main button has 4 states: unfocused, focused (animated), mousedown, disabled - if focused && pressed { - return r.Share + 1 - } else if focused { - return r.SpriteId[0] + (step % r.DrawType) - } - - // TODO: disabled - return r.Share - } - - return -1 -} - func setProperty(r *Record, k, v string) { vSplit := strings.Split(v, ",") vInt, _ := strconv.Atoi(v) diff --git a/internal/ui/interface.go b/internal/ui/interface.go new file mode 100644 index 0000000..b56cf57 --- /dev/null +++ b/internal/ui/interface.go @@ -0,0 +1,192 @@ +package ui + +import ( + "fmt" + "image" + "reflect" // For DeepEqual + + "github.com/hajimehoshi/ebiten" + + "code.ur.gs/lupine/ordoor/internal/assetstore" + "code.ur.gs/lupine/ordoor/internal/menus" +) + +// type Interface encapsulates a user interface, providing a means to track UI +// state, draw the interface, and execute code when the widgets are interacted +// with. +// +// The graphics for UI elements were all created with a 640x480 resolution in +// mind. The interface transparently scales them all to the current screen size +// to compensate. +type Interface struct { + menu *assetstore.Menu + static []*assetstore.Sprite // Static elements in the interface + ticks int + widgets []*Widget +} + +func NewInterface(menu *assetstore.Menu) (*Interface, error) { + iface := &Interface{ + menu: menu, + } + + for _, record := range menu.Records() { + if err := iface.addRecord(record); err != nil { + return nil, err + } + } + + return iface, nil +} + +// Find a widget by its hierarchical ID path +func (i *Interface) Widget(path ...int) (*Widget, error) { + for _, widget := range i.widgets { + if reflect.DeepEqual(path, widget.path) { + return widget, nil + } + } + + return nil, fmt.Errorf("Couldn't find widget %#+v", path) +} + +func (i *Interface) Update(screenX, screenY int) error { + // Used in animation effects + i.ticks += 1 + + mousePos := i.getMousePos(screenX, screenY) + + // Iterate through all widgets, update mouse state + for _, widget := range i.widgets { + mouseIsOver := mousePos.In(widget.Bounds) + widget.hovering(mouseIsOver) + widget.mouseButton(mouseIsOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)) + } + + return nil +} + +func (i *Interface) Draw(screen *ebiten.Image) error { + geo := i.scale(screen.Size()) + do := &ebiten.DrawImageOptions{GeoM: geo} + + for _, sprite := range i.static { + do.GeoM.Translate(geo.Apply(float64(sprite.XOffset), float64(sprite.YOffset))) + if err := screen.DrawImage(sprite.Image, do); err != nil { + return err + } + do.GeoM = geo + } + + for _, widget := range i.widgets { + img, err := widget.Image(i.ticks / 2) + if err != nil { + return err + } + + if img == nil { + continue + } + + do.GeoM.Translate(geo.Apply(float64(widget.Bounds.Min.X), float64(widget.Bounds.Min.Y))) + if err := screen.DrawImage(img, do); err != nil { + return err + } + do.GeoM = geo + } + + return nil +} + +func (i *Interface) addRecord(record *menus.Record) error { + switch record.Type { + case menus.TypeStatic: // These are static + if sprite, err := i.menu.Sprite(record.SpriteId[0]); err != nil { + return err + } else { + i.static = append(i.static, sprite) + } + case menus.TypeMenu: // These aren't drawable and can be ignored + case menus.TypeOverlay, menus.TypeMainButton: // Widgets \o/ + if widget, err := i.widgetFromRecord(record); err != nil { + return err + } else { + i.widgets = append(i.widgets, widget) + } + + default: + return fmt.Errorf("ui.interface: encountered unknown menu record: %#+v", record) + } + + // Recursively add all children + for _, record := range record.Children { + if err := i.addRecord(record); err != nil { + return err + } + } + + return nil +} + +// Works out how much we have to scale the current screen by to draw correctly +func (i *Interface) scale(w, h int) ebiten.GeoM { + var geo ebiten.GeoM + geo.Scale(float64(w)/640.0, float64(h)/480.0) + + return geo +} + +func (i *Interface) unscale(w, h int) ebiten.GeoM { + geo := i.scale(w, h) + geo.Invert() + + return geo +} + +// Returns the current position of the mouse in 640x480 coordinates. Needs the +// actual size of the screen to do so. +func (i *Interface) getMousePos(w, h int) image.Point { + cX, cY := ebiten.CursorPosition() + geo := i.unscale(w, h) + + sX, sY := geo.Apply(float64(cX), float64(cY)) + + return image.Pt(int(sX), int(sY)) +} + +func (i *Interface) widgetFromRecord(record *menus.Record) (*Widget, error) { + // FIXME: we assume that all widgets have a share sprite, but is that true? + sprite, err := i.menu.Sprite(record.Share) + if err != nil { + 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, + path: path, + record: record, + sprite: sprite, + } + + switch record.Type { + case menus.TypeMainButton: + hovers, err := i.menu.Images(record.SpriteId[0], record.DrawType) + if err != nil { + return nil, err + } + widget.hoverAnimation = hovers + + sprite, err := i.menu.Sprite(record.Share + 1) + if err != nil { + return nil, err + } + widget.mouseButtonDownImage = sprite.Image + } + + return widget, nil +} diff --git a/internal/ui/widget.go b/internal/ui/widget.go new file mode 100644 index 0000000..397689d --- /dev/null +++ b/internal/ui/widget.go @@ -0,0 +1,84 @@ +package ui + +import ( + "image" + + "github.com/hajimehoshi/ebiten" + + "code.ur.gs/lupine/ordoor/internal/assetstore" + "code.ur.gs/lupine/ordoor/internal/menus" +) + +// Widget represents an interactive area of the screen. Backgrounds and other +// non-interactive areas are not widgets. +type Widget struct { + // Position on the screen in original (i.e., unscaled) coordinates + Bounds image.Rectangle + // Tooltip string // TODO: show the tooltip when hovering? + + OnHoverEnter func() + OnHoverLeave func() + + // Mouse up can happen without a click taking place if, for instance, the + // mouse cursor leaves the bounds while still pressed. + OnMouseDown func() + OnMouseClick func() + OnMouseUp func() + + // These are expected to have the same dimensions as the Bounds + hoverAnimation []*ebiten.Image + hoverState bool + + // FIXME: We assume right mouse button isn't needed here + // TODO: down, up, and click hooks. + mouseButtonDownImage *ebiten.Image + mouseButtonState bool + + path []int + record *menus.Record + sprite *assetstore.Sprite +} + +func (w *Widget) hovering(value bool) { + if w.OnHoverEnter != nil && !w.hoverState && value { + w.OnHoverEnter() + } + + if w.OnHoverLeave != nil && w.hoverState && !value { + w.OnHoverLeave() + } + + w.hoverState = value + + return +} + +func (w *Widget) mouseButton(value bool) { + if w.OnMouseDown != nil && !w.mouseButtonState && value { + w.OnMouseDown() + } + + if w.mouseButtonState && !value { + if w.OnMouseClick != nil && w.hoverState { + w.OnMouseClick() + } + + if w.OnMouseUp != nil { + w.OnMouseUp() + } + } + + w.mouseButtonState = value +} + +func (w *Widget) Image(aniStep int) (*ebiten.Image, error) { + if w.hoverState && w.mouseButtonState { + return w.mouseButtonDownImage, nil + } + + if w.hoverState && len(w.hoverAnimation) > 0 { + return w.hoverAnimation[(aniStep)%len(w.hoverAnimation)], nil + } + + return w.sprite.Image, nil +} diff --git a/internal/ui/window.go b/internal/ui/window.go index 378a221..e0ec5f3 100644 --- a/internal/ui/window.go +++ b/internal/ui/window.go @@ -12,6 +12,11 @@ import ( "github.com/hajimehoshi/ebiten/inpututil" ) +type Game interface { + Update(screenX, screenY int) error + Draw(*ebiten.Image) error +} + var ( screenScale = flag.Float64("screen-scale", 1.0, "Scale the window by this factor") @@ -26,9 +31,8 @@ type Window struct { KeyUpHandlers map[ebiten.Key]func() MouseWheelHandler func(float64, float64) - // User-provided update actions - updateFn func() error - drawFn func(*ebiten.Image) error + // Allow the "game" to be switched out at any time + game Game debug bool firstRun bool @@ -37,7 +41,7 @@ type Window struct { // 0,0 is the *top left* of the window // // ebiten assumes a single window, so only call this once... -func NewWindow(title string) (*Window, error) { +func NewWindow(game Game, title string) (*Window, error) { ebiten.SetRunnableInBackground(true) return &Window{ @@ -45,6 +49,7 @@ func NewWindow(title string) (*Window, error) { KeyUpHandlers: make(map[ebiten.Key]func()), debug: true, firstRun: true, + game: game, }, nil } @@ -57,13 +62,12 @@ func (w *Window) OnMouseWheel(f func(x, y float64)) { w.MouseWheelHandler = f } -func (w *Window) run(screen *ebiten.Image) error { - if w.firstRun { - ebiten.SetScreenScale(*screenScale) - w.firstRun = false - } +func (w *Window) Layout(_, _ int) (int, int) { + return *winX, *winY +} - if err := w.updateFn(); err != nil { +func (w *Window) Update(screen *ebiten.Image) error { + if err := w.game.Update(screen.Size()); err != nil { return err } @@ -83,16 +87,18 @@ func (w *Window) run(screen *ebiten.Image) error { } } - if !ebiten.IsDrawingSkipped() { - if err := w.drawFn(screen); err != nil { - return err - } + if ebiten.IsDrawingSkipped() { + return nil + } - if w.debug { - // Draw FPS, etc, to the screen - msg := fmt.Sprintf("tps=%0.2f fps=%0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS()) - ebitenutil.DebugPrint(screen, msg) - } + if err := w.game.Draw(screen); err != nil { + return err + } + + if w.debug { + // Draw FPS, etc, to the screen + msg := fmt.Sprintf("tps=%0.2f fps=%0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS()) + ebitenutil.DebugPrint(screen, msg) } return nil @@ -101,10 +107,7 @@ func (w *Window) run(screen *ebiten.Image) error { // TODO: a stop or other cancellation mechanism // // Note that this must be called on the main OS thread -func (w *Window) Run(updateFn func() error, drawFn func(*ebiten.Image) error) error { - w.updateFn = updateFn - w.drawFn = drawFn - +func (w *Window) Run() error { if *cpuprofile != "" { f, err := os.Create(*cpuprofile) if err != nil { @@ -117,5 +120,7 @@ func (w *Window) Run(updateFn func() error, drawFn func(*ebiten.Image) error) er defer pprof.StopCPUProfile() } - return ebiten.Run(w.run, *winX, *winY, 1, w.Title) // Native game resolution: 640x480 + ebiten.SetWindowSize(int(float64(*winX)**screenScale), int(float64(*winY)**screenScale)) + ebiten.SetWindowTitle(w.Title) + return ebiten.RunGame(w) // Native game resolution: 640x480 } diff --git a/internal/wh40k/wh40k.go b/internal/wh40k/wh40k.go index 08a75e5..2322af1 100644 --- a/internal/wh40k/wh40k.go +++ b/internal/wh40k/wh40k.go @@ -27,5 +27,7 @@ func Run(configFile string) error { wh40k.PlaySkippableVideo("LOGOS") wh40k.PlaySkippableVideo("movie1") + // TODO: load main interface + return nil }