diff --git a/cmd/view-menu/main.go b/cmd/view-menu/main.go index e6c68f3..6861965 100644 --- a/cmd/view-menu/main.go +++ b/cmd/view-menu/main.go @@ -45,6 +45,13 @@ func main() { log.Fatal("Couldn't create window: %v", err) } + // TODO: move this into driver. It will need to be able to change cursor. + cursor, err := assets.Cursor(assetstore.UltPointer) + if err != nil { + log.Fatalf("Couldn't load cursor: %v", err) + } + win.SetCursor(cursor.Image) + if err := win.Run(); err != nil { log.Fatal(err) } diff --git a/internal/assetstore/assetstore.go b/internal/assetstore/assetstore.go index ecf8cd3..4316c3c 100644 --- a/internal/assetstore/assetstore.go +++ b/internal/assetstore/assetstore.go @@ -34,6 +34,9 @@ type AssetStore struct { entries entryMap // These members are used to store things we've already loaded + cursorObj *Object + + cursors map[CursorName]*Cursor fonts map[string]*Font generic *data.Generic maps map[string]*Map @@ -85,6 +88,8 @@ func (a *AssetStore) Refresh() error { } // Refresh + a.cursorObj = nil + a.cursors = make(map[CursorName]*Cursor) a.entries = newEntryMap a.fonts = make(map[string]*Font) a.maps = make(map[string]*Map) diff --git a/internal/assetstore/cursor.go b/internal/assetstore/cursor.go new file mode 100644 index 0000000..191d93c --- /dev/null +++ b/internal/assetstore/cursor.go @@ -0,0 +1,100 @@ +package assetstore + +import ( + "image" + + "github.com/hajimehoshi/ebiten" +) + +// These are just offsets into the Cursors.cur file +type CursorName int + +type Cursor struct { + Hotspot image.Point + Image *ebiten.Image +} + +const ( + UltPointer CursorName = 0 + ChaosPointer CursorName = 1 + UltWaiter CursorName = 2 + ChaosWaiter CursorName = 3 + + // I think these cursors are used in drag + drop + ChaosMarine1 CursorName = 4 + ChaosMarine2 CursorName = 5 + ChaosMarine3 CursorName = 6 + + UltMarine1 CursorName = 7 + UltMarine2 CursorName = 8 + UltMarine3 CursorName = 9 + UltMarine4 CursorName = 10 + UltMarine5 CursorName = 11 + + ChaosHeavy1 CursorName = 12 + ChaosHeavy2 CursorName = 13 + + UltHeavy1 CursorName = 14 + UltHeavy2 CursorName = 15 + UltHeavy3 CursorName = 16 + UltHeavy4 CursorName = 17 + UltHeavy5 CursorName = 18 + UltHeavy6 CursorName = 19 + + ChaosTerminator1 CursorName = 20 + ChaosTerminator2 CursorName = 21 + + UltTerminator1 CursorName = 22 + UltTerminator2 CursorName = 23 + UltTerminator3 CursorName = 24 + UltTerminator4 CursorName = 25 + UltTerminator5 CursorName = 26 + + Deny CursorName = 27 // Red X + + UltLogo CursorName = 28 + UltSquadMarine CursorName = 29 + UltSquadHeavy CursorName = 30 + UltSquadAssault CursorName = 31 + + UltCaptain CursorName = 32 + UltChaplain CursorName = 33 // (maybe?) + UltApothecary CursorName = 34 + UltTechmarine CursorName = 35 + UltLibrarian CursorName = 36 + + DenyAgain CursorName = 37 // Identical to Deny as far as I can see *shrug* +) + +func (a *AssetStore) Cursor(name CursorName) (*Cursor, error) { + if cur, ok := a.cursors[name]; ok { + return cur, nil + } + + if a.cursorObj == nil { + filename, err := a.lookup("Cursors.cur", "", "Cursor") + if err != nil { + return nil, err + } + + obj, err := a.ObjectByPath(filename) + if err != nil { + return nil, err + } + + a.cursorObj = obj + } + + spr, err := a.cursorObj.Sprite(int(name)) + if err != nil { + return nil, err + } + + // TODO: hotspot info. We're using Cursor.cur because it's object format, + // but we do also have .ani files that might contain hotspots. + cur := &Cursor{Image: spr.Image} + + a.cursors[name] = cur + + return cur, nil +} diff --git a/internal/ordoor/ordoor.go b/internal/ordoor/ordoor.go index 6730c5e..37102ea 100644 --- a/internal/ordoor/ordoor.go +++ b/internal/ordoor/ordoor.go @@ -93,6 +93,13 @@ func Run(configFile string, overrideX, overrideY int) error { return fmt.Errorf("Failed to create window: %v", err) } + // TODO: move this into driver. It will need to be able to change cursor. + cursor, err := assets.Cursor(assetstore.UltPointer) + if err != nil { + log.Fatalf("Couldn't load cursor: %v", err) + } + win.SetCursor(cursor.Image) + ordoor.win = win if err := ordoor.setupFlow(); err != nil { diff --git a/internal/ui/window.go b/internal/ui/window.go index 1ac2d6c..c0bf619 100644 --- a/internal/ui/window.go +++ b/internal/ui/window.go @@ -3,6 +3,7 @@ package ui import ( "flag" "fmt" + "image" "log" "os" "runtime/debug" @@ -23,11 +24,16 @@ var ( cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`") ) +// TODO: move all scaling into Window, so drivers only need to cope with one +// coordinate space. This will allow us to draw custom mouse cursors in the +// window, rather than in the driver. type Window struct { Title string KeyUpHandlers map[ebiten.Key]func() MouseWheelHandler func(float64, float64) + cursor *ebiten.Image + // Allow the "game" to be switched out at any time game Game @@ -68,6 +74,17 @@ func (w *Window) Layout(_, _ int) (int, int) { return w.xRes, w.yRes } +func (w *Window) SetCursor(cursor *ebiten.Image) { + w.cursor = cursor + + // Hide the system cursor if we have a custom one + if cursor == nil { + ebiten.SetCursorMode(ebiten.CursorModeVisible) + } else { + ebiten.SetCursorMode(ebiten.CursorModeHidden) + } +} + func (w *Window) Update(screen *ebiten.Image) (outErr error) { // Ebiten does not like it if we panic inside its main loop defer func() { @@ -108,6 +125,16 @@ func (w *Window) Update(screen *ebiten.Image) (outErr error) { return err } + if w.cursor != nil { + // TODO: account for scaling, including the hotspot. + pos := image.Pt(ebiten.CursorPosition()) + op := ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(pos.X), float64(pos.Y)) + if err := screen.DrawImage(w.cursor, &op); 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())