diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-02-16 18:57:15 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:43:50 +0100 |
| commit | c12057320c06f9b4a0bd6c9b25a1dd2d2bd8fd8d (patch) | |
| tree | 77e2d4f4b1430b95fd20dc14a8c2c561a80d6bc8 | |
| parent | 2f01274717d62e59e4bf3b4426a2ec6a76ffac11 (diff) | |
| download | muhqs-game-c12057320c06f9b4a0bd6c9b25a1dd2d2bd8fd8d.tar.gz muhqs-game-c12057320c06f9b4a0bd6c9b25a1dd2d2bd8fd8d.zip | |
move more ui logic into Collection
* Generalize hover detection and move it into ui.Collection
* Implement transparencu for the prompt banner
| -rw-r--r-- | go/client/game.go | 228 | ||||
| -rw-r--r-- | go/client/main.go | 2 | ||||
| -rw-r--r-- | go/client/sealed.go | 45 | ||||
| -rw-r--r-- | go/client/startMenu.go | 23 | ||||
| -rw-r--r-- | go/ui/button.go | 7 | ||||
| -rw-r--r-- | go/ui/choice.go | 12 | ||||
| -rw-r--r-- | go/ui/collection.go | 44 | ||||
| -rw-r--r-- | go/ui/hoverDetector.go | 41 | ||||
| -rw-r--r-- | go/ui/hoverable.go | 120 | ||||
| -rw-r--r-- | go/ui/mapView.go | 22 | ||||
| -rw-r--r-- | go/ui/prompt.go | 37 | ||||
| -rw-r--r-- | go/ui/stateBar.go | 4 | ||||
| -rw-r--r-- | go/ui/textBox.go | 14 | ||||
| -rw-r--r-- | go/ui/widget.go | 6 |
14 files changed, 374 insertions, 231 deletions
diff --git a/go/client/game.go b/go/client/game.go index abd68378..8447e452 100644 --- a/go/client/game.go +++ b/go/client/game.go @@ -19,9 +19,7 @@ const ( RESOLVE_BUTTON_X = 500 STACK_BUFFER_WIDTH = 300 - HOVER_THRESHOLD = 60 - HOVER_CARD_WIDTH = 500 - HOVER_CARD_HEIGHT = 700 + HOVER_THRESHOLD = 60 HAND_VIEW_WIDTH = 500 @@ -30,45 +28,8 @@ const ( POC_BUTTON_WIDTH = 150 ) -// A simple structure to detect if the cursor has not moved for HOVER_THRESHOLD -// ticks. -// hoverDetector.update must be called during each Update invocation to track the -// frame and the current cursor position. -type hoverDetector struct { - last_x, last_y int - ticks int - // ResetFunc is called when the cursor has moved. - // If resetFunc returns true the hoverDetector is reset - // otherwise the reset is skipped. - resetFunc func() bool -} - -func (h *hoverDetector) update(x, y int) { - if x == h.last_x && y == h.last_y { - h.ticks++ - } else { - h.last_x, h.last_y = x, y - h.reset() - } -} - -func (h *hoverDetector) reset() { - if h.resetFunc != nil && !h.resetFunc() { - return - } - h.ticks = 0 - h.resetFunc = nil -} - -func (h *hoverDetector) startedHovering() bool { - return h.ticks == HOVER_THRESHOLD -} - type Game struct { - width, height int - selectedObject interface{} - hoverDetector hoverDetector handLayer *ui.HandView stateBar *ui.StateBar @@ -81,7 +42,7 @@ type Game struct { storeButton *ui.SimpleButton stackBuffer *ui.Buffer prompt *ui.Prompt - widgets []ui.Widget + ui.Collection activePlayer *game.Player @@ -94,15 +55,16 @@ type Game struct { func newGame(app *app) *Game { g := &Game{ - width: app.windowWidth, - height: app.windowHeight, + Collection: ui.Collection{ + Width: app.windowWidth, + Height: app.windowHeight, + }, } - g.widgets = []ui.Widget{} g.gameState = game.NewState() - g.passButton = ui.NewSimpleButton(g.width-PASS_BUTTON_WIDTH, - g.height-DEFAULT_BUTTON_HEIGHT, + g.passButton = ui.NewSimpleButton(g.Width-PASS_BUTTON_WIDTH, + g.Height-DEFAULT_BUTTON_HEIGHT, PASS_BUTTON_WIDTH, DEFAULT_BUTTON_HEIGHT, "Pass", @@ -110,7 +72,7 @@ func newGame(app *app) *Game { if g.prompt != nil { a := g.prompt.Confirm() g.declareAction(a) - g.removeWidget(g.prompt) + g.RemoveWidget(g.prompt) g.prompt = nil } else if g.hasPriority { @@ -118,7 +80,7 @@ func newGame(app *app) *Game { } }) - g.stackBuffer = ui.NewBuffer(g.width-STACK_BUFFER_WIDTH, g.passButton.Y-400, + g.stackBuffer = ui.NewBuffer(g.Width-STACK_BUFFER_WIDTH, g.passButton.Y-400, STACK_BUFFER_WIDTH, 400) return g } @@ -130,6 +92,7 @@ func (g *Game) loadMap(mapName string) *Game { } g.gameState.Map = m g.mapView = ui.NewMapView(g.gameState) + g.AddWidget(g.mapView) storeTiles := m.FilterTiles(func(t *game.Tile) bool { return t.Type == game.TileTypes.Store }) g.storesOnMap = len(storeTiles) > 0 @@ -163,56 +126,56 @@ func (g *Game) initPlayerUi(player *game.Player) *Game { g.handLayer = ui.NewHandView(0, g.mapView.Height(), HAND_VIEW_WIDTH, - g.height-g.mapView.Height()-DEFAULT_BUTTON_HEIGHT, + g.Height-g.mapView.Height()-DEFAULT_BUTTON_HEIGHT, player.Hand, true) - g.addWidget(g.handLayer) + g.AddWidget(g.handLayer) g.stateBar = ui.NewStateBar(0, - g.height-DEFAULT_BUTTON_HEIGHT, + g.Height-DEFAULT_BUTTON_HEIGHT, STATE_BAR_WIDTH, DEFAULT_BUTTON_HEIGHT, g.gameState, player) - g.addWidget(g.stateBar) + g.AddWidget(g.stateBar) g.discardPileButton = ui.NewSimpleButton(g.stateBar.Width, - g.height-DEFAULT_BUTTON_HEIGHT, + g.Height-DEFAULT_BUTTON_HEIGHT, POC_BUTTON_WIDTH, DEFAULT_BUTTON_HEIGHT, "DiscardPile", func(*ui.SimpleButton) { - if idx := g.findWidget(g.discardPileView); idx == -1 { + if idx := g.FindWidget(g.discardPileView); idx == -1 { g.discardPileView.ForceRedraw() - g.addWidget(g.discardPileView) + g.AddWidget(g.discardPileView) } else { - g.removeWidget(g.discardPileView) + g.RemoveWidget(g.discardPileView) } }) - g.addWidget(g.discardPileButton) + g.AddWidget(g.discardPileButton) x = g.discardPileButton.X + g.discardPileButton.Width/2 - y = g.height - DEFAULT_BUTTON_HEIGHT + y = g.Height - DEFAULT_BUTTON_HEIGHT g.discardPileView = ui.NewPocList(x, y, g.activePlayer.DiscardPile) if !g.storesOnMap { x = g.stateBar.Width + g.discardPileButton.Width - y = g.height - DEFAULT_BUTTON_HEIGHT + y = g.Height - DEFAULT_BUTTON_HEIGHT g.storeButton = ui.NewSimpleButton(x, y, POC_BUTTON_WIDTH, DEFAULT_BUTTON_HEIGHT, "Store", func(*ui.SimpleButton) { - if idx := g.findWidget(g.storeView); idx == -1 { + if idx := g.FindWidget(g.storeView); idx == -1 { g.showStore() } else { g.hideStore() } }) - g.addWidget(g.storeButton) + g.AddWidget(g.storeButton) x = g.storeButton.X + g.storeButton.Width/2 - y = g.height - DEFAULT_BUTTON_HEIGHT + y = g.Height - DEFAULT_BUTTON_HEIGHT g.storeView = ui.NewPocList(x, y, g.activePlayer.Store) } @@ -221,38 +184,11 @@ func (g *Game) initPlayerUi(player *game.Player) *Game { func (g *Game) showStore() { g.storeView.ForceRedraw() - g.addWidget(g.storeView) + g.AddWidget(g.storeView) } func (g *Game) hideStore() { - g.removeWidget(g.storeView) -} - -func (g *Game) addWidget(w ui.Widget) { - g.widgets = append(g.widgets, w) -} - -func (g *Game) findWidget(widget ui.Widget) int { - for i, w := range g.widgets { - if w != widget { - continue - } - return i - } - return -1 -} - -func (g *Game) removeWidget(widget ui.Widget) { - idx := g.findWidget(widget) - if idx != -1 { - g.removeWidgetAt(idx) - } -} - -func (g *Game) removeWidgetAt(idx int) { - nwidgets := len(g.widgets) - g.widgets[idx] = g.widgets[nwidgets-1] - g.widgets = g.widgets[:nwidgets-1] + g.RemoveWidget(g.storeView) } func (g *Game) addPermActionChoice(perm game.Permanent, x, y int) { @@ -276,7 +212,7 @@ func (g *Game) addPermActionChoice(perm game.Permanent, x, y int) { g.addActionChoice(p, x, y) } g.choice = ui.NewPermChoice(x, y, perms, onClick) - g.addWidget(g.choice) + g.AddWidget(g.choice) } else { g.selectedObject = perms[0] g.addActionChoice(perms[0], x, y) @@ -297,12 +233,12 @@ func (g *Game) addActionChoice(perm game.Permanent, x, y int) { } } g.choice = ui.NewActionChoice(x, y, actions, onClick) - g.addWidget(g.choice) + g.AddWidget(g.choice) } } func (g *Game) removeChoice() { - g.removeWidget(g.choice) + g.RemoveWidget(g.choice) g.choice = nil } @@ -337,11 +273,11 @@ func (g *Game) declareAction(a game.Action) { } func (g *Game) showPassButton() { - g.addWidget(g.passButton) + g.AddWidget(g.passButton) } func (g *Game) hidePassButton() { - g.removeWidget(g.passButton) + g.RemoveWidget(g.passButton) } func (g *Game) passPriority() { @@ -363,10 +299,16 @@ func (g *Game) handlePlayerNotifications() { } a := n.Context.(game.Action) g.stackBuffer.AddLine(a.String()) + if !g.gameState.Stack.IsEmpty() && g.FindWidget(g.stackBuffer) == -1 { + g.AddWidget(g.stackBuffer) + } case game.ResolvedActionNotification: g.clearMapHighlights() g.stackBuffer.RemoveLast() + if g.gameState.Stack.IsEmpty() { + g.RemoveWidget(g.stackBuffer) + } g.mapView.ForceRedraw() case game.TargetSelectionPrompt: @@ -377,8 +319,8 @@ func (g *Game) handlePlayerNotifications() { } } - g.prompt = ui.NewPrompt(g.height/2, g.width, ctx.Action, ctx.Prompt) - g.addWidget(g.prompt) + g.prompt = ui.NewPrompt(g.Height/2, g.Width, ctx.Action, ctx.Prompt) + g.AddWidget(g.prompt) g.showPassButton() } @@ -392,8 +334,9 @@ func (g *Game) handlePlayerNotifications() { func (g *Game) findObjectAt(x, y int) interface{} { // Iterate the widget in reverse order to ensure that objects are found // in the newer widget possibly drawn over older ones - for i := len(g.widgets) - 1; i >= 0; i-- { - w := g.widgets[i] + widgets := g.Widgets() + for i := len(widgets) - 1; i >= 0; i-- { + w := widgets[i] if obj := w.FindObjectAt(x, y); obj != nil { return obj } @@ -428,9 +371,6 @@ func (g *Game) handleSelection(obj interface{}, x, y int) { g.handleTargetSelection(obj) } else { switch obj := obj.(type) { - case ui.Button: - obj.Click(x, y) - case *game.Tile: if g.storesOnMap && obj.Type == game.TileTypes.Store && g.activePlayer.KnowsStore(obj.Position) { if g.storeView != nil { @@ -467,72 +407,17 @@ func (g *Game) highlightTargets() { } } -func (g *Game) handleHover(obj interface{}, x, y int) { - var hoverHint ui.Widget - switch obj := obj.(type) { - case *game.Card: - wx, wy := x, y - if x+HOVER_CARD_WIDTH > g.width || y+HOVER_CARD_HEIGHT > g.height { - wx = x - HOVER_CARD_WIDTH - wy = y - HOVER_CARD_HEIGHT - } - if wx < 0 { - wx = 0 - } - if wy < 0 { - wy = 0 - } - hoverHint = ui.NewScaledCardView(wx, wy, HOVER_CARD_WIDTH, HOVER_CARD_HEIGHT, obj.Path()) - case *game.Unit: - hoverHint = ui.NewUnitInfo(x+ui.TILE_WIDTH, y, obj) - case game.Permanent: - hoverHint = ui.NewPermInfo(x+ui.TILE_WIDTH, y, obj) - // case *game.Tile: - // if g.storesOnMap && obj.Type == game.TileTypes.Store { - // poc := g.gameState.Map.StoreOn(obj.Position) - // hoverHint = ui.NewPocList(x, y, poc) - // // Set a special hoverDetector resetFunc keeping the hovering store - // // around and allow card selection until the cursor leaves the store widget. - // g.hoverDetector.resetFunc = func() bool { - // x, y := ebiten.CursorPosition() - // if hoverHint.Contains(x, y) { - // return false - // } - // g.removeWidget(hoverHint) - // return true - // } - // } - } - - if hoverHint != nil { - g.addWidget(hoverHint) - // Set the default reset function removing the hoverHint - if g.hoverDetector.resetFunc == nil { - g.hoverDetector.resetFunc = func() bool { - g.removeWidget(hoverHint) - return true - } - } - } -} - func (g *Game) Update() error { g.handlePlayerNotifications() + if err := g.Collection.Update(); err != nil { + return nil + } + x, y := ebiten.CursorPosition() - g.hoverDetector.update(x, y) obj := g.findObjectAt(x, y) - scrollX, scrollY := ebiten.Wheel() - if scrollX != 0 || scrollY != 0 { - for _, w := range g.widgets { - if scrollable, ok := w.(ui.Scrollable); ok && w.Contains(x, y) { - scrollable.Scroll(int(scrollX), int(scrollY)) - } - } - } - g.highlightTargets() if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { @@ -546,36 +431,31 @@ func (g *Game) Update() error { } else { g.handleSelection(obj, x, y) } - } else if g.hoverDetector.startedHovering() && g.selectedObject == nil && obj != nil { - g.handleHover(obj, x, y) } else if inpututil.IsKeyJustPressed(ebiten.KeySpace) { g.passButton.Click(0, 0) } else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { g.removeChoice() g.selectedObject = nil - g.hoverDetector.reset() + g.ResetHover() g.clearMapHighlights() g.handLayer.ClearHighlights() g.hideStore() - g.removeWidget(g.discardPileView) + g.RemoveWidget(g.discardPileView) + if g.prompt != nil { + g.prompt.ClearSelections() + } } return nil } func (g *Game) Draw(screen *ebiten.Image) { - g.mapView.Draw(screen) - if !g.gameState.Stack.IsEmpty() { - g.stackBuffer.Draw(screen) - } - for _, b := range g.widgets { - b.Draw(screen) - } + g.Collection.Draw(screen) ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS())) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { - return g.width, g.height + return g.Width, g.Height } func (g *Game) Start() *Game { diff --git a/go/client/main.go b/go/client/main.go index 2b7496ad..82b60a40 100644 --- a/go/client/main.go +++ b/go/client/main.go @@ -70,7 +70,7 @@ func main() { ebiten.SetWindowSize(app.windowWidth, app.windowHeight) ebiten.SetWindowTitle("Muhq's Game") - startMenu.width, startMenu.height = app.windowWidth, app.windowHeight + startMenu.Width, startMenu.Height = app.windowWidth, app.windowHeight if startDeckPath != "" { file, err := os.Open(startDeckPath) diff --git a/go/client/sealed.go b/go/client/sealed.go index ab0f0296..c219dbf6 100644 --- a/go/client/sealed.go +++ b/go/client/sealed.go @@ -3,6 +3,9 @@ package main import ( "strings" + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "muhq.space/muhqs-game/go/game" "muhq.space/muhqs-game/go/ui" ) @@ -13,27 +16,29 @@ const ( type Sealed struct { ui.Collection - app *app - width, height int + app *app pool game.PileOfCards deck game.PileOfCards } func NewSealed(width, height int, app *app) *Sealed { - s := &Sealed{app: app, width: width, height: height} + s := &Sealed{ + app: app, + Collection: ui.Collection{Width: width, Height: height}, + } s.AddWidget(ui.NewFixedTextBox( - (s.width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH, - (s.height-START_BUTTON_HEIGHT)/2-START_BUTTON_HEIGHT, + (s.Width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH, + (s.Height-START_BUTTON_HEIGHT)/2-START_BUTTON_HEIGHT, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, "sets:", ).Centering(true)) setInput := ui.NewTextInput( - (s.width-START_BUTTON_WIDTH)/2, - (s.height-START_BUTTON_HEIGHT)/2-START_BUTTON_HEIGHT, + (s.Width-START_BUTTON_WIDTH)/2, + (s.Height-START_BUTTON_HEIGHT)/2-START_BUTTON_HEIGHT, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, "set list", @@ -41,8 +46,8 @@ func NewSealed(width, height int, app *app) *Sealed { s.AddWidget(setInput) s.AddWidget(ui.NewSimpleButton( - (s.width-START_BUTTON_WIDTH)/2, - (s.height-START_BUTTON_HEIGHT)/2, + (s.Width-START_BUTTON_WIDTH)/2, + (s.Height-START_BUTTON_HEIGHT)/2, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, "Start", @@ -53,8 +58,20 @@ func NewSealed(width, height int, app *app) *Sealed { return s } +func (s *Sealed) Update() error { + if err := s.Collection.Update(); err != nil { + return err + } + + if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + s.app.popActivity() + } + + return nil +} + func (s *Sealed) Layout(width, height int) (int, int) { - return s.width, s.height + return s.Width, s.Height } func (s *Sealed) startSealed(_sets string) { @@ -75,11 +92,11 @@ func (s *Sealed) startSealed(_sets string) { setDeck.MoveInto(s.pool) } - deckList := ui.NewPocList(s.width-SIDEBARD_WIDTH/2, s.height-START_BUTTON_HEIGHT-20, s.deck) + deckList := ui.NewPocList(s.Width-SIDEBARD_WIDTH/2, s.Height-START_BUTTON_HEIGHT-20, s.deck) deckList.Bg(ui.Gray) s.AddWidget(deckList) - grid := ui.NewCardGrid(0, 0, s.width-SIDEBARD_WIDTH, s.height, 0.5, s.pool).Columns(3) + grid := ui.NewCardGrid(0, 0, s.Width-SIDEBARD_WIDTH, s.Height, 0.5, s.pool).Columns(3) selectCard := func(x, y int) { _card := grid.FindObjectAt(x, y) if _card == nil { @@ -94,8 +111,8 @@ func (s *Sealed) startSealed(_sets string) { s.AddWidget(ui.NewClickWrapper(grid, selectCard)) s.AddWidget(ui.NewSimpleButton( - s.width-SIDEBARD_WIDTH/2-START_BUTTON_WIDTH/2, - s.height-START_BUTTON_HEIGHT, + s.Width-SIDEBARD_WIDTH/2-START_BUTTON_WIDTH/2, + s.Height-START_BUTTON_HEIGHT, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, "Done", diff --git a/go/client/startMenu.go b/go/client/startMenu.go index 29a4a18c..94ec3aef 100644 --- a/go/client/startMenu.go +++ b/go/client/startMenu.go @@ -17,8 +17,7 @@ const ( type StartMenu struct { ui.Collection - app *app - width, height int + app *app deckInput *ui.TextInput @@ -36,8 +35,8 @@ func NewStartMenu(app *app) *StartMenu { func (m *StartMenu) build() { deckInput := ui.NewTextInput( - (m.width-DECK_LIST_WIDTH)/2, - (m.height-DECK_LIST_HEIGHT)/2, + (m.Width-DECK_LIST_WIDTH)/2, + (m.Height-DECK_LIST_HEIGHT)/2, DECK_LIST_WIDTH, DECK_LIST_HEIGHT, "deck", @@ -52,7 +51,7 @@ func (m *StartMenu) build() { m.AddWidget(deckInput) m.AddWidget(ui.NewFixedTextBox( - (m.width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH, + (m.Width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH, deckInput.Y-START_BUTTON_HEIGHT, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, @@ -60,7 +59,7 @@ func (m *StartMenu) build() { ).Centering(true)) mapInput := ui.NewTextInput( - (m.width-START_BUTTON_WIDTH)/2, + (m.Width-START_BUTTON_WIDTH)/2, deckInput.Y-START_BUTTON_HEIGHT, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, @@ -74,7 +73,7 @@ func (m *StartMenu) build() { m.AddWidget(mapInput) m.AddWidget(ui.NewFixedTextBox( - (m.width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH, + (m.Width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH, mapInput.Y-START_BUTTON_HEIGHT, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, @@ -82,7 +81,7 @@ func (m *StartMenu) build() { ).Centering(true)) playerInput := ui.NewTextInput( - (m.width-START_BUTTON_WIDTH)/2, + (m.Width-START_BUTTON_WIDTH)/2, mapInput.Y-START_BUTTON_HEIGHT, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, @@ -96,18 +95,18 @@ func (m *StartMenu) build() { m.AddWidget(playerInput) m.AddWidget(ui.NewSimpleButton( - (m.width-START_BUTTON_WIDTH)/2+START_BUTTON_WIDTH/2, + (m.Width-START_BUTTON_WIDTH)/2+START_BUTTON_WIDTH/2, deckInput.Y+deckInput.Height, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, "Sealed", func(*ui.SimpleButton) { - sealed := NewSealed(m.width, m.height, m.app) + sealed := NewSealed(m.Width, m.Height, m.app) m.app.pushActivity(sealed) })) m.AddWidget(ui.NewSimpleButton( - (m.width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH/2, + (m.Width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH/2, deckInput.Y+deckInput.Height, START_BUTTON_WIDTH, START_BUTTON_HEIGHT, @@ -129,7 +128,7 @@ func (m *StartMenu) Update() error { } func (m *StartMenu) Layout(width, height int) (int, int) { - return m.width, m.height + return m.Width, m.Height } func (m *StartMenu) startGame() { diff --git a/go/ui/button.go b/go/ui/button.go index 78ab459f..eac3c2e4 100644 --- a/go/ui/button.go +++ b/go/ui/button.go @@ -41,13 +41,6 @@ func (b *SimpleButton) render() *ebiten.Image { return img } -func (b *SimpleButton) FindObjectAt(x, y int) interface{} { - if b.Contains(x, y) { - return b - } - return nil -} - func (b *SimpleButton) Click(x, y int) { b.OnClick(b) } diff --git a/go/ui/choice.go b/go/ui/choice.go index bacc6657..28878604 100644 --- a/go/ui/choice.go +++ b/go/ui/choice.go @@ -51,16 +51,8 @@ func (c *Choice) Contains(x, y int) bool { return x >= c.X && x <= c.X+c.Width && y >= c.Y && y <= c.Y+height } -func (c *Choice) FindObjectAt(x, y int) interface{} { - if c.Contains(x, y) { - return c - } - return nil -} - -func (c *Choice) Render() *ebiten.Image { - return nil -} +func (c *Choice) FindObjectAt(x, y int) interface{} { return nil } +func (c *Choice) Render() *ebiten.Image { return nil } func NewObjectChoice(x, y int, objs []interface{}, onClick func(*Choice, int, int)) *Choice { // TODO: use dynamic with width and height diff --git a/go/ui/collection.go b/go/ui/collection.go index ac53ba8c..d75e3267 100644 --- a/go/ui/collection.go +++ b/go/ui/collection.go @@ -1,12 +1,20 @@ package ui import ( + "log" + "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/inpututil" ) type Collection struct { - widgets []Widget + Width, Height int + hoverDetector hoverDetector + widgets []Widget +} + +func (c *Collection) Layout() (int, int) { + return c.Width, c.Height } func (c *Collection) Widgets() []Widget { @@ -17,10 +25,40 @@ func (c *Collection) Clear() { c.widgets = []Widget{} } +func (c *Collection) ResetHover() { + c.hoverDetector.reset(0, 0) +} + func (c *Collection) AddWidget(w Widget) { + if w == nil { + log.Panicf("Adding nil widget to collection") + } c.widgets = append(c.widgets, w) } +func (c *Collection) FindWidget(toFind Widget) int { + for i, w := range c.widgets { + if w != toFind { + continue + } + return i + } + return -1 +} + +func (c *Collection) RemoveWidget(widget Widget) { + idx := c.FindWidget(widget) + if idx != -1 { + c.RemoveWidgetAt(idx) + } +} + +func (c *Collection) RemoveWidgetAt(idx int) { + nwidgets := len(c.widgets) + c.widgets[idx] = c.widgets[nwidgets-1] + c.widgets = c.widgets[:nwidgets-1] +} + func (c *Collection) Draw(screen *ebiten.Image) { for _, w := range c.widgets { w.Draw(screen) @@ -39,6 +77,8 @@ func (c *Collection) Update() error { break } + c.hoverDetector.update(c, x, y, activeWidget) + if activeWidget == nil { return nil } @@ -61,7 +101,7 @@ func (c *Collection) Update() error { } if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { - if btn, ok := activeWidget.(Button); ok { + if btn, ok := activeWidget.(Clickable); ok { btn.Click(x, y) } } diff --git a/go/ui/hoverDetector.go b/go/ui/hoverDetector.go new file mode 100644 index 00000000..49161f4f --- /dev/null +++ b/go/ui/hoverDetector.go @@ -0,0 +1,41 @@ +package ui + +// A simple structure to detect if the cursor has not moved or started moving again. +// hoverDetector.update must be called during each Update invocation to track the +// frame and the current cursor position. +type hoverDetector struct { + last_x, last_y int + ticks int + hovering Hoverable +} + +func (h *hoverDetector) update(collection *Collection, x, y int, w Widget) { + if x == h.last_x && y == h.last_y { + h.hover(collection, x, y, w) + } else { + h.reset(x, y) + h.last_x, h.last_y = x, y + } +} + +func (h *hoverDetector) hover(collection *Collection, x, y int, w Widget) { + h.ticks++ + + if w == nil || h.hovering != nil { + return + } + + if hoverable, ok := w.(Hoverable); ok && hoverable.onHover(h.ticks, x, y, collection) { + h.hovering = hoverable + } +} + +func (h *hoverDetector) reset(x, y int) { + if h.hovering != nil { + if !h.hovering.stopHover(h.ticks, x, y) { + return + } + h.hovering = nil + } + h.ticks = 0 +} diff --git a/go/ui/hoverable.go b/go/ui/hoverable.go new file mode 100644 index 00000000..54d4ec15 --- /dev/null +++ b/go/ui/hoverable.go @@ -0,0 +1,120 @@ +package ui + +import ( + "muhq.space/muhqs-game/go/game" +) + +const ( + HOVER_THRESHOLD = 60 + + HOVER_CARD_WIDTH = 500 + HOVER_CARD_HEIGHT = 700 +) + +type Hoverable interface { + onHover(ticks int, x, y int, c *Collection) bool + stopHover(ticks int, x, y int) bool +} + +type hoverWidget struct { + createHint func(x, y int) Widget + reset func() + xMax, yMax int +} + +func (h *hoverWidget) showHint(x, y int, c *Collection) { + hint := h.createHint(x, y) + if hint == nil { + return + } + + c.AddWidget(hint) + h.reset = func() { + c.RemoveWidget(hint) + } +} + +func (h *hoverWidget) onHover(ticks int, x, y int, c *Collection) bool { + if h.xMax == 0 || h.yMax == 0 { + width, height := c.Layout() + h.xMax = width + h.yMax = height + } + + if ticks == HOVER_THRESHOLD { + h.showHint(x, y, c) + return true + } + + return false +} + +func (h *hoverWidget) stopHover(ticks int, x, y int) bool { + if h.reset != nil { + h.reset() + } + return true +} + +type hoverCardView struct { + hoverWidget +} + +func (h *hoverCardView) init(w Widget) { + h.createHint = func(x, y int) Widget { + card := w.FindObjectAt(x, y).(*game.Card) + if card == nil { + return nil + } + + wx, wy := x, y + if x+HOVER_CARD_WIDTH > h.xMax || y+HOVER_CARD_HEIGHT > h.yMax { + wx = x - HOVER_CARD_WIDTH + wy = y - HOVER_CARD_HEIGHT + } + if wx < 0 { + wx = 0 + } + if wy < 0 { + wy = 0 + } + + return NewScaledCardView(wx, wy, HOVER_CARD_WIDTH, HOVER_CARD_HEIGHT, card.Path()) + } +} + +type hoverPermInfo struct { + hoverWidget +} + +func (h *hoverPermInfo) init(w Widget) { + h.createHint = func(x, y int) Widget { + obj := w.FindObjectAt(x, y) + if obj == nil { + return nil + } + + perm, ok := obj.(game.Permanent) + if !ok { + return nil + } + + hint := NewPermInfo(x+TILE_WIDTH, y, perm) + + wx, wy := x, y + if hint.X+hint.Width > h.xMax || y+hint.Height > h.yMax { + wx = x - hint.Width + wy = y - hint.Height + } + if wx < 0 { + wx = 0 + } + if wy < 0 { + wy = 0 + } + + hint.X, hint.Y = wx, wy + + return hint + } +} diff --git a/go/ui/mapView.go b/go/ui/mapView.go index 155750b4..0553b348 100644 --- a/go/ui/mapView.go +++ b/go/ui/mapView.go @@ -23,6 +23,7 @@ const ( ) type MapView struct { + hoverPermInfo gameState *game.State mapLayer *ebiten.Image permanentsLayer *ebiten.Image @@ -31,14 +32,25 @@ type MapView struct { } func NewMapView(g *game.State) *MapView { - vw := MapView{g, nil, nil, []game.Position{}, []game.Permanent{}} - return &vw + vw := &MapView{gameState: g} + vw.hoverPermInfo.init(vw) + return vw } func (mv *MapView) Height() int { return len(mv.gameState.Map.Tiles) * TILE_HEIGHT } +func (mv *MapView) Width() int { + maxWidth := 0 + for _, row := range mv.gameState.Map.Tiles { + if n := len(row); n > maxWidth { + maxWidth = n + } + } + return maxWidth * TILE_WIDTH +} + func rotateStreetImg(x, y int, radian float64, op *ebiten.DrawImageOptions) { // Move the image's center to the screen's upper-left corner. // This is a preparation for rotating. When geometry matrices are applied, @@ -278,6 +290,12 @@ func (vw *MapView) ClearPermanentsHighlights() { vw.HighlightPermanents([]game.Permanent{}) } +func (mv *MapView) Contains(x, y int) bool { + w := mv.Width() + h := mv.Height() + return x >= 0 && y >= 0 && x <= w && y <= h +} + func (*MapView) Render(Widget) *ebiten.Image { return nil } diff --git a/go/ui/prompt.go b/go/ui/prompt.go index b8d60a49..93c4b4cd 100644 --- a/go/ui/prompt.go +++ b/go/ui/prompt.go @@ -1,6 +1,8 @@ package ui import ( + "image/color" + "github.com/hajimehoshi/ebiten/v2" "muhq.space/muhqs-game/go/game" @@ -42,6 +44,10 @@ func (p *Prompt) Height() int { return PROMPT_HEIGHT } +func (p *Prompt) ClearSelections() { + p.action.Targets().ClearSelections() +} + func (p *Prompt) Draw(screen *ebiten.Image) { for _, comp := range p.components { comp.Draw(screen) @@ -54,3 +60,34 @@ func (p *Prompt) Contains(x, y int) bool { func (p *Prompt) FindObjectAt(screenX, screenY int) interface{} { return nil } func (*Prompt) Render(Widget) *ebiten.Image { return nil } + +func (p *Prompt) setTextBgAlpha(alpha uint8) { + for _, comp := range p.components { + textBox, ok := comp.(*TextBox) + if !ok { + continue + } + bg := color.RGBAModel.Convert(textBox.bg).(color.RGBA) + bg.A = alpha + textBox.Bg(bg) + textBox.ForceRedraw() + } +} + +func (p *Prompt) onHover(ticks int, x int, y int, c *Collection) bool { + if ticks == 1 { + p.setTextBgAlpha(0x7f) + return true + } + + return false +} + +func (p *Prompt) stopHover(ticks int, x, y int) bool { + if p.Contains(x, y) { + return false + } + + p.setTextBgAlpha(0xff) + return true +} diff --git a/go/ui/stateBar.go b/go/ui/stateBar.go index 93de9af9..1686b5e1 100644 --- a/go/ui/stateBar.go +++ b/go/ui/stateBar.go @@ -44,9 +44,7 @@ func (sb *StateBar) Draw(screen *ebiten.Image) { sb.phaseView.Draw(screen) } -func (sb *StateBar) FindObjectAt(x, y int) interface{} { - return nil -} +func (sb *StateBar) FindObjectAt(x, y int) interface{} { return nil } func (sb *StateBar) ForceRedraw() { if sb.resourceView != nil { diff --git a/go/ui/textBox.go b/go/ui/textBox.go index 2635535c..66b90698 100644 --- a/go/ui/textBox.go +++ b/go/ui/textBox.go @@ -84,11 +84,20 @@ func NewUnitInfo(x, y int, u *game.Unit) *TextBox { return NewAutoTextBox(x, y, info) } -func NewPermInfo(x, y int, p game.Permanent) *TextBox { +func NewGenericPermInfo(x, y int, p game.Permanent) *TextBox { info := fmt.Sprintf("%s\nDamage: %d", p.String(), p.Damage()) return NewAutoTextBox(x, y, info) } +func NewPermInfo(x, y int, p game.Permanent) *TextBox { + switch p := p.(type) { + case *game.Unit: + return NewUnitInfo(x, y, p) + default: + return NewGenericPermInfo(x, y, p) + } +} + func (tb *TextBox) render() *ebiten.Image { var x, y int b := text.BoundString(font.Font24, tb.text) @@ -115,6 +124,7 @@ func (tb *TextBox) render() *ebiten.Image { type PocList struct { TextBox + hoverCardView centerX int bottomY int poc game.PileOfCards @@ -128,6 +138,8 @@ func NewPocList(centerX, bottomY int, poc game.PileOfCards) *PocList { poc: poc, } + w.hoverCardView.init(w) + w.renderImpl = func() *ebiten.Image { w.setText() return w.render() diff --git a/go/ui/widget.go b/go/ui/widget.go index cfcdd766..4ee0caac 100644 --- a/go/ui/widget.go +++ b/go/ui/widget.go @@ -17,12 +17,8 @@ type WidgetBase struct { renderImpl func() *ebiten.Image } -func renderDummy() *ebiten.Image { - return nil -} - func NewWidgetBase(x, y, width, height int) WidgetBase { - return WidgetBase{x, y, width, height, nil, renderDummy} + return WidgetBase{X: x, Y: y, Width: width, Height: height} } func (w *WidgetBase) Draw(screen *ebiten.Image) { |
