aboutsummaryrefslogtreecommitdiff
path: root/go/ui
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-02-16 18:57:15 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:50:10 +0200
commit63ad22242562b1e66353694f2a17df6d1c64a8fd (patch)
treef91a3a98e554e58a30d5bb0a34b021c7fb206311 /go/ui
parentc0ab1110429be3a2a1664084469952ea082961c1 (diff)
downloadmuhqs-game-63ad22242562b1e66353694f2a17df6d1c64a8fd.tar.gz
muhqs-game-63ad22242562b1e66353694f2a17df6d1c64a8fd.zip
move more ui logic into Collection
* Generalize hover detection and move it into ui.Collection * Implement transparencu for the prompt banner
Diffstat (limited to 'go/ui')
-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
10 files changed, 277 insertions, 30 deletions
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) {