aboutsummaryrefslogtreecommitdiff
path: root/go
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-02-04 18:51:31 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:47 +0100
commite9070ef3752c5d2ab1f9faac48095995772ce726 (patch)
treed1842db95e73039224476d0a5794d951e3fdfb3b /go
parentea54efacae089aa3644cfe00c8e180d5cc2c8d9e (diff)
downloadmuhqs-game-e9070ef3752c5d2ab1f9faac48095995772ce726.tar.gz
muhqs-game-e9070ef3752c5d2ab1f9faac48095995772ce726.zip
improve permanent pile support
* Fix alot of bugs caused by permanents without tiles * Implement a permanent choice to select the permanents in a pile * Fix how pile hooks are called
Diffstat (limited to 'go')
-rw-r--r--go/client/game.go46
-rw-r--r--go/game/action.go12
-rw-r--r--go/game/ai.go6
-rw-r--r--go/game/cardImplementations.go2
-rw-r--r--go/game/equipment.go3
-rw-r--r--go/game/permanent.go19
-rw-r--r--go/game/state.go12
-rw-r--r--go/game/targets.go33
-rw-r--r--go/game/unit.go19
-rw-r--r--go/game/unit_test.go12
-rw-r--r--go/ui/choice.go20
-rw-r--r--go/ui/mapView.go12
-rw-r--r--go/ui/textBox.go7
13 files changed, 161 insertions, 42 deletions
diff --git a/go/client/game.go b/go/client/game.go
index 00ef4153..77baa4ad 100644
--- a/go/client/game.go
+++ b/go/client/game.go
@@ -70,7 +70,7 @@ type Game struct {
handLayer *ui.HandView
stateBar *ui.StateBar
mapView *ui.MapView
- actionChoice *ui.Choice
+ choice *ui.Choice
passButton *ui.SimpleButton
discardPileView *ui.PocList
discardPileButton *ui.SimpleButton
@@ -252,23 +252,51 @@ func (g *Game) removeWidgetAt(idx int) {
g.widgets = g.widgets[:nwidgets-1]
}
+func (g *Game) addPermActionChoice(perm game.Permanent, x, y int) {
+ if len(perm.Pile()) == 0 {
+ g.addActionChoice(perm, x, y)
+ return
+ }
+
+ perms := []game.Permanent{perm}
+ for _, p := range perm.Pile() {
+ if len(p.AvailActions()) > 0 {
+ perms = append(perms, p)
+ }
+ }
+
+ if len(perms) > 1 {
+ g.selectedObject = perms
+ onClick := func(c *ui.Choice, x, y int) {
+ g.removeChoice()
+ p := perms[c.GetChoosen(x, y)]
+ g.addActionChoice(p, x, y)
+ }
+ g.choice = ui.NewPermChoice(x, y, perms, onClick)
+ g.addWidget(g.choice)
+ } else {
+ g.selectedObject = perms[0]
+ g.addActionChoice(perms[0], x, y)
+ }
+}
+
func (g *Game) addActionChoice(perm game.Permanent, x, y int) {
actions := perm.AvailActions()
if len(actions) > 0 {
g.selectedObject = perm
onClick := func(c *ui.Choice, x, y int) {
- g.removeActionChoice()
+ g.removeChoice()
a := actions[c.GetChoosen(x, y)]
g.selectedObject = a
}
- g.actionChoice = ui.NewActionChoice(x, y, actions, onClick)
- g.addWidget(g.actionChoice)
+ g.choice = ui.NewActionChoice(x, y, actions, onClick)
+ g.addWidget(g.choice)
}
}
-func (g *Game) removeActionChoice() {
- g.removeWidget(g.actionChoice)
- g.actionChoice = nil
+func (g *Game) removeChoice() {
+ g.removeWidget(g.choice)
+ g.choice = nil
}
func (g *Game) AddHighlight(obj interface{}) {
@@ -394,7 +422,7 @@ func (g *Game) handleSelection(obj interface{}, x, y int) {
case game.Permanent:
perm := obj
- g.addActionChoice(perm, x, y)
+ g.addPermActionChoice(perm, x, y)
case ui.HandCard:
g.selectedObject = game.NewPlayAction(g.activePlayer, obj.C)
@@ -496,7 +524,7 @@ func (g *Game) Update() error {
} else if inpututil.IsKeyJustPressed(ebiten.KeySpace) {
g.passButton.Click(0, 0)
} else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
- g.removeActionChoice()
+ g.removeChoice()
g.selectedObject = nil
g.hoverDetector.reset()
g.clearMapHighlights()
diff --git a/go/game/action.go b/go/game/action.go
index 08e42f64..934f5645 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -129,6 +129,14 @@ type MoveAction struct {
const moveActionTargetDesc = "available tile"
+func (a *MoveAction) targetTile() *Tile {
+ if t, ok := a.targets.sel[0].(*Tile); ok {
+ return t
+ }
+
+ return a.targets.sel[0].(*Unit).Tile()
+}
+
func NewMoveAction(u *Unit) *MoveAction {
a := &MoveAction{
ActionBase{
@@ -140,7 +148,7 @@ func NewMoveAction(u *Unit) *MoveAction {
a.targets = newTargets(u.Controller().gameState, []string{moveActionTargetDesc}, a)
a.resolveFunc = func(s *State) {
- tile := a.targets.sel[0].(*Tile)
+ tile := a.targetTile()
u.move(s, tile)
}
@@ -161,7 +169,7 @@ func (a *MoveAction) String() string {
return fmt.Sprintf("move %s", u.Movement.String())
}
- t := a.targets.sel[0].(*Tile)
+ t := a.targetTile()
return fmt.Sprintf("%v: %v -> %v", u.Controller().Name, u.Card().Name, t.Position)
}
diff --git a/go/game/ai.go b/go/game/ai.go
index ac6fb29a..181f6dc1 100644
--- a/go/game/ai.go
+++ b/go/game/ai.go
@@ -102,7 +102,7 @@ func (m *Map) generateMapGraphFor(u *Unit) *dijkstra.Graph {
}
func findPathTo(graph *dijkstra.Graph, u *Unit, pos Position) (dijkstra.BestPath, error) {
- srcId, err := graph.GetMapping(u.Tile().Position.String())
+ srcId, err := graph.GetMapping(u.TileOrContainingPermTile().Position.String())
if err != nil {
log.Fatalf("No mapping %s in Graph: %s", u.Tile().Position.String(), err)
}
@@ -119,6 +119,10 @@ func findPathsToUnits(graph *dijkstra.Graph, m *Map, u *Unit, units []*Unit) []*
paths := []*dijkstra.BestPath{}
for _, unit := range units {
+ if unit.tile == nil {
+ continue
+ }
+
var bestPathToUnit *dijkstra.BestPath = nil
for _, adjacentTile := range TilesInRangeFromOrigin(m, unit.Tile().Position, 1) {
path, err := findPathTo(graph, u, adjacentTile.Position)
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go
index b20b46bc..be625211 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -56,7 +56,7 @@ func (*swordImplementation) onPile(p Permanent) {
}
}
-func (*swordImplementation) onDrop(p Permanent) {
+func (*swordImplementation) onUnpile(p Permanent) {
if u, ok := p.(*Unit); ok && !INVALID_ATTACK(u.Attack) {
u.Attack.attacks[0] -= 1
}
diff --git a/go/game/equipment.go b/go/game/equipment.go
index 14e8b317..af06fdb1 100644
--- a/go/game/equipment.go
+++ b/go/game/equipment.go
@@ -28,11 +28,10 @@ func newEquipment(card *Card, containing interface{}, owner *Player) *Equipment
}
func (e *Equipment) onDrop(p Permanent) {
+ e.PermanentBase.onDrop(p)
e.Durability--
if e.Durability < 1 {
e.Controller().gameState.DestroyPermanent(e)
return
}
-
- p.Card().Impl.onDrop(p)
}
diff --git a/go/game/permanent.go b/go/game/permanent.go
index 91aa0cda..67c39e0b 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -85,8 +85,12 @@ func (p *PermanentBase) Owner() *Player {
}
func (p *PermanentBase) String() string {
- return fmt.Sprintf("%s's %s@%s",
- p.Controller().Name, p.Card().Name, p.Tile().Position.String())
+ if p.Tile() != nil {
+ return fmt.Sprintf("%s's %s@%v",
+ p.Controller().Name, p.Card().Name, p.Tile().Position)
+ }
+
+ return fmt.Sprintf("%s's %s", p.Controller().Name, p.Card().Name)
}
func DealDamage(src, dest Permanent, damage int) {
@@ -157,14 +161,17 @@ func (p *PermanentBase) RemovePermanentFromPile(rm Permanent) {
return
}
}
-func (b *PermanentBase) onPile(p Permanent) { p.Card().Impl.onPile(p) }
-func (b *PermanentBase) onUnpile(p Permanent) { p.Card().Impl.onUnpile(p) }
-func (b *PermanentBase) onDrop(p Permanent) { p.Card().Impl.onDrop(p) }
+func (b *PermanentBase) onPile(p Permanent) { b.Card().Impl.onPile(p) }
+func (b *PermanentBase) onUnpile(p Permanent) { b.Card().Impl.onUnpile(p) }
+func (b *PermanentBase) onDrop(p Permanent) {
+ b.onUnpile(p)
+ b.Card().Impl.onDrop(p)
+}
func AddPermanentToPile(containing, p Permanent) {
p.SetContainingPerm(containing)
containing.AddPermanentToPile(p)
- p.onPile(p)
+ p.onPile(containing)
}
func RemovePermanentFromPile(containing, rm Permanent) {
diff --git a/go/game/state.go b/go/game/state.go
index 45c8bba5..35aeb342 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -126,16 +126,16 @@ func (s *State) IsValidMove(a *MoveAction) error {
}
u := a.Source().(*Unit)
- tile := a.Targets().sel[0].(*Tile)
+ tile := relaxedTileTarget(a, a.Targets().sel[0])
if !slices.Contains(u.MoveRangeTiles(s.Map), tile) {
- return fmt.Errorf("Unit %s at %s can not move to %s",
- u.card.Name, u.tile.Position.String(), tile.Position.String())
+ return fmt.Errorf("Unit %s@%v can not move to %s",
+ u.card.Name, u.tile.Position, tile.Position.String())
}
- if tile.Permanent != nil {
- return fmt.Errorf("Unit %s at %s can not move to occupied %s",
- u.card.Name, u.tile.Position.String(), tile.Position.String())
+ if !u.IsAvailableTile(tile) {
+ return fmt.Errorf("Unit %s@%v can not move to not available tile %v",
+ u.card.Name, u.tile.Position, tile)
}
return nil
diff --git a/go/game/targets.go b/go/game/targets.go
index 0ab61cbc..046c53d6 100644
--- a/go/game/targets.go
+++ b/go/game/targets.go
@@ -165,6 +165,7 @@ func attackableTargetConstraint(action Action) TargetConstraintFunc {
p, _ := t.(Permanent)
if !p.Attackable() {
err = fmt.Errorf("Permanent %v is not attackable", p)
+ return
}
if attackAction, ok := action.(*AttackAction); ok {
@@ -215,7 +216,6 @@ func parsePermanentTargetConstraint(desc string, s *State, action Action) []Targ
}
if strings.Contains(desc, "attackable ") {
- log.Println("adding attackable constrained")
constraints = append(constraints, attackableTargetConstraint(action))
}
@@ -271,6 +271,25 @@ func parseArtifactTargetConstraint(constraint string, s *State, action Action) [
return constraints
}
+func relaxedTileTarget(action Action, t interface{}) *Tile {
+ // Strict case
+ if _, ok := action.(*MoveAction); !ok {
+ return t.(*Tile)
+ }
+
+ // Move actions allow to also move to Tile occupied by a crewable permanent
+ switch t := t.(type) {
+ case *Tile:
+ return t
+ case *Unit:
+ return t.Tile()
+ default:
+ log.Fatalf("Not handled target type %T", t)
+ }
+
+ return nil
+}
+
var tileTargetConstraint TargetConstraintFunc = func(t interface{}) (err error) {
_, ok := t.(*Tile)
if !ok {
@@ -280,18 +299,20 @@ var tileTargetConstraint TargetConstraintFunc = func(t interface{}) (err error)
}
func parseTileTargetConstraint(desc string, s *State, action Action) []TargetConstraintFunc {
- log.Println("parsing", desc)
- constraints := []TargetConstraintFunc{tileTargetConstraint}
+ constraints := []TargetConstraintFunc{}
if moveAction, ok := action.(*MoveAction); ok {
u := moveAction.Source().(*Unit)
constraints = append(constraints, func(t interface{}) (err error) {
- tile := t.(*Tile)
+ tile := relaxedTileTarget(action, t)
if slices.Contains(u.MoveRangeTiles(s.Map), tile) {
return nil
}
return fmt.Errorf("tile %v is not in %v's movement range", tile, u)
})
+ // Move action allow also unit targets as though they were tiles
+ } else {
+ constraints = append(constraints, tileTargetConstraint)
}
var player *Player
@@ -314,7 +335,7 @@ func parseTileTargetConstraint(desc string, s *State, action Action) []TargetCon
}
constraints = append(constraints, func(t interface{}) (err error) {
- tile := t.(*Tile)
+ tile := relaxedTileTarget(action, t)
if slices.Contains(s.AvailableSpawnTiles(player, card), tile) {
return nil
}
@@ -324,7 +345,7 @@ func parseTileTargetConstraint(desc string, s *State, action Action) []TargetCon
if strings.Contains(desc, "available") {
constraints = append(constraints, func(t interface{}) (err error) {
- tile := t.(*Tile)
+ tile := relaxedTileTarget(action, t)
if card == nil {
log.Fatalf("Not implemented availability tile constraint for %T", action)
diff --git a/go/game/unit.go b/go/game/unit.go
index a0d6bfa4..d38d156d 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -146,11 +146,21 @@ func (u *Unit) move(s *State, tile *Tile) {
}
}
+func (u *Unit) TileOrContainingPermTile() *Tile {
+ tile := u.tile
+ if tile == nil {
+ tile = u.containingPerm.Tile()
+ }
+
+ return tile
+}
+
func (u *Unit) MoveRangeTiles(m *Map) []*Tile {
tiles := []*Tile{}
// TODO: build graph only containing possible tile in movement range
graph := m.generateMapGraphFor(u)
- for _, pos := range PositionsInRange(u.tile.Position, u.Movement.Range, false) {
+ origin := u.TileOrContainingPermTile().Position
+ for _, pos := range PositionsInRange(origin, u.Movement.Range, false) {
tile := m.TileAt(pos)
if tile == nil || !u.IsAvailableTile(tile) {
continue
@@ -171,6 +181,10 @@ func (u *Unit) MoveRangeTiles(m *Map) []*Tile {
}
func (u *Unit) AttackableTiles(m *Map) []*Tile {
+ // Units not on the map can not attack
+ if u.tile == nil {
+ return nil
+ }
return TilesInRange(m, u, u.Attack.MaxRange())
}
@@ -207,7 +221,8 @@ func (u *Unit) IsDestroyed() bool {
}
func (u *Unit) Attackable() bool {
- return true
+ // Only units on the map are attackable
+ return u.tile != nil
}
func (u *Unit) HasFullAction() bool {
diff --git a/go/game/unit_test.go b/go/game/unit_test.go
index 341fb350..d6b5b079 100644
--- a/go/game/unit_test.go
+++ b/go/game/unit_test.go
@@ -44,6 +44,18 @@ symbols:
}
}
+ if INVALID_ATTACK(f.Attack) {
+ t.Fatalf("fisher has an invalid attack")
+ }
+
+ if a.Attackable() {
+ t.Fatalf("Piled archer is attackable")
+ }
+
+ if len(a.AttackableTiles(s.Map)) != 0 {
+ t.Fatalf("Piled archer has attackable tiles")
+ }
+
a.move(s, s.Map.TileAt(Position{1, 1}))
if len(f.Pile()) != 0 {
t.Fatalf("fisher's pile is not empty")
diff --git a/go/ui/choice.go b/go/ui/choice.go
index 003c2a6a..bacc6657 100644
--- a/go/ui/choice.go
+++ b/go/ui/choice.go
@@ -1,8 +1,11 @@
package ui
import (
+ "fmt"
+
"github.com/hajimehoshi/ebiten/v2"
"muhq.space/muhqs-game/go/game"
+ "muhq.space/muhqs-game/go/utils"
)
type Choice struct {
@@ -59,14 +62,21 @@ func (c *Choice) Render() *ebiten.Image {
return nil
}
-func NewActionChoice(x, y int, actions []game.Action, onClick func(*Choice, int, int)) *Choice {
+func NewObjectChoice(x, y int, objs []interface{}, 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())
+ labels := make([]string, 0, len(objs))
+ for _, obj := range objs {
+ labels = append(labels, fmt.Sprintf("%v", obj))
}
-
return NewChoice(x, y, width, choiceHeight, labels, onClick)
}
+
+func NewActionChoice(x, y int, actions []game.Action, onClick func(*Choice, int, int)) *Choice {
+ return NewObjectChoice(x, y, utils.TypedSliceToInterfaceSlice(actions), onClick)
+}
+
+func NewPermChoice(x, y int, perms []game.Permanent, onClick func(*Choice, int, int)) *Choice {
+ return NewObjectChoice(x, y, utils.TypedSliceToInterfaceSlice(perms), onClick)
+}
diff --git a/go/ui/mapView.go b/go/ui/mapView.go
index 8c83bd41..a6fd55c1 100644
--- a/go/ui/mapView.go
+++ b/go/ui/mapView.go
@@ -150,8 +150,12 @@ func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) {
vw.permanentsLayer = vw.newLayerImage()
for i, p := range vw.gameState.Permanents {
- permanentSymbol := getPermanentSymbol(p, i)
t := p.Tile()
+ // Skip permanents with no containing tiles (e.g. piled ones)
+ if t == nil {
+ continue
+ }
+ permanentSymbol := getPermanentSymbol(p, i)
x_px := t.Position.X*TILE_WIDTH + (TILE_WIDTH-PERMANENT_WIDTH)/2
y_px := t.Position.Y*TILE_HEIGHT + (TILE_HEIGHT-PERMANENT_HEIGHT)/2
@@ -206,7 +210,11 @@ func (vw *MapView) FindObjectAt(screenX, screenY int) interface{} {
relativeY >= yMargin && relativeY <= TILE_HEIGHT-yMargin {
for _, p := range vw.gameState.Permanents {
- pos := p.Tile().Position
+ t := p.Tile()
+ if t == nil {
+ continue
+ }
+ pos := t.Position
if pos.X == x && pos.Y == y {
if isSymbolTransparentAt(p, relativeX, relativeY) {
break
diff --git a/go/ui/textBox.go b/go/ui/textBox.go
index d0afbd9b..58a3e679 100644
--- a/go/ui/textBox.go
+++ b/go/ui/textBox.go
@@ -66,6 +66,13 @@ func NewUnitInfo(x, y int, u *game.Unit) *TextBox {
info = fmt.Sprintf("%s\nAttack: %s", info, u.Attack.String())
}
+ if len(u.Pile()) > 0 {
+ info = fmt.Sprintf("%s\nPile:", info)
+ for _, p := range u.Pile() {
+ info = fmt.Sprintf("%s\n %s", info, p.Card().Name)
+ }
+ }
+
return NewAutoTextBox(x, y, info)
}