aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2025-09-08 22:32:22 +0200
committerFlorian Fischer <florian.fischer@muhq.space>2025-09-08 22:32:22 +0200
commitddd9cc924977201f7d2bc162c0d5fe4f86841ef1 (patch)
tree67ae8a7f34cd1e119b09e0c1925dca52855b70c4
parent6438cef725e6310c49b5f4ef615b2ee82261b11b (diff)
downloadmuhqs-game-ddd9cc924977201f7d2bc162c0d5fe4f86841ef1.tar.gz
muhqs-game-ddd9cc924977201f7d2bc162c0d5fe4f86841ef1.zip
support action targeting from the stack buffer
-rw-r--r--README.md3
-rw-r--r--go/client/game.go37
-rw-r--r--go/ui/buffer.go75
3 files changed, 97 insertions, 18 deletions
diff --git a/README.md b/README.md
index 07fd006a..7a4577ee 100644
--- a/README.md
+++ b/README.md
@@ -120,8 +120,7 @@ Things that should be done eventually:
- [ ] implement game log
- [ ] implement log with hoverable components
- [ ] support big card grids (for sealed)
- - [ ] implement declared action targeting
- - [ ] implement action targeting
+ - [X] implement declared action targeting
- [ ] Game Logic
- [ ] implement constrained X-Cost
- [ ] implement HandCardSelection with condition
diff --git a/go/client/game.go b/go/client/game.go
index 5ab70df2..8a3bcae3 100644
--- a/go/client/game.go
+++ b/go/client/game.go
@@ -62,7 +62,7 @@ type Game struct {
storeButton *ui.SimpleButton
cardsInStore int
- stackBuffer *ui.Buffer
+ stackBuffer *ui.StackBuffer
prompt *ui.Prompt
messageBuffer *ui.Buffer
triggers []*game.TriggeredAction
@@ -132,10 +132,6 @@ func newGame(app *app, gameState game.State) *Game {
g.reset()
})
- g.stackBuffer = ui.NewBuffer(
- g.Width-STACK_BUFFER_WIDTH, g.passButton.Y-400,
- STACK_BUFFER_WIDTH, STACK_BUFFER_HEIGHT)
-
g.messageBuffer = ui.NewBuffer(
MESSAGE_BUFFER_MARGIN,
g.Height-DEFAULT_BUTTON_HEIGHT-MESSAGE_BUFFER_HEIGHT-MESSAGE_BUFFER_MARGIN,
@@ -272,6 +268,21 @@ func (g *Game) showWinners() {
})
}
+func (g *Game) updateStack() {
+ if g.visible(g.stackBuffer) {
+ g.hide(g.stackBuffer)
+ }
+
+ if !g.gameState.Stack().IsEmpty() {
+ g.stackBuffer = ui.NewStackBuffer(
+ g.Width-STACK_BUFFER_WIDTH, g.passButton.Y-400,
+ STACK_BUFFER_WIDTH, STACK_BUFFER_HEIGHT,
+ g.gameState.Stack().Actions)
+
+ g.show(g.stackBuffer)
+ }
+}
+
func (g *Game) showStores() {
if g.storesVisible {
return
@@ -468,6 +479,8 @@ func (g *Game) addHighlight(obj any, color color.Color) {
g.mapView.AddHighlightTile(obj, color)
case *game.Unit:
g.mapView.AddHighlightPermanent(obj, color)
+ case game.Action:
+ g.stackBuffer.AddHighlight(obj, color)
default:
log.Panicf("Unhandled highlight of type %T", obj)
}
@@ -488,6 +501,9 @@ func (g *Game) clearMapHighlights() {
func (g *Game) clearHighlights() {
g.clearMapHighlights()
g.handLayer.ClearHighlights()
+ if g.stackBuffer != nil {
+ g.stackBuffer.ClearHighlights()
+ }
}
func (g *Game) declareAction(a game.Action) {
@@ -610,18 +626,11 @@ func (g *Game) handlePlayerNotifications() {
if n.Error != nil {
log.Fatal(n.Error)
}
- a := n.Context.(game.Action)
- g.stackBuffer.AddLine(a.String())
- if !g.gameState.Stack().IsEmpty() && !g.visible(g.stackBuffer) {
- g.AddWidget(g.stackBuffer)
- }
+ g.updateStack()
case game.ResolvedActionNotification:
g.clearMapHighlights()
- g.stackBuffer.RemoveLast()
- if g.gameState.Stack().IsEmpty() {
- g.RemoveWidget(g.stackBuffer)
- }
+ g.updateStack()
g.mapView.ForceRedraw()
case game.TargetSelectionPrompt:
diff --git a/go/ui/buffer.go b/go/ui/buffer.go
index d2a4667e..c44bbdd0 100644
--- a/go/ui/buffer.go
+++ b/go/ui/buffer.go
@@ -8,6 +8,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/text/v2"
"muhq.space/muhqs-game/go/font"
+ "muhq.space/muhqs-game/go/game"
)
var (
@@ -18,6 +19,7 @@ var (
type Buffer struct {
WidgetBase
lines []string
+ highlights map[int]color.Color
pos int
font *text.GoTextFace
lineSpacing float64
@@ -30,6 +32,7 @@ func NewBuffer(x, y int, width, height int) *Buffer {
b := &Buffer{
NewWidgetBase(x, y, width, height),
[]string{},
+ make(map[int]color.Color),
0,
font.Font18,
1.5,
@@ -45,14 +48,18 @@ func (b *Buffer) render() *ebiten.Image {
img := ebiten.NewImage(b.Width, b.Height)
img.Fill(b.bg)
var y float64 = -b.font.Size
- for _, line := range b.lines[b.pos:] {
+ for i, line := range b.lines[b.pos:] {
_, h := text.Measure(line, b.font, b.font.Size*b.lineSpacing)
y += h
if y > float64(b.Height) {
break
}
op := &text.DrawOptions{}
- op.ColorScale.ScaleWithColor(b.fg)
+ if clr, highlighted := b.highlights[i]; highlighted {
+ op.ColorScale.ScaleWithColor(clr)
+ } else {
+ op.ColorScale.ScaleWithColor(b.fg)
+ }
op.GeoM.Translate(b.xMargin, float64(y))
op.LineSpacing = b.font.Size * b.lineSpacing
text.Draw(img, line, b.font, op)
@@ -129,3 +136,67 @@ func (b *Buffer) Fg(fg color.Color) *Buffer {
b.fg = fg
return b
}
+
+type StackBuffer struct {
+ Buffer
+ actions []game.Action
+}
+
+func NewStackBuffer(x, y int, width, height int, actions []game.Action) *StackBuffer {
+ sb := &StackBuffer{Buffer: *NewBuffer(x, y, width, height)}
+ // This is needed because we do not use the pointer created in NewBuffer
+ sb.renderImpl = func() *ebiten.Image { return sb.render() }
+
+ for _, action := range actions {
+ sb.AddAction(action)
+ }
+ return sb
+}
+
+func (b *StackBuffer) AddAction(a game.Action) {
+ b.AddLine(a.String())
+ b.actions = append(b.actions, a)
+}
+
+func (b *StackBuffer) AddHighlight(a game.Action, clr color.Color) {
+ for i, ba := range b.actions {
+ if a == ba {
+ b.highlights[i] = clr
+ }
+ }
+ b.ForceRedraw()
+}
+
+func (b *StackBuffer) ClearHighlights() {
+ b.highlights = make(map[int]color.Color)
+ b.ForceRedraw()
+}
+
+func (b *StackBuffer) RemoveLast() {
+ b.Buffer.RemoveLast()
+
+ // Remove potential highlight
+ i := len(b.actions) - 1
+ if _, found := b.highlights[i]; found {
+ delete(b.highlights, i)
+ }
+
+ b.actions = b.actions[:i]
+}
+
+func (b *StackBuffer) FindObjectAt(x, y int) any {
+ if !b.Contains(x, y) {
+ return nil
+ }
+
+ var _y float64 = 0
+ for i, line := range b.lines[b.pos:] {
+ _, h := text.Measure(line, b.font, b.font.Size*b.lineSpacing)
+ _y += h
+
+ if int(_y)+b.Y >= y {
+ return b.actions[i]
+ }
+ }
+ return nil
+}