aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-02-14 16:05:06 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-08-20 15:50:09 +0200
commit379c17fe2244dc8fe8365cdbf3d62e21951c8121 (patch)
tree32a1741be38f3745c1edea86a4adf9116cc7c93f
parent1ac413e1d0ddc39a41374ff9b3375e6de3e1af1c (diff)
downloadmuhqs-game-379c17fe2244dc8fe8365cdbf3d62e21951c8121.tar.gz
muhqs-game-379c17fe2244dc8fe8365cdbf3d62e21951c8121.zip
add sealed activity to select a starting deck
-rw-r--r--go/client/game.go6
-rw-r--r--go/client/main.go16
-rw-r--r--go/client/sealed.go106
-rw-r--r--go/client/startMenu.go132
-rw-r--r--go/game/card.go2
-rw-r--r--go/game/pileOfCards.go17
-rw-r--r--go/game/set.go43
-rw-r--r--go/go.mod1
-rw-r--r--go/go.sum1
-rw-r--r--go/ui/buffer.go2
-rw-r--r--go/ui/button.go4
-rw-r--r--go/ui/cardGrid.go183
-rw-r--r--go/ui/clickable.go40
-rw-r--r--go/ui/collection.go70
-rw-r--r--go/ui/colors.go4
-rw-r--r--go/ui/hand.go100
-rw-r--r--go/ui/stateBar.go2
-rw-r--r--go/ui/textBox.go10
-rw-r--r--go/ui/textInput.go1
19 files changed, 570 insertions, 170 deletions
diff --git a/go/client/game.go b/go/client/game.go
index 91e7e365..76b04fa7 100644
--- a/go/client/game.go
+++ b/go/client/game.go
@@ -503,8 +503,10 @@ func (g *Game) Update() error {
scrollX, scrollY := ebiten.Wheel()
if scrollX != 0 || scrollY != 0 {
- if scrollable, ok := obj.(ui.Scrollable); ok {
- scrollable.Scroll(int(scrollX), int(scrollY))
+ for _, w := range g.widgets {
+ if scrollable, ok := w.(ui.Scrollable); ok && w.Contains(x, y) {
+ scrollable.Scroll(int(scrollX), int(scrollY))
+ }
}
}
diff --git a/go/client/main.go b/go/client/main.go
index 632bcc13..2b7496ad 100644
--- a/go/client/main.go
+++ b/go/client/main.go
@@ -28,15 +28,23 @@ type app struct {
windowWidth int
windowHeight int
- active ebiten.Game
+ activities []ebiten.Game
+}
+
+func (a *app) pushActivity(activity ebiten.Game) {
+ a.activities = append(a.activities, activity)
+}
+
+func (a *app) popActivity() {
+ a.activities = a.activities[:len(a.activities)-1]
}
func (a *app) Update() error {
- return a.active.Update()
+ return a.activities[len(a.activities)-1].Update()
}
func (a *app) Draw(screen *ebiten.Image) {
- a.active.Draw(screen)
+ a.activities[len(a.activities)-1].Draw(screen)
}
func (a *app) Layout(outsideWidth, outsideHeight int) (int, int) {
@@ -84,7 +92,7 @@ func main() {
1 nautics/galley`
}
- app.active = startMenu
+ app.pushActivity(startMenu)
if err := ebiten.RunGame(app); err != nil {
log.Fatal(err)
}
diff --git a/go/client/sealed.go b/go/client/sealed.go
new file mode 100644
index 00000000..6096a1d5
--- /dev/null
+++ b/go/client/sealed.go
@@ -0,0 +1,106 @@
+package main
+
+import (
+ "strings"
+
+ "muhq.space/muhqs-game/go/game"
+ "muhq.space/muhqs-game/go/ui"
+)
+
+const (
+ SIDEBARD_WIDTH = 200
+)
+
+type Sealed struct {
+ ui.Collection
+ app *app
+ width, height int
+
+ pool game.PileOfCards
+ deck game.PileOfCards
+}
+
+func NewSealed(width, height int, app *app) *Sealed {
+ s := &Sealed{app: app, 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,
+ 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,
+ START_BUTTON_WIDTH,
+ START_BUTTON_HEIGHT,
+ "set list",
+ )
+ s.AddWidget(setInput)
+
+ s.AddWidget(ui.NewSimpleButton(
+ (s.width-START_BUTTON_WIDTH)/2,
+ (s.height-START_BUTTON_HEIGHT)/2,
+ START_BUTTON_WIDTH,
+ START_BUTTON_HEIGHT,
+ "Start",
+ func(*ui.SimpleButton) {
+ s.startSealed(setInput.Text())
+ }))
+
+ return s
+}
+
+func (s *Sealed) Layout(width, height int) (int, int) {
+ return s.width, s.height
+}
+
+func (s *Sealed) startSealed(_sets string) {
+ s.pool = game.NewPileOfCards()
+ s.deck = game.NewPileOfCards()
+
+ s.Clear()
+
+ sets := strings.Split(_sets, ",")
+ for _, setName := range sets {
+ set := game.SetNames[setName]
+ setDeck := game.NewDeckFromCardPaths(set.CardPaths())
+ setDeck.MoveInto(s.pool)
+ }
+
+ 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)
+ selectCard := func(x, y int) {
+ _card := grid.FindObjectAt(x, y)
+ if _card == nil {
+ return
+ }
+
+ card := _card.(*game.Card)
+ s.pool.MoveCard(card, s.deck)
+ grid.ForceRedraw()
+ deckList.ForceRedraw()
+ }
+ s.AddWidget(ui.NewClickWrapper(grid, selectCard))
+
+ s.AddWidget(ui.NewSimpleButton(
+ s.width-SIDEBARD_WIDTH/2-START_BUTTON_WIDTH/2,
+ s.height-START_BUTTON_HEIGHT,
+ START_BUTTON_WIDTH,
+ START_BUTTON_HEIGHT,
+ "Done",
+ func(*ui.SimpleButton) {
+ s.done()
+ }))
+}
+
+func (s *Sealed) done() {
+ startMenu := s.app.activities[len(s.app.activities)-2].(*StartMenu)
+ startMenu.deckInput.SetInput(s.deck.ToList())
+ s.app.popActivity()
+}
diff --git a/go/client/startMenu.go b/go/client/startMenu.go
index 8f149f8b..29a4a18c 100644
--- a/go/client/startMenu.go
+++ b/go/client/startMenu.go
@@ -1,11 +1,8 @@
package main
import (
- "image/color"
"log"
- "github.com/hajimehoshi/ebiten/v2"
- "github.com/hajimehoshi/ebiten/v2/inpututil"
// "muhq.space/muhqs-game/go/game"
"muhq.space/muhqs-game/go/ui"
)
@@ -13,17 +10,22 @@ import (
const (
START_BUTTON_WIDTH = 150
START_BUTTON_HEIGHT = 50
+
+ DECK_LIST_WIDTH = 300
+ DECK_LIST_HEIGHT = 500
)
type StartMenu struct {
+ ui.Collection
app *app
width, height int
+ deckInput *ui.TextInput
+
playerName string
startDeck string
remote string
mapPath string
- widgets []ui.Widget
}
func NewStartMenu(app *app) *StartMenu {
@@ -33,18 +35,55 @@ func NewStartMenu(app *app) *StartMenu {
}
func (m *StartMenu) build() {
- playerLabel := ui.NewFixedTextBox(
+ deckInput := ui.NewTextInput(
+ (m.width-DECK_LIST_WIDTH)/2,
+ (m.height-DECK_LIST_HEIGHT)/2,
+ DECK_LIST_WIDTH,
+ DECK_LIST_HEIGHT,
+ "deck",
+ )
+ deckInput.Bg(ui.Gray)
+
+ if m.startDeck != "" {
+ deckInput.AddInput([]rune(m.startDeck))
+ }
+
+ m.deckInput = deckInput
+ m.AddWidget(deckInput)
+
+ m.AddWidget(ui.NewFixedTextBox(
+ (m.width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH,
+ deckInput.Y-START_BUTTON_HEIGHT,
+ START_BUTTON_WIDTH,
+ START_BUTTON_HEIGHT,
+ "map:",
+ ).Centering(true))
+
+ mapInput := ui.NewTextInput(
+ (m.width-START_BUTTON_WIDTH)/2,
+ deckInput.Y-START_BUTTON_HEIGHT,
+ START_BUTTON_WIDTH,
+ START_BUTTON_HEIGHT,
+ "map",
+ )
+
+ if m.mapPath != "" {
+ mapInput.AddInput([]rune(m.mapPath))
+ }
+
+ m.AddWidget(mapInput)
+
+ m.AddWidget(ui.NewFixedTextBox(
(m.width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH,
- (m.height-START_BUTTON_HEIGHT)/2-START_BUTTON_HEIGHT,
+ mapInput.Y-START_BUTTON_HEIGHT,
START_BUTTON_WIDTH,
START_BUTTON_HEIGHT,
"name:",
- ).Centering(true).Bg(color.Black)
- m.widgets = append(m.widgets, playerLabel)
+ ).Centering(true))
playerInput := ui.NewTextInput(
(m.width-START_BUTTON_WIDTH)/2,
- (m.height-START_BUTTON_HEIGHT)/2-START_BUTTON_HEIGHT,
+ mapInput.Y-START_BUTTON_HEIGHT,
START_BUTTON_WIDTH,
START_BUTTON_HEIGHT,
"player name",
@@ -54,77 +93,46 @@ func (m *StartMenu) build() {
playerInput.AddInput([]rune(m.playerName))
}
- m.widgets = append(m.widgets, playerInput)
+ m.AddWidget(playerInput)
- m.widgets = append(m.widgets, ui.NewSimpleButton(
- (m.width-START_BUTTON_WIDTH)/2,
- (m.height-START_BUTTON_HEIGHT)/2,
+ m.AddWidget(ui.NewSimpleButton(
+ (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)
+ m.app.pushActivity(sealed)
+ }))
+
+ m.AddWidget(ui.NewSimpleButton(
+ (m.width-START_BUTTON_WIDTH)/2-START_BUTTON_WIDTH/2,
+ deckInput.Y+deckInput.Height,
START_BUTTON_WIDTH,
START_BUTTON_HEIGHT,
"Start",
func(*ui.SimpleButton) {
m.playerName = playerInput.Text()
- m.app.active = m.next()
+ m.mapPath = mapInput.Text()
+ m.startDeck = deckInput.Text()
+ m.startGame()
}))
}
func (m *StartMenu) Update() error {
- if m.widgets == nil {
+ if m.Widgets() == nil {
m.build()
}
- x, y := ebiten.CursorPosition()
-
- var activeWidget ui.Widget
- for _, w := range m.widgets {
- if !w.Contains(x, y) {
- continue
- }
- activeWidget = w
- break
- }
-
- if activeWidget == nil {
- return nil
- }
-
- scrollX, scrollY := ebiten.Wheel()
- if scrollX != 0 || scrollY != 0 {
- if scrollable, ok := activeWidget.(ui.Scrollable); ok {
- scrollable.Scroll(int(scrollX), int(scrollY))
- }
- }
-
- if textInput, ok := activeWidget.(*ui.TextInput); ok {
- // var input []rune
- // ebiten.AppendInputChars(input)
- input := ebiten.InputChars()
- if len(input) > 0 {
- textInput.AddInput(input)
- }
- textInput.HandleKey()
- }
-
- if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
- if btn, ok := activeWidget.(ui.Button); ok {
- btn.Click(x, y)
- }
- }
-
- return nil
-}
-
-func (m *StartMenu) Draw(screen *ebiten.Image) {
- for _, w := range m.widgets {
- w.Draw(screen)
- }
+ return m.Collection.Update()
}
func (m *StartMenu) Layout(width, height int) (int, int) {
return m.width, m.height
}
-func (m *StartMenu) next() ebiten.Game {
+func (m *StartMenu) startGame() {
if m.remote != "" {
log.Fatal("Remote games are currently not implemented")
}
@@ -138,5 +146,5 @@ func (m *StartMenu) next() ebiten.Game {
g.gameState.Players[idx].Color = color
}
- return g.Start()
+ m.app.pushActivity(g.Start())
}
diff --git a/go/game/card.go b/go/game/card.go
index a6abffd1..031b95bf 100644
--- a/go/game/card.go
+++ b/go/game/card.go
@@ -291,7 +291,7 @@ func NewCard(cardPath string) *Card {
c := Card{
Name: "",
- Set: SetIdentifierNames[set],
+ Set: SetNames[set],
Type: undefinedCardType,
BuyCost: NOT_BUYABLE,
Values: map[string]interface{}{},
diff --git a/go/game/pileOfCards.go b/go/game/pileOfCards.go
index 0a5eaa97..f88dfe93 100644
--- a/go/game/pileOfCards.go
+++ b/go/game/pileOfCards.go
@@ -1,6 +1,8 @@
package game
import (
+ "fmt"
+
"golang.org/x/exp/slices"
)
@@ -14,6 +16,7 @@ type PileOfCards interface {
RemoveCard(card *Card)
MoveCard(card *Card, into PileOfCards)
MoveInto(into PileOfCards)
+ ToList() string
}
type PileOfCardsBase struct {
@@ -75,3 +78,17 @@ func (poc *PileOfCardsBase) MoveInto(into PileOfCards) {
into.AddCards(poc.cards)
poc.cards = []*Card{}
}
+
+func (poc *PileOfCardsBase) ToList() (list string) {
+ m := make(map[string]int)
+ for _, card := range poc.cards {
+ path := card.Path()
+ m[path]++
+ }
+
+ for path, amount := range m {
+ list += fmt.Sprintf("%d %s\n", amount, path)
+ }
+
+ return list[:len(list)-1]
+}
diff --git a/go/game/set.go b/go/game/set.go
index b0565eca..aca5b7f5 100644
--- a/go/game/set.go
+++ b/go/game/set.go
@@ -2,6 +2,11 @@ package game
import (
"log"
+ "net/http"
+ "path"
+ "strings"
+
+ "golang.org/x/net/html"
)
type SetIdentifier int
@@ -43,7 +48,7 @@ func (si SetIdentifier) String() string {
return ""
}
-var SetIdentifiers = struct {
+var Sets = struct {
Base SetIdentifier
Magic SetIdentifier
Equipments SetIdentifier
@@ -65,7 +70,7 @@ var SetIdentifiers = struct {
Exp1: Exp1,
}
-var SetIdentifierNames = map[string]SetIdentifier{
+var SetNames = map[string]SetIdentifier{
"base": Base,
"magic": Magic,
"equipments": Equipments,
@@ -76,3 +81,37 @@ var SetIdentifierNames = map[string]SetIdentifier{
"tyrant": Tyrant,
"exp1": Exp1,
}
+
+func (set *SetIdentifier) CardPaths() []string {
+ url := PROTOCOL + path.Join(BASE_URL, CARD_DATA_URL, set.String())
+ log.Printf("loading %s\n", url)
+ resp, err := http.Get(url)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ defer resp.Body.Close()
+
+ doc, err := html.Parse(resp.Body)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ paths := []string{}
+ var walk func(*html.Node)
+ walk = func(n *html.Node) {
+ if n.Type == html.ElementNode && n.Data == "a" {
+ for _, a := range n.Attr {
+ if a.Key == "href" {
+ paths = append(paths, path.Join(set.String(), strings.TrimSuffix(a.Val, ".yml")))
+ break
+ }
+ }
+ }
+ for c := n.FirstChild; c != nil; c = c.NextSibling {
+ walk(c)
+ }
+ }
+ walk(doc)
+ return paths
+}
diff --git a/go/go.mod b/go/go.mod
index 24b6cb52..4534d920 100644
--- a/go/go.mod
+++ b/go/go.mod
@@ -7,6 +7,7 @@ require (
github.com/hajimehoshi/ebiten/v2 v2.4.15
golang.org/x/exp v0.0.0-20230116083435-1de6713980de
golang.org/x/image v0.3.0
+ golang.org/x/net v0.0.0-20220722155237-a158d28d115b
gopkg.in/yaml.v3 v3.0.1
)
diff --git a/go/go.sum b/go/go.sum
index f505893f..6bd6322a 100644
--- a/go/go.sum
+++ b/go/go.sum
@@ -60,6 +60,7 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b h1:PxfKdU9lEEDYjdIzOtC4qFWgkU2rGHdKlKowJSMN9h0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
diff --git a/go/ui/buffer.go b/go/ui/buffer.go
index e95102fe..201273b8 100644
--- a/go/ui/buffer.go
+++ b/go/ui/buffer.go
@@ -10,7 +10,7 @@ import (
)
var (
- BUFFER_BACKGROUND = gray
+ BUFFER_BACKGROUND = Gray
BUFFER_FOREGROUND = color.White
)
diff --git a/go/ui/button.go b/go/ui/button.go
index 0fd26936..78ab459f 100644
--- a/go/ui/button.go
+++ b/go/ui/button.go
@@ -12,7 +12,7 @@ import (
type Button interface {
Widget
- Click(x, y int)
+ Clickable
}
type SimpleButton struct {
@@ -24,7 +24,7 @@ type SimpleButton struct {
}
func NewSimpleButton(x, y, w, h int, label string, onClick func(b *SimpleButton)) *SimpleButton {
- sb := &SimpleButton{NewWidgetBase(x, y, w, h), label, onClick, gray, color.White}
+ sb := &SimpleButton{NewWidgetBase(x, y, w, h), label, onClick, Gray, color.White}
sb.renderImpl = func() *ebiten.Image { return sb.render() }
return sb
}
diff --git a/go/ui/cardGrid.go b/go/ui/cardGrid.go
new file mode 100644
index 00000000..9ed92aa4
--- /dev/null
+++ b/go/ui/cardGrid.go
@@ -0,0 +1,183 @@
+package ui
+
+import (
+ "log"
+ "math"
+
+ "github.com/hajimehoshi/ebiten/v2"
+ "muhq.space/muhqs-game/go/assets"
+ "muhq.space/muhqs-game/go/game"
+
+ "golang.org/x/exp/slices"
+)
+
+type CardGrid struct {
+ WidgetBase
+ cards game.PileOfCards
+ columns, rows int
+ scale float64
+ cardWidth, cardHeight float64
+ xOffset, yOffset int
+ highlights []*game.Card
+ grid *ebiten.Image
+ gridCards int
+}
+
+func NewCardGrid(x, y, width, height int, scale float64, cards game.PileOfCards) *CardGrid {
+ w := &CardGrid{
+ WidgetBase: NewWidgetBase(x, y, width, height),
+ cards: cards,
+ scale: scale,
+ }
+ w.renderImpl = func() *ebiten.Image { return w.render() }
+ return w
+}
+
+func (w *CardGrid) Columns(columns int) *CardGrid {
+ if w.rows != 0 {
+ log.Fatalf("CardGrid can not have fixed columns and rows")
+ }
+ w.columns = columns
+ return w
+}
+
+func (w *CardGrid) Rows(rows int) *CardGrid {
+ if w.columns != 0 {
+ log.Fatalf("CardGrid can not have fixed columns and rows")
+ }
+ w.rows = rows
+ return w
+}
+
+func (w *CardGrid) render() *ebiten.Image {
+ img := ebiten.NewImage(w.Width, w.Height)
+
+ cards := w.cards.Cards()
+ nCards := len(cards)
+
+ if nCards == 0 {
+ return img
+ }
+
+ if nCards != w.gridCards {
+ if w.cardWidth == 0 && w.cardHeight == 0 {
+ cardImg := assets.GetCard(w.cards.Cards()[0].Path(), "en")
+ b := cardImg.Bounds()
+
+ // Fit the row height to the widgets height
+ if w.rows == 1 && w.scale == -1 {
+ w.scale = float64(w.Height) / float64(b.Dy())
+ }
+
+ w.cardWidth = float64(b.Dx()) * w.scale
+ w.cardHeight = float64(b.Dy()) * w.scale
+ }
+
+ var width, height int
+ if w.rows != 0 {
+ width = int(math.Ceil(float64(nCards)/float64(w.rows)) * w.cardWidth)
+ height = int(float64(w.rows) * w.cardHeight)
+ } else if w.columns != 0 {
+ width = int(float64(w.columns) * w.cardWidth)
+ height = int(math.Ceil(float64(nCards)/float64(w.columns)) * w.cardHeight)
+ } else {
+ log.Fatalf("CardGrid must have have fixed columns or rows")
+ }
+ w.gridCards = nCards
+ w.grid = ebiten.NewImage(width, height)
+
+ xOffset, yOffset := 0.0, 0.0
+ for i, card := range cards {
+ cardImg := assets.GetCard(card.Path(), "en")
+ if cardImg == nil {
+ log.Fatalf("No image found for %s", card.Path())
+ }
+
+ op := &ebiten.DrawImageOptions{}
+ op.GeoM.Scale(w.scale, w.scale)
+ op.GeoM.Translate(xOffset, yOffset)
+
+ if slices.Contains(w.highlights, card) {
+ op.ColorM.Scale(1, 0, 0, 0.5)
+ }
+
+ w.grid.DrawImage(cardImg, op)
+
+ columns := w.columns
+ if columns == 0 {
+ columns = nCards / w.rows
+ }
+
+ if i != 0 && i%columns == columns-1 {
+ xOffset = 0.0
+ yOffset += w.cardHeight
+ } else {
+ xOffset += w.cardWidth
+ }
+ }
+ }
+
+ op := &ebiten.DrawImageOptions{}
+ op.GeoM.Translate(float64(w.xOffset), float64(w.yOffset))
+ img.DrawImage(w.grid, op)
+ return img
+}
+
+func (w *CardGrid) FindObjectAt(_x, _y int) interface{} {
+ if !w.Contains(_x, _y) || w.grid == nil {
+ return nil
+ }
+
+ x, y := _x-w.X-w.xOffset, _y-w.Y-w.yOffset
+
+ gridBounds := w.grid.Bounds()
+ if x > gridBounds.Dx() || y > gridBounds.Dy() {
+ return nil
+ }
+
+ column := int(float64(x) / w.cardWidth)
+ row := int(float64(y) / w.cardHeight)
+ var i int
+ if w.rows != 0 {
+ i = row*w.cards.Size()/w.rows + column
+ } else {
+ i = row*w.columns + column
+ }
+
+ if i >= w.cards.Size() {
+ return nil
+ }
+
+ return w.cards.Cards()[i]
+}
+
+func (w *CardGrid) setHighlights(cards []*game.Card) {
+ w.highlights = cards
+ w.ForceRedraw()
+}
+
+func (w *CardGrid) HighlightCard(card *game.Card) {
+ w.setHighlights([]*game.Card{card})
+}
+
+func (w *CardGrid) AddHighlightCard(card *game.Card) {
+ w.setHighlights(append(w.highlights, card))
+}
+
+func (w *CardGrid) ClearHighlights() {
+ w.setHighlights([]*game.Card{})
+}
+
+func (w *CardGrid) Scroll(x, y int) {
+ w.xOffset += x * 10
+ if w.xOffset > 0 {
+ w.xOffset = 0
+ }
+
+ w.yOffset += y * 10
+ if w.yOffset > 0 {
+ w.yOffset = 0
+ }
+
+ w.ForceRedraw()
+}
diff --git a/go/ui/clickable.go b/go/ui/clickable.go
new file mode 100644
index 00000000..761b7534
--- /dev/null
+++ b/go/ui/clickable.go
@@ -0,0 +1,40 @@
+package ui
+
+import (
+ "github.com/hajimehoshi/ebiten/v2"
+)
+
+type Clickable interface {
+ Click(x, y int)
+}
+
+type ClickWrapper struct {
+ w Widget
+ onClick func(x, y int)
+}
+
+func NewClickWrapper(w Widget, onClick func(x, y int)) *ClickWrapper {
+ return &ClickWrapper{w, onClick}
+}
+
+func (w *ClickWrapper) Draw(screen *ebiten.Image) {
+ w.w.Draw(screen)
+}
+
+func (w *ClickWrapper) FindObjectAt(x, y int) interface{} {
+ return w.w.FindObjectAt(x, y)
+}
+
+func (w *ClickWrapper) Contains(x, y int) bool {
+ return w.w.Contains(x, y)
+}
+
+func (w *ClickWrapper) Click(x, y int) {
+ w.onClick(x, y)
+}
+
+func (w *ClickWrapper) Scroll(x, y int) {
+ if w, ok := w.w.(Scrollable); ok {
+ w.Scroll(x, y)
+ }
+}
diff --git a/go/ui/collection.go b/go/ui/collection.go
new file mode 100644
index 00000000..ac53ba8c
--- /dev/null
+++ b/go/ui/collection.go
@@ -0,0 +1,70 @@
+package ui
+
+import (
+ "github.com/hajimehoshi/ebiten/v2"
+ "github.com/hajimehoshi/ebiten/v2/inpututil"
+)
+
+type Collection struct {
+ widgets []Widget
+}
+
+func (c *Collection) Widgets() []Widget {
+ return c.widgets
+}
+
+func (c *Collection) Clear() {
+ c.widgets = []Widget{}
+}
+
+func (c *Collection) AddWidget(w Widget) {
+ c.widgets = append(c.widgets, w)
+}
+
+func (c *Collection) Draw(screen *ebiten.Image) {
+ for _, w := range c.widgets {
+ w.Draw(screen)
+ }
+}
+
+func (c *Collection) Update() error {
+ x, y := ebiten.CursorPosition()
+
+ var activeWidget Widget
+ for _, w := range c.widgets {
+ if !w.Contains(x, y) {
+ continue
+ }
+ activeWidget = w
+ break
+ }
+
+ if activeWidget == nil {
+ return nil
+ }
+
+ scrollX, scrollY := ebiten.Wheel()
+ if scrollX != 0 || scrollY != 0 {
+ if scrollable, ok := activeWidget.(Scrollable); ok {
+ scrollable.Scroll(int(scrollX), int(scrollY))
+ }
+ }
+
+ if textInput, ok := activeWidget.(*TextInput); ok {
+ // var input []rune
+ // ebiten.AppendInputChars(input)
+ input := ebiten.InputChars()
+ if len(input) > 0 {
+ textInput.AddInput(input)
+ }
+ textInput.HandleKey()
+ }
+
+ if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
+ if btn, ok := activeWidget.(Button); ok {
+ btn.Click(x, y)
+ }
+ }
+
+ return nil
+}
diff --git a/go/ui/colors.go b/go/ui/colors.go
index 3998e640..d8422479 100644
--- a/go/ui/colors.go
+++ b/go/ui/colors.go
@@ -5,6 +5,6 @@ import (
)
var (
- gray = color.RGBA{0x80, 0x80, 0x80, 0xff}
- resourceBg = color.RGBA{230, 180, 128, 0xff}
+ Gray = color.RGBA{0x80, 0x80, 0x80, 0xff}
+ ResourceBg = color.RGBA{230, 180, 128, 0xff}
)
diff --git a/go/ui/hand.go b/go/ui/hand.go
index f7157232..7c8feab8 100644
--- a/go/ui/hand.go
+++ b/go/ui/hand.go
@@ -1,13 +1,8 @@
package ui
import (
- "log"
-
"github.com/hajimehoshi/ebiten/v2"
- "muhq.space/muhqs-game/go/assets"
"muhq.space/muhqs-game/go/game"
-
- "golang.org/x/exp/slices"
)
type HandCard struct {
@@ -15,99 +10,24 @@ type HandCard struct {
}
type HandView struct {
- WidgetBase
- hand *game.Hand
- cardWidth float64
- cardHeight float64
- scale float64
- bottomAligned bool
- highlights []*game.Card
+ CardGrid
}
func NewHandView(x, y, width, height int, h *game.Hand, bottomAligned bool) *HandView {
- hv := &HandView{
- WidgetBase: NewWidgetBase(x, y, width, height),
- hand: h,
- cardWidth: 0,
- cardHeight: 0,
- scale: 0,
- bottomAligned: bottomAligned,
- highlights: []*game.Card{},
- }
+ hv := &HandView{CardGrid{
+ WidgetBase: NewWidgetBase(x, y, width, height),
+ cards: h,
+ rows: 1,
+ scale: -1,
+ }}
hv.renderImpl = func() *ebiten.Image { return hv.render() }
return hv
}
-func (hv *HandView) render() *ebiten.Image {
- img := ebiten.NewImage(hv.Width, hv.Height)
- handCards := hv.hand.Cards()
- nHandCards := len(handCards)
- for i, card := range handCards {
- cardImg := assets.GetCard(card.Path(), "en")
- if cardImg == nil {
- log.Fatalf("No image found for %s", card.Path())
- }
-
- if hv.scale == 0 {
- b := cardImg.Bounds()
- x_scale := (float64(hv.Width) / float64(nHandCards)) / float64(b.Dx())
- y_scale := float64(hv.Height) / float64(b.Dy())
- hv.scale = x_scale
- if y_scale < x_scale {
- hv.scale = y_scale
- }
- hv.cardWidth = float64(b.Dx()) * hv.scale
- hv.cardHeight = float64(b.Dy()) * hv.scale
- }
-
- yOffset := 0.0
- if hv.bottomAligned {
- yOffset = float64(hv.Height) - hv.cardHeight
- }
-
- op := &ebiten.DrawImageOptions{}
- op.GeoM.Scale(hv.scale, hv.scale)
- op.GeoM.Translate(float64(i)*hv.cardWidth, yOffset)
-
- if slices.Contains(hv.highlights, card) {
- op.ColorM.Scale(1, 0, 0, 0.5)
- }
-
- img.DrawImage(cardImg, op)
- }
- return img
-}
-
func (hv *HandView) FindObjectAt(x, y int) interface{} {
- if !hv.Contains(x, y) {
- return nil
+ if obj := hv.CardGrid.FindObjectAt(x, y); obj != nil {
+ return HandCard{obj.(*game.Card)}
}
- if hv.bottomAligned && float64(y-hv.Y) < float64(hv.Height)-hv.cardHeight {
- return nil
- }
-
- i := int(float64(x) / hv.cardWidth)
- if i >= hv.hand.Size() {
- return nil
- }
-
- return HandCard{hv.hand.Cards()[i]}
-}
-
-func (hv *HandView) setHighlights(cards []*game.Card) {
- hv.highlights = cards
- hv.ForceRedraw()
-}
-
-func (hv *HandView) HighlightCard(card *game.Card) {
- hv.setHighlights([]*game.Card{card})
-}
-
-func (hv *HandView) AddHighlightCard(card *game.Card) {
- hv.setHighlights(append(hv.highlights, card))
-}
-
-func (hv *HandView) ClearHighlights() {
- hv.setHighlights([]*game.Card{})
+ return nil
}
diff --git a/go/ui/stateBar.go b/go/ui/stateBar.go
index b698100a..93de9af9 100644
--- a/go/ui/stateBar.go
+++ b/go/ui/stateBar.go
@@ -33,7 +33,7 @@ func (sb *StateBar) phaseLabel() string {
func (sb *StateBar) Draw(screen *ebiten.Image) {
if sb.resourceView == nil {
sb.resourceView = NewFixedTextBox(sb.X, sb.Y,
- sb.Height*2, sb.Height, sb.resourceLabel()).Centering(true).Bg(resourceBg)
+ sb.Height*2, sb.Height, sb.resourceLabel()).Centering(true).Bg(ResourceBg)
}
sb.resourceView.Draw(screen)
diff --git a/go/ui/textBox.go b/go/ui/textBox.go
index 1c82a307..cc813a4e 100644
--- a/go/ui/textBox.go
+++ b/go/ui/textBox.go
@@ -127,6 +127,7 @@ func NewPocList(centerX, bottomY int, poc game.PileOfCards) *PocList {
bottomY: bottomY,
poc: poc,
}
+
w.renderImpl = func() *ebiten.Image {
w.setText()
return w.render()
@@ -135,6 +136,7 @@ func NewPocList(centerX, bottomY int, poc game.PileOfCards) *PocList {
}
func (w *PocList) setText() {
+ w.Width, w.Height = -1, -1
t := ""
for _, c := range w.poc.Cards() {
t = fmt.Sprintf("%s%s\n", t, c.Name)
@@ -188,11 +190,15 @@ func NewTextInput(x, y int, width, height int, label string) *TextInput {
return w
}
-func (ti *TextInput) AddInput(input []rune) {
- ti.input += string(input)
+func (ti *TextInput) SetInput(input string) {
+ ti.input = input
ti.ForceRedraw()
}
+func (ti *TextInput) AddInput(input []rune) {
+ ti.SetInput(ti.input + string(input))
+}
+
func (ti *TextInput) HandleKey() {
if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) {
if len(ti.input) > 0 {
diff --git a/go/ui/textInput.go b/go/ui/textInput.go
deleted file mode 100644
index 5b1faa29..00000000
--- a/go/ui/textInput.go
+++ /dev/null
@@ -1 +0,0 @@
-package ui