aboutsummaryrefslogtreecommitdiff
path: root/go
diff options
context:
space:
mode:
Diffstat (limited to 'go')
-rw-r--r--go/client/game.go228
-rw-r--r--go/client/main.go2
-rw-r--r--go/client/sealed.go45
-rw-r--r--go/client/startMenu.go23
-rw-r--r--go/ui/button.go7
-rw-r--r--go/ui/choice.go12
-rw-r--r--go/ui/collection.go44
-rw-r--r--go/ui/hoverDetector.go41
-rw-r--r--go/ui/hoverable.go120
-rw-r--r--go/ui/mapView.go22
-rw-r--r--go/ui/prompt.go37
-rw-r--r--go/ui/stateBar.go4
-rw-r--r--go/ui/textBox.go14
-rw-r--r--go/ui/widget.go6
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) {