diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-02-04 18:51:31 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-08-20 15:46:40 +0200 |
| commit | 8c3456f5a120a00f8a4e89065d7d50f2c1b4ad0d (patch) | |
| tree | 6e201152c264184bcd5aad784386654e921c8634 /go | |
| parent | 083ec284a9ef286729e544df9340a12760f03ab7 (diff) | |
| download | muhqs-game-8c3456f5a120a00f8a4e89065d7d50f2c1b4ad0d.tar.gz muhqs-game-8c3456f5a120a00f8a4e89065d7d50f2c1b4ad0d.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.go | 46 | ||||
| -rw-r--r-- | go/game/action.go | 12 | ||||
| -rw-r--r-- | go/game/ai.go | 6 | ||||
| -rw-r--r-- | go/game/cardImplementations.go | 2 | ||||
| -rw-r--r-- | go/game/equipment.go | 3 | ||||
| -rw-r--r-- | go/game/permanent.go | 19 | ||||
| -rw-r--r-- | go/game/state.go | 12 | ||||
| -rw-r--r-- | go/game/targets.go | 33 | ||||
| -rw-r--r-- | go/game/unit.go | 19 | ||||
| -rw-r--r-- | go/game/unit_test.go | 12 | ||||
| -rw-r--r-- | go/ui/choice.go | 20 | ||||
| -rw-r--r-- | go/ui/mapView.go | 12 | ||||
| -rw-r--r-- | go/ui/textBox.go | 7 |
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) } |
