diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2022-12-27 14:29:39 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:46:38 +0200 |
| commit | d6b5eeaa5e639c5ab89b5e1c93075a1b765794dc (patch) | |
| tree | f87422bb91c2197f88aa8d29092592858764049d /go/ui | |
| parent | 3c2ee4ee48480a28dc606878503f314ec8862d45 (diff) | |
| download | muhqs-game-d6b5eeaa5e639c5ab89b5e1c93075a1b765794dc.tar.gz muhqs-game-d6b5eeaa5e639c5ab89b5e1c93075a1b765794dc.zip | |
intermediate commit
Diffstat (limited to 'go/ui')
| -rw-r--r-- | go/ui/buffer.go | 66 | ||||
| -rw-r--r-- | go/ui/button.go | 60 | ||||
| -rw-r--r-- | go/ui/cardView.go | 46 | ||||
| -rw-r--r-- | go/ui/choice.go | 72 | ||||
| -rw-r--r-- | go/ui/colors.go | 7 | ||||
| -rw-r--r-- | go/ui/hand.go | 113 | ||||
| -rw-r--r-- | go/ui/mapView.go | 271 | ||||
| -rw-r--r-- | go/ui/prompt.go | 78 | ||||
| -rw-r--r-- | go/ui/scrollable.go | 5 | ||||
| -rw-r--r-- | go/ui/stateBar.go | 65 | ||||
| -rw-r--r-- | go/ui/textBox.go | 161 | ||||
| -rw-r--r-- | go/ui/widget.go | 47 |
12 files changed, 991 insertions, 0 deletions
diff --git a/go/ui/buffer.go b/go/ui/buffer.go new file mode 100644 index 00000000..c451d7fc --- /dev/null +++ b/go/ui/buffer.go @@ -0,0 +1,66 @@ +package ui + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text" + + "muhq.space/muhqs-game/go/font" +) + +var ( + BUFFER_BACKGROUND = gray + BUFFER_FOREGROUND = color.White +) + +type Buffer struct { + WidgetBase + lines []string + pos int +} + +func NewBuffer(x, y int, width, height int) *Buffer { + b := &Buffer{NewWidgetBase(x, y, width, height), []string{}, 0} + b.renderImpl = func() *ebiten.Image { return b.render() } + return b +} + +func (b *Buffer) render() *ebiten.Image { + img := ebiten.NewImage(b.Width, b.Height) + img.Fill(BUFFER_BACKGROUND) + y := 0 + for _, line := range b.lines[b.pos:] { + bounds := text.BoundString(font.Font18, line) + y += bounds.Dy() + if y > b.Height { + break + } + text.Draw(img, line, font.Font18, 0, y, BUFFER_FOREGROUND) + } + return img +} + +func (b *Buffer) AddLines(lines []string) { + b.lines = append(b.lines, lines...) + b.ForceRedraw() +} + +func (b *Buffer) AddLine(line string) { + b.AddLines([]string{line}) +} + +func (b *Buffer) ClearLines() { + b.lines = []string{} + b.ForceRedraw() +} + +func (b *Buffer) RemoveLast() { + b.lines = b.lines[:len(b.lines)-1] + b.ForceRedraw() +} + +func (b *Buffer) Remove(idx int) { + b.lines = append(b.lines[:idx], b.lines[idx+1:]...) + b.ForceRedraw() +} diff --git a/go/ui/button.go b/go/ui/button.go new file mode 100644 index 00000000..6454ccac --- /dev/null +++ b/go/ui/button.go @@ -0,0 +1,60 @@ +package ui + +import ( + "image/color" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/text" + + "muhq.space/muhqs-game/go/font" +) + +type Button interface { + Widget + Click(x, y int) +} + +type SimpleButton struct { + WidgetBase + label string + OnClick func(b *SimpleButton) + BG color.Color + FG color.Color +} + +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.renderImpl = func() *ebiten.Image { return sb.render() } + return sb +} + +func (b *SimpleButton) render() *ebiten.Image { + img := ebiten.NewImage(b.Width, b.Height) + + ebitenutil.DrawRect(img, float64(0), float64(0), float64(b.Width), float64(b.Height), b.BG) + + bounds := text.BoundString(font.Font24, b.label) + x := (b.Width - bounds.Dx()) / 2 + if x < 0 { + x = 0 + } + + y := b.Height - bounds.Dy()/2 + if y < 0 { + y = 0 + } + text.Draw(img, b.label, font.Font24, x, y, b.FG) + 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/cardView.go b/go/ui/cardView.go new file mode 100644 index 00000000..a1c004c5 --- /dev/null +++ b/go/ui/cardView.go @@ -0,0 +1,46 @@ +package ui + +import ( + "github.com/hajimehoshi/ebiten/v2" + "muhq.space/muhqs-game/go/assets" +) + +type CardView struct { + WidgetBase + cardPath string +} + +func NewScaledCardView(x, y int, width, height int, cardPath string) *CardView { + cv := &CardView{NewWidgetBase(x, y, width, height), cardPath} + cv.renderImpl = func() *ebiten.Image { return cv.render() } + return cv +} + +func NewAutoCardView(x, y int, cardPath string) *CardView { + cv := &CardView{NewWidgetBase(x, y, -1, -1), cardPath} + cv.renderImpl = func() *ebiten.Image { return cv.render() } + return cv +} + +func (cv *CardView) render() *ebiten.Image { + card := assets.GetCard(cv.cardPath, "en") + op := &ebiten.DrawImageOptions{} + + b := card.Bounds() + if cv.Width == -1 && cv.Height == -1 { + cv.Width = b.Dx() + cv.Height = b.Dy() + } else { + x_scale := float64(cv.Width) / float64(b.Dx()) + y_scale := float64(cv.Height) / float64(b.Dy()) + scale := x_scale + if y_scale < x_scale { + scale = y_scale + } + op.GeoM.Scale(scale, scale) + } + + img := ebiten.NewImage(cv.Width, cv.Height) + img.DrawImage(card, op) + return img +} diff --git a/go/ui/choice.go b/go/ui/choice.go new file mode 100644 index 00000000..003c2a6a --- /dev/null +++ b/go/ui/choice.go @@ -0,0 +1,72 @@ +package ui + +import ( + "github.com/hajimehoshi/ebiten/v2" + "muhq.space/muhqs-game/go/game" +) + +type Choice struct { + X, Y int + Width, choiceHeight int + choices []string + onClick func(*Choice, int, int) + buttons []Button +} + +func choiceButtonOnClickDummy(b *SimpleButton) {} + +func NewChoice(x, y int, width, choiceHeight int, choices []string, + onClick func(*Choice, int, int), +) *Choice { + c := &Choice{x, y, width, choiceHeight, choices, onClick, []Button{}} + + for i, choice := range c.choices { + b := NewSimpleButton(c.X, c.Y+i*c.choiceHeight, c.Width, c.choiceHeight, choice, + choiceButtonOnClickDummy) + + c.buttons = append(c.buttons, b) + } + return c +} + +func (c *Choice) Draw(screen *ebiten.Image) { + for _, b := range c.buttons { + b.Draw(screen) + } +} + +func (c *Choice) GetChoosen(x, y int) int { + return (y - c.Y) / c.choiceHeight +} + +func (c *Choice) Click(x, y int) { + c.onClick(c, x, y) +} + +func (c *Choice) Contains(x, y int) bool { + height := len(c.choices) * c.choiceHeight + 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 NewActionChoice(x, y int, actions []game.Action, onClick func(*Choice, int, int)) *Choice { + // TODO: use dynamic with width and height + width := 200 + choiceHeight := 40 + labels := make([]string, 0, len(actions)) + for _, action := range actions { + labels = append(labels, action.String()) + } + + return NewChoice(x, y, width, choiceHeight, labels, onClick) +} diff --git a/go/ui/colors.go b/go/ui/colors.go new file mode 100644 index 00000000..a45aa16b --- /dev/null +++ b/go/ui/colors.go @@ -0,0 +1,7 @@ +package ui + +import ( + "image/color" +) + +var gray = color.RGBA{0x80, 0x80, 0x80, 0xff} diff --git a/go/ui/hand.go b/go/ui/hand.go new file mode 100644 index 00000000..f7157232 --- /dev/null +++ b/go/ui/hand.go @@ -0,0 +1,113 @@ +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 { + C *game.Card +} + +type HandView struct { + WidgetBase + hand *game.Hand + cardWidth float64 + cardHeight float64 + scale float64 + bottomAligned bool + highlights []*game.Card +} + +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.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 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{}) +} diff --git a/go/ui/mapView.go b/go/ui/mapView.go new file mode 100644 index 00000000..8c83bd41 --- /dev/null +++ b/go/ui/mapView.go @@ -0,0 +1,271 @@ +package ui + +import ( + "fmt" + "log" + "math" + "strings" + "unicode" + + "golang.org/x/exp/slices" + + "github.com/hajimehoshi/ebiten/v2" + "muhq.space/muhqs-game/go/assets" + "muhq.space/muhqs-game/go/game" +) + +const ( + PERMANENT_WIDTH int = 40 + PERMANENT_HEIGHT int = 40 + + TILE_WIDTH int = 50 + TILE_HEIGHT int = 50 +) + +type MapView struct { + gameState *game.State + mapLayer *ebiten.Image + permanentsLayer *ebiten.Image + tileHighlights []game.Position + permanentsHighlights []game.Permanent +} + +func NewMapView(g *game.State) *MapView { + vw := MapView{g, nil, nil, []game.Position{}, []game.Permanent{}} + return &vw +} + +func (mv *MapView) Height() int { + return len(mv.gameState.Map.Tiles) * TILE_HEIGHT +} + +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, + // the origin point is the upper-left corner. + op.GeoM.Translate(-float64(TILE_WIDTH)/2, -float64(TILE_HEIGHT)/2) + + // Rotate the image. As a result, the anchor point of this rotate is + // the center of the image. + op.GeoM.Rotate(radian) + + // Reset translation + op.GeoM.Translate(float64(TILE_WIDTH)/2, float64(TILE_HEIGHT)/2) +} + +func (vw *MapView) rotateStreet(x, y int, op *ebiten.DrawImageOptions) { + connections, left, right, above, below := vw.gameState.Map.FindStreetConnections(x, y) + + if connections == 0 { + // This street is not connected to another street -> + // check any other non neutral tiles + connections, left, right, above, below = vw.gameState.Map.FindAnyConnections(x, y) + } + + // This street is not connected to anything. Seams odd! + if connections == 0 { + log.Printf("Street at (%d, %d) is not connected", x, y) + } else if (connections == 1 || + (connections == 2 && ((left && right) || (above && below)))) && + (above || below) { + rotateStreetImg(x, y, math.Pi/2, op) + } else if connections == 2 { + // normal orientation above and right + if right && below { + rotateStreetImg(x, y, 3*math.Pi/2, op) + } else if left && below { + rotateStreetImg(x, y, math.Pi, op) + } else if above && left { + rotateStreetImg(x, y, math.Pi/2, op) + } + } else if connections == 3 { + // normal orientation left above right + if above && right && below { + rotateStreetImg(x, y, 3*math.Pi/2, op) + } else if left && below && right { + rotateStreetImg(x, y, math.Pi, op) + } else if below && left && above { + rotateStreetImg(x, y, math.Pi/2, op) + } + } +} + +func (vw *MapView) newLayerImage() *ebiten.Image { + // TODO: support non symetric maps + maxWidth := len(vw.gameState.Map.Tiles[0]) * TILE_WIDTH + maxHeight := len(vw.gameState.Map.Tiles) * TILE_HEIGHT + return ebiten.NewImage(maxWidth, maxHeight) +} + +func (vw *MapView) drawMapLayer(screen *ebiten.Image) { + if vw.mapLayer == nil { + vw.mapLayer = vw.newLayerImage() + + x_px, y_px := 0.0, 0.0 + for y := 0; y < len(vw.gameState.Map.Tiles); y++ { + for x := 0; x < len(vw.gameState.Map.Tiles[y]); x++ { + pos := game.Position{X: x, Y: y} + tile := vw.gameState.Map.TileAt(pos) + tileImg := assets.GetTile(tile.Raw) + + op := &ebiten.DrawImageOptions{} + // Rotate Street + if tile.Type == game.TileTypes.Street { + vw.rotateStreet(x, y, op) + } + + op.GeoM.Translate(x_px, y_px) + + if slices.Contains(vw.tileHighlights, pos) { + op.ColorM.Scale(1, 0, 0, 0.5) + } + + vw.mapLayer.DrawImage(tileImg, op) + + x_px += float64(TILE_WIDTH) + } + x_px = 0 + y_px += float64(TILE_HEIGHT) + } + } + + screen.DrawImage(vw.mapLayer, &ebiten.DrawImageOptions{}) +} + +func getPermanentSymbol(p game.Permanent, i int) *ebiten.Image { + permanentSymbol := assets.GetSymbol(strings.ToLower(p.Card().Name)) + if permanentSymbol == nil { + symbol := fmt.Sprintf("%c%d", unicode.ToUpper(rune(p.Card().Type.String()[0])), i) + permanentSymbol = assets.GetGenericSymbol(symbol) + if permanentSymbol == nil { + log.Fatalf("Failed to generate generic symbol %s", symbol) + } + } + + return permanentSymbol +} + +func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) { + if vw.permanentsLayer == nil { + vw.permanentsLayer = vw.newLayerImage() + + for i, p := range vw.gameState.Permanents { + permanentSymbol := getPermanentSymbol(p, i) + t := p.Tile() + x_px := t.Position.X*TILE_WIDTH + (TILE_WIDTH-PERMANENT_WIDTH)/2 + y_px := t.Position.Y*TILE_HEIGHT + (TILE_HEIGHT-PERMANENT_HEIGHT)/2 + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(x_px), float64(y_px)) + op.ColorM.ScaleWithColor(p.Controller().Color) + + for _, h := range vw.permanentsHighlights { + if p == h { + op.ColorM.Scale(255, 0.5, 0.5, 1) + break + } + } + + vw.permanentsLayer.DrawImage(permanentSymbol, op) + } + } + + screen.DrawImage(vw.permanentsLayer, &ebiten.DrawImageOptions{}) +} + +func (vw *MapView) Draw(screen *ebiten.Image) { + vw.drawMapLayer(screen) + vw.drawPermanentsLayer(screen) +} + +func (vw *MapView) ForceRedraw() { + vw.mapLayer = nil + vw.permanentsLayer = nil +} + +func isSymbolTransparentAt(p game.Permanent, relativeX, relativeY int) bool { + permanentSymbol := getPermanentSymbol(p, 0) + _, _, _, alpha := permanentSymbol.At(relativeX, relativeY).RGBA() + return alpha == 0 +} + +func (vw *MapView) FindObjectAt(screenX, screenY int) interface{} { + x := screenX / TILE_WIDTH + y := screenY / TILE_HEIGHT + + if x < 0 || y < 0 { + return nil + } + + relativeX := screenX % TILE_WIDTH + relativeY := screenY % TILE_HEIGHT + xMargin := (TILE_WIDTH - PERMANENT_WIDTH) / 2 + yMargin := (TILE_HEIGHT - PERMANENT_HEIGHT) / 2 + // detect if a permanent or the containing tile was selected + if relativeX >= xMargin && relativeX <= TILE_WIDTH-xMargin && + relativeY >= yMargin && relativeY <= TILE_HEIGHT-yMargin { + + for _, p := range vw.gameState.Permanents { + pos := p.Tile().Position + if pos.X == x && pos.Y == y { + if isSymbolTransparentAt(p, relativeX, relativeY) { + break + } + + return p + } + } + } + + if y < len(vw.gameState.Map.Tiles) && x < len(vw.gameState.Map.Tiles[y]) { + return &vw.gameState.Map.Tiles[y][x] + } + + return nil +} + +func (vw *MapView) HighlightPositions(pos []game.Position) { + vw.tileHighlights = pos + vw.ForceRedraw() +} + +func (vw *MapView) AddHighlightPosition(pos game.Position) { + vw.HighlightPositions(append(vw.tileHighlights, pos)) +} + +func (vw *MapView) HighlightTiles(tiles []*game.Tile) { + pos := make([]game.Position, 0, len(tiles)) + for _, t := range tiles { + pos = append(pos, t.Position) + } + vw.HighlightPositions(pos) +} + +func (vw *MapView) HighlightTile(t *game.Tile) { + vw.HighlightTiles([]*game.Tile{t}) +} + +func (vw *MapView) AddHighlightTile(t *game.Tile) { + vw.AddHighlightPosition(t.Position) +} + +func (vw *MapView) ClearTileHighlights() { + vw.HighlightTiles([]*game.Tile{}) +} + +func (vw *MapView) HighlightPermanents(permanents []game.Permanent) { + vw.permanentsHighlights = permanents + vw.ForceRedraw() +} + +func (vw *MapView) HighlightPermanent(p game.Permanent) { + vw.HighlightPermanents([]game.Permanent{p}) +} + +func (vw *MapView) ClearPermanentsHighlights() { + vw.HighlightPermanents([]game.Permanent{}) +} + +func (*MapView) Render(Widget) *ebiten.Image { + return nil +} diff --git a/go/ui/prompt.go b/go/ui/prompt.go new file mode 100644 index 00000000..bfcd5d14 --- /dev/null +++ b/go/ui/prompt.go @@ -0,0 +1,78 @@ +package ui + +import ( + "log" + + "github.com/hajimehoshi/ebiten/v2" + + "muhq.space/muhqs-game/go/game" +) + +type promptType int + +const ( + handCard promptType = iota + + PROMPT_HEIGHT int = 50 +) + +type Prompt struct { + promptType promptType + y, width int + sel []interface{} + components []Widget +} + +func NewHandCardPrompt(y, width int, promptText string) *Prompt { + p := &Prompt{ + y: y, + width: width, + promptType: handCard, + components: []Widget{NewCenteringFixedTextBox(0, y, width, PROMPT_HEIGHT, promptText)}, + } + return p +} + +func (p *Prompt) Add(obj interface{}) { + var ok bool + switch p.promptType { + case handCard: + _, ok = obj.(HandCard) + } + + if !ok { + log.Fatalf("Invalid type %T for prompt %d", obj, p.promptType) + } + + p.sel = append(p.sel, obj) +} + +func (p *Prompt) Confirm() game.Action { + switch p.promptType { + case handCard: + cards := make([]*game.Card, 0, len(p.sel)) + for _, c := range p.sel { + cards = append(cards, c.(HandCard).C) + } + return game.NewHandCardSelection(cards) + } + + return nil +} + +func (p *Prompt) Height() int { + return PROMPT_HEIGHT +} + +func (p *Prompt) Draw(screen *ebiten.Image) { + for _, comp := range p.components { + comp.Draw(screen) + } +} + +func (p *Prompt) Contains(x, y int) bool { + return x >= 0 && y >= 0 && x <= p.width && y >= p.y && y <= p.y+PROMPT_HEIGHT +} + +func (p *Prompt) FindObjectAt(screenX, screenY int) interface{} { return nil } +func (*Prompt) Render(Widget) *ebiten.Image { return nil } diff --git a/go/ui/scrollable.go b/go/ui/scrollable.go new file mode 100644 index 00000000..90e0a3a5 --- /dev/null +++ b/go/ui/scrollable.go @@ -0,0 +1,5 @@ +package ui + +type Scrollable interface { + Scroll(x, y int) +} diff --git a/go/ui/stateBar.go b/go/ui/stateBar.go new file mode 100644 index 00000000..6914de6e --- /dev/null +++ b/go/ui/stateBar.go @@ -0,0 +1,65 @@ +package ui + +import ( + "fmt" + + "github.com/hajimehoshi/ebiten/v2" + "muhq.space/muhqs-game/go/game" +) + +type StateBar struct { + X, Y int + Width, Height int + gameState *game.State + activePlayer *game.Player + resourceView *TextBox + phaseView *TextBox +} + +func NewStateBar(x, y int, width, height int, s *game.State, p *game.Player) *StateBar { + sb := &StateBar{x, y, width, height, s, p, nil, nil} + return sb +} + +func (sb *StateBar) resourceLabel() string { + return fmt.Sprintf("%d", sb.activePlayer.Resource) +} + +func (sb *StateBar) phaseLabel() string { + return fmt.Sprintf("%s: %s", sb.gameState.ActivePlayer.Name, + sb.gameState.ActivePhase.String()) +} + +func (sb *StateBar) Draw(screen *ebiten.Image) { + if sb.resourceView == nil { + sb.resourceView = NewCenteringFixedTextBox(sb.X, sb.Y, sb.Height, sb.Height, + sb.resourceLabel()) + } + sb.resourceView.Draw(screen) + + if sb.phaseView == nil { + sb.phaseView = NewFixedTextBox(sb.X+sb.resourceView.Width, sb.Y, + sb.Width-sb.resourceView.Width, sb.Height, sb.phaseLabel(), false) + } + sb.phaseView.Draw(screen) +} + +func (sb *StateBar) FindObjectAt(x, y int) interface{} { + return nil +} + +func (sb *StateBar) ForceRedraw() { + if sb.resourceView != nil { + sb.resourceView.Text = sb.resourceLabel() + sb.resourceView.ForceRedraw() + } + + if sb.phaseView != nil { + sb.phaseView.Text = sb.phaseLabel() + sb.phaseView.ForceRedraw() + } +} + +func (sb *StateBar) Contains(x, y int) bool { + return sb.resourceView.Contains(x, y) || sb.phaseView.Contains(x, y) +} diff --git a/go/ui/textBox.go b/go/ui/textBox.go new file mode 100644 index 00000000..78199410 --- /dev/null +++ b/go/ui/textBox.go @@ -0,0 +1,161 @@ +package ui + +import ( + "fmt" + "image/color" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/text" + "muhq.space/muhqs-game/go/font" + "muhq.space/muhqs-game/go/game" +) + +var ( + TEXTBOX_DEFAULT_BACKGROUND = gray + TEXTBOX_DEFAULT_FOREGROUND = color.White + + TEXTBOX_DEFAULT_MARGIN = 3 +) + +type TextBox struct { + WidgetBase + Text string + Fg color.Color + Bg color.Color + XMargin int + YMargin int + Center bool +} + +func NewFixedTextBox(x, y int, width, height int, t string, center bool) *TextBox { + tb := &TextBox{ + WidgetBase: NewWidgetBase(x, y, width, height), + Text: t, + Fg: TEXTBOX_DEFAULT_FOREGROUND, + Bg: TEXTBOX_DEFAULT_BACKGROUND, + XMargin: TEXTBOX_DEFAULT_MARGIN, + YMargin: TEXTBOX_DEFAULT_MARGIN, + Center: center, + } + tb.renderImpl = func() *ebiten.Image { return tb.render() } + return tb +} + +func NewCenteringFixedTextBox(x, y int, width, height int, t string) *TextBox { + return NewFixedTextBox(x, y, width, height, t, true) +} + +func NewAutoTextBox(x, y int, t string) *TextBox { + return NewFixedTextBox(x, y, -1, -1, t, false) +} + +func NewCenteringAutoTextBox(x, y int, t string) *TextBox { + return NewFixedTextBox(x, y, -1, -1, t, true) +} + +func NewUnitInfo(x, y int, u *game.Unit) *TextBox { + info := fmt.Sprintf("%s\nDamage: %d\nHealth: %d\nUpkeep: %d", + u.String(), u.Damage(), u.Health, u.Upkeep) + + if u.Movement != game.INVALID_MOVEMENT() { + info = fmt.Sprintf("%s\nMovement: %s", info, u.Movement.String()) + } + + if u.Attack != game.INVALID_ATTACK() { + info = fmt.Sprintf("%s\nAttack: %s", info, u.Attack.String()) + } + + return NewAutoTextBox(x, y, info) +} + +func NewPermInfo(x, y int, p game.Permanent) *TextBox { + info := fmt.Sprintf("%s\nDamage: %d", p.String(), p.Damage()) + return NewAutoTextBox(x, y, info) +} + +func (tb *TextBox) render() *ebiten.Image { + var x, y int + b := text.BoundString(font.Font24, tb.Text) + if tb.Width == -1 || tb.Height == -1 { + tb.Width = b.Dx() + 2*tb.XMargin + tb.Height = b.Dy() + 2*tb.YMargin + x = tb.XMargin + b.Min.X + y = tb.YMargin - b.Min.Y + } else { + if !tb.Center { + x = tb.XMargin + b.Min.X + } else { + x = (tb.Width - b.Dx()) / 2 + if x < 0 { + x = 0 + } + } + + y = tb.Height - b.Dy()/2 + if y < 0 { + y = 0 + } + } + + img := ebiten.NewImage(tb.Width, tb.Height) + img.Fill(tb.Bg) + text.Draw(img, tb.Text, font.Font24, x, y, tb.Fg) + return img +} + +type PocList struct { + TextBox + centerX int + bottomY int + poc game.PileOfCards +} + +func NewPocList(centerX, bottomY int, poc game.PileOfCards) *PocList { + w := &PocList{ + TextBox: TextBox{ + WidgetBase: NewWidgetBase(-1, -1, -1, -1), + Text: "", + Fg: TEXTBOX_DEFAULT_FOREGROUND, + Bg: TEXTBOX_DEFAULT_BACKGROUND, + XMargin: TEXTBOX_DEFAULT_MARGIN, + YMargin: TEXTBOX_DEFAULT_MARGIN, + Center: true, + }, + centerX: centerX, + bottomY: bottomY, + poc: poc, + } + w.renderImpl = func() *ebiten.Image { + w.setText() + return w.render() + } + return w +} + +func (w *PocList) setText() { + t := "" + for _, c := range w.poc.Cards() { + t = fmt.Sprintf("%s%s\n", t, c.Name) + } + + if t != "" { + w.Text = t[:len(t)-1] + } else { + w.Text = "No cards" + } + + b := text.BoundString(font.Font24, w.Text) + w.X = w.centerX - b.Dx()/2 - w.XMargin + w.Y = w.bottomY - b.Dy() - w.YMargin +} + +func (w *PocList) FindObjectAt(x, y int) interface{} { + cards := w.poc.Cards() + if len(cards) == 0 { + return nil + } + + // b := text.BoundString(font.Font, w.Text) + // i := b.Dy() / w.poc.Size() + return cards[0] +} diff --git a/go/ui/widget.go b/go/ui/widget.go new file mode 100644 index 00000000..cfcdd766 --- /dev/null +++ b/go/ui/widget.go @@ -0,0 +1,47 @@ +package ui + +import ( + "github.com/hajimehoshi/ebiten/v2" +) + +type Widget interface { + Draw(*ebiten.Image) + FindObjectAt(int, int) interface{} + Contains(int, int) bool +} + +type WidgetBase struct { + X, Y int + Width, Height int + img *ebiten.Image + 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} +} + +func (w *WidgetBase) Draw(screen *ebiten.Image) { + if w.img == nil { + w.img = w.renderImpl() + } + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(w.X), float64(w.Y)) + screen.DrawImage(w.img, op) +} + +func (w *WidgetBase) ForceRedraw() { + w.img = nil +} + +func (w *WidgetBase) Contains(x, y int) bool { + return x >= 0 && y >= 0 && x >= w.X && x <= w.X+w.Width && y >= w.Y && y <= w.Y+w.Height +} + +func (w *WidgetBase) FindObjectAt(int, int) interface{} { + return nil +} |
