aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Fischer <florian.fischer@muhq.space>2023-02-15 13:13:06 +0100
committerFlorian Fischer <florian.fischer@muhq.space>2025-01-27 16:43:49 +0100
commitf9ab2cfdb25eb3fcd85868a2e375326a9c8b6022 (patch)
treeb96f16e6cf4a6dcf93b9557a7219179ab2f46c8d
parent805cbf397589e5cb861f866713c2eb4bec9ce40b (diff)
downloadmuhqs-game-f9ab2cfdb25eb3fcd85868a2e375326a9c8b6022.tar.gz
muhqs-game-f9ab2cfdb25eb3fcd85868a2e375326a9c8b6022.zip
intermediate commit
* fix a lot of target and action bugs * check target selection before adding it to the prompt * use the store view for stores on the map * fix card highlighting in CardGrid * Only prompt for Upkeep-/DiscardActions if appropriate * Add helper for areaEffects granting new FullActions * Add Target() helper selecting the first target to the Action interface * Fix BuyAction and DiscardAction targets * Remember the stores a player has seen
-rw-r--r--go/TODO3
-rw-r--r--go/client/game.go95
-rw-r--r--go/game/action.go77
-rw-r--r--go/game/ai.go4
-rw-r--r--go/game/areaEffect.go24
-rw-r--r--go/game/cardImplementations.go56
-rw-r--r--go/game/deck.go4
-rw-r--r--go/game/map.go8
-rw-r--r--go/game/marks.go4
-rw-r--r--go/game/permanent.go6
-rw-r--r--go/game/player.go63
-rw-r--r--go/game/playerControl.go19
-rw-r--r--go/game/stack.go5
-rw-r--r--go/game/state.go18
-rw-r--r--go/game/targets.go99
-rw-r--r--go/game/tile.go37
-rw-r--r--go/game/unit.go20
-rw-r--r--go/ui/cardGrid.go3
-rw-r--r--go/ui/mapView.go4
-rw-r--r--go/ui/prompt.go8
-rw-r--r--go/ui/textBox.go19
21 files changed, 393 insertions, 183 deletions
diff --git a/go/TODO b/go/TODO
index 23274884..74869443 100644
--- a/go/TODO
+++ b/go/TODO
@@ -1,8 +1,7 @@
-* implement upkeep action
-* implement target selection for triggers
* implement Pile UI hint
* implement spell target parsing / selection
* implement triggers
+ * implement target selection for triggers
* implement draft
* implement game log
* finish AIs
diff --git a/go/client/game.go b/go/client/game.go
index f954362a..b459d42a 100644
--- a/go/client/game.go
+++ b/go/client/game.go
@@ -19,12 +19,15 @@ const (
RESOLVE_BUTTON_X = 500
STACK_BUFFER_WIDTH = 300
- HOVER_THRESHOLD = 60
+ HOVER_THRESHOLD = 60
+ HOVER_CARD_WIDTH = 500
+ HOVER_CARD_HEIGHT = 700
- HAND_VIEW_HEIGHT = 500
- HAND_VIEW_WIDTH = 500
+ HAND_VIEW_WIDTH = 500
STATE_BAR_WIDTH = 300
+
+ POC_BUTTON_WIDTH = 150
)
// A simple structure to detect if the cursor has not moved for HOVER_THRESHOLD
@@ -175,7 +178,7 @@ func (g *Game) initPlayerUi(player *game.Player) *Game {
g.discardPileButton = ui.NewSimpleButton(g.stateBar.Width,
g.height-DEFAULT_BUTTON_HEIGHT,
- 150,
+ POC_BUTTON_WIDTH,
DEFAULT_BUTTON_HEIGHT,
"DiscardPile",
func(*ui.SimpleButton) {
@@ -196,7 +199,7 @@ func (g *Game) initPlayerUi(player *game.Player) *Game {
x = g.stateBar.Width + g.discardPileButton.Width
y = g.height - DEFAULT_BUTTON_HEIGHT
g.storeButton = ui.NewSimpleButton(x, y,
- 150,
+ POC_BUTTON_WIDTH,
DEFAULT_BUTTON_HEIGHT,
"Store",
func(*ui.SimpleButton) {
@@ -287,7 +290,11 @@ func (g *Game) addActionChoice(perm game.Permanent, x, y int) {
onClick := func(c *ui.Choice, x, y int) {
g.removeChoice()
a := actions[c.GetChoosen(x, y)]
- g.selectedObject = a
+ if a.Targets() != nil && a.Targets().RequireSelection() {
+ g.selectedObject = a
+ } else {
+ g.declareAction(a)
+ }
}
g.choice = ui.NewActionChoice(x, y, actions, onClick)
g.addWidget(g.choice)
@@ -303,8 +310,14 @@ func (g *Game) AddHighlight(obj interface{}) {
switch obj := obj.(type) {
case ui.HandCard:
g.handLayer.AddHighlightCard(obj.C)
+ case *game.Card:
+ g.handLayer.AddHighlightCard(obj)
case *game.Tile:
g.mapView.AddHighlightTile(obj)
+ case *game.Unit:
+ g.mapView.AddHighlightPermanent(obj)
+ default:
+ log.Fatalf("Unhandled highlight of type %T", obj)
}
}
@@ -338,7 +351,7 @@ func (g *Game) passPriority() {
func (g *Game) handlePlayerNotifications() {
n := g.playerCtrl.RecvNotification()
for n != nil {
- log.Println("Received notification", n)
+ log.Println("Received", n)
switch n.Notification {
case game.PriorityNotification:
g.showPassButton()
@@ -394,7 +407,10 @@ func (g *Game) findObjectAt(x, y int) interface{} {
func (g *Game) handleTargetSelection(obj interface{}) {
a := g.selectedObject.(game.Action)
- a.Targets().AddSelection(obj)
+ err := a.Targets().AddSelection(obj)
+ if err != nil {
+ log.Println("Not added", obj, "as target for", a, "because", err)
+ }
if !a.Targets().RequireSelection() {
g.declareAction(a)
@@ -416,7 +432,14 @@ func (g *Game) handleSelection(obj interface{}, x, y int) {
obj.Click(x, y)
case *game.Tile:
- g.mapView.HighlightTile(obj)
+ if g.storesOnMap && obj.Type == game.TileTypes.Store && g.activePlayer.KnowsStore(obj.Position) {
+ if g.storeView != nil {
+ g.hideStore()
+ }
+
+ g.storeView = ui.NewPocList(x, y, g.gameState.Map.StoreOn(obj.Position))
+ g.showStore()
+ }
case game.Permanent:
perm := obj
@@ -449,30 +472,36 @@ func (g *Game) handleHover(obj interface{}, x, y int) {
switch obj := obj.(type) {
case *game.Card:
wx, wy := x, y
- if x+500 > g.width || y+700 > g.height {
- wx = x - 500
- wy = y - 700
+ if x+HOVER_CARD_WIDTH > g.width || y+HOVER_CARD_HEIGHT > g.height {
+ wx = x - HOVER_CARD_WIDTH
+ wy = y - HOVER_CARD_HEIGHT
}
- hoverHint = ui.NewScaledCardView(wx, wy, 500, 700, obj.Path())
+ if wx < 0 {
+ wx = 0
+ }
+ if wy < 0 {
+ wy = 0
+ }
+ hoverHint = ui.NewScaledCardView(wx, wy, HOVER_CARD_WIDTH, HOVER_CARD_HEIGHT, obj.Path())
case *game.Unit:
hoverHint = ui.NewUnitInfo(x+ui.TILE_WIDTH, y, obj)
case game.Permanent:
hoverHint = ui.NewPermInfo(x+ui.TILE_WIDTH, y, obj)
- case *game.Tile:
- if g.storesOnMap && obj.Type == game.TileTypes.Store {
- poc := g.gameState.Map.StoreOn(obj.Position)
- hoverHint = ui.NewPocList(x, y, poc)
- // Set a special hoverDetector resetFunc keeping the hovering store
- // around and allow card selection until the cursor leaves the store widget.
- g.hoverDetector.resetFunc = func() bool {
- x, y := ebiten.CursorPosition()
- if hoverHint.Contains(x, y) {
- return false
- }
- g.removeWidget(hoverHint)
- return true
- }
- }
+ // case *game.Tile:
+ // if g.storesOnMap && obj.Type == game.TileTypes.Store {
+ // poc := g.gameState.Map.StoreOn(obj.Position)
+ // hoverHint = ui.NewPocList(x, y, poc)
+ // // Set a special hoverDetector resetFunc keeping the hovering store
+ // // around and allow card selection until the cursor leaves the store widget.
+ // g.hoverDetector.resetFunc = func() bool {
+ // x, y := ebiten.CursorPosition()
+ // if hoverHint.Contains(x, y) {
+ // return false
+ // }
+ // g.removeWidget(hoverHint)
+ // return true
+ // }
+ // }
}
if hoverHint != nil {
@@ -508,8 +537,12 @@ func (g *Game) Update() error {
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
if g.prompt != nil {
- g.prompt.Add(obj)
- g.AddHighlight(obj)
+ if err := g.prompt.Add(obj); err == nil {
+ g.AddHighlight(obj)
+ } else {
+ log.Println("Not added", obj, "to active prompt:", err)
+ g.handleSelection(obj, x, y)
+ }
} else {
g.handleSelection(obj, x, y)
}
@@ -523,6 +556,8 @@ func (g *Game) Update() error {
g.hoverDetector.reset()
g.clearMapHighlights()
g.handLayer.ClearHighlights()
+ g.hideStore()
+ g.removeWidget(g.discardPileView)
}
return nil
}
diff --git a/go/game/action.go b/go/game/action.go
index 65eb03b6..77e9743f 100644
--- a/go/game/action.go
+++ b/go/game/action.go
@@ -2,8 +2,6 @@ package game
import (
"fmt"
-
- "golang.org/x/exp/slices"
)
type (
@@ -15,6 +13,7 @@ type (
type Action interface {
Source() interface{}
Targets() *Targets
+ Target() *Target // Shortcut for to select the first target
CheckTargets(*State) error
PayCosts(s *State) bool
@@ -26,6 +25,7 @@ type PassPriority struct{ player *Player }
func (a *PassPriority) Source() interface{} { return a.player }
func (*PassPriority) Targets() *Targets { return nil }
+func (*PassPriority) Target() *Target { return nil }
func (*PassPriority) CheckTargets(*State) error { return nil }
func (*PassPriority) PayCosts(*State) bool { return true }
func (*PassPriority) Resolve(*State) {}
@@ -39,6 +39,7 @@ type TargetSelection struct {
func (sel *TargetSelection) Source() interface{} { return sel.player }
func (sel *TargetSelection) Targets() *Targets { return sel.targets }
+func (sel *TargetSelection) Target() *Target { return sel.targets.ts[0] }
func (sel *TargetSelection) CheckTargets(s *State) error { return sel.targets.CheckTargets(s) }
func (*TargetSelection) PayCosts(*State) bool { return true }
func (*TargetSelection) Resolve(*State) {}
@@ -50,7 +51,8 @@ func newTargetSelection(player *Player, targets *Targets) Action {
func newHandCardSelection(p *Player, min, max int) Action {
a := &TargetSelection{player: p}
- a.targets = newTargets(newTarget(p.gameState, newTargetDesc("hand card"), a))
+ targetDesc := TargetDesc{"hand card", fmt.Sprintf("%d-%d", min, max)}
+ a.targets = newTargets(newTarget(p.gameState, targetDesc, a))
return a
}
@@ -62,23 +64,15 @@ type ActionBase struct {
costFunc ActionCostFunc
}
-func (a *ActionBase) Source() interface{} {
- return a.source
-}
-
-func (a *ActionBase) Resolve(s *State) {
- a.resolveFunc(s)
-}
-
-func (a *ActionBase) PayCosts(s *State) bool {
- return a.costFunc(s)
-}
-
-func (a *ActionBase) Targets() *Targets {
- return a.targets
-}
-
+func (a *ActionBase) Source() interface{} { return a.source }
+func (a *ActionBase) Resolve(s *State) { a.resolveFunc(s) }
+func (a *ActionBase) PayCosts(s *State) bool { return a.costFunc(s) }
+func (a *ActionBase) Targets() *Targets { return a.targets }
+func (a *ActionBase) Target() *Target { return a.targets.ts[0] }
func (a *ActionBase) CheckTargets(s *State) error {
+ if a.targets == nil {
+ return nil
+ }
return a.targets.CheckTargets(s)
}
@@ -144,7 +138,7 @@ type MoveAction struct {
var moveActionTargetDesc = TargetDesc{"available tile", "1"}
func (a *MoveAction) targetTile() *Tile {
- t := a.Targets().ts[0]
+ t := a.Target()
if tile, ok := t.sel[0].(*Tile); ok {
return tile
}
@@ -203,7 +197,7 @@ func NewAttackAction(u *Unit) *AttackAction {
a.targets = newTargets(newTarget(u.Controller().gameState, attackActionTargetDesc, a))
a.resolveFunc = func(s *State) {
- s.Fight(u, a.targets.ts[0].sel[0].(Permanent))
+ s.Fight(u, a.Target().sel[0].(Permanent))
}
a.costFunc = func(*State) bool {
@@ -223,7 +217,7 @@ func (a *AttackAction) String() string {
return fmt.Sprintf("attack %v", u.Attack)
}
- target := a.targets.ts[0].sel[0].(Permanent)
+ target := a.Target().sel[0].(Permanent)
return fmt.Sprintf("%s x %v", u.FmtWController(), target.Tile().Position)
}
@@ -261,7 +255,7 @@ func NewFullAction(u *Unit, proto ActionFuncPrototype, desc string) *FullAction
func (a *FullAction) String() string {
u := a.source.(*Unit)
- if a.targets.RequireSelection() {
+ if a.targets == nil || a.targets.RequireSelection() {
return fmt.Sprintf("↻ %s", a.desc)
}
@@ -302,12 +296,12 @@ func (a *FreeAction) String() string {
return fmt.Sprintf("%v: %s free_action@%v", p.Controller(), p.Card().Name, a.targets)
}
-var buyActionTargetDesc = TargetDesc{"store card", "1"}
+var buyActionTargetDesc = TargetDesc{"store card", "?"}
type BuyAction struct{ TargetSelection }
func (a *BuyAction) card() *Card {
- return a.Targets().ts[0].sel[0].(*Card)
+ return a.Target().sel[0].(*Card)
}
func (a *BuyAction) CheckTargets(s *State) error {
@@ -333,7 +327,7 @@ func (a *BuyAction) PayCosts(*State) bool {
}
func (a *BuyAction) String() string {
- if a.Targets().RequireSelection() {
+ if !a.Targets().HasSelections() {
return fmt.Sprintf("%s buy", a.player.Name)
}
@@ -354,7 +348,7 @@ type UpkeepAction struct {
func (a *UpkeepAction) PayCosts(*State) bool {
costs := 0
- for _, t := range a.targets.ts[0].sel {
+ for _, t := range a.Target().sel {
u := t.(*Unit)
costs += u.UpkeepCost()
}
@@ -368,31 +362,26 @@ func (a *UpkeepAction) PayCosts(*State) bool {
}
func (a *UpkeepAction) Resolve(*State) {
- s := a.player.gameState
+ p := a.player
+ s := p.gameState
+ for _, i := range a.Target().sel {
+ u := i.(*Unit)
+ s.DestroyPermanent(u)
+ }
+
+ // Keep and pay for the rest
for _, u := range s.Units {
- if u.Controller() != a.player {
+ if u.Controller() != p {
continue
}
- if !slices.Contains(a.targets.ts[0].sel, interface{}(u)) {
- s.DestroyPermanent(u)
- }
+ p.Resource -= u.upkeep
+ u.onUpkeep()
}
}
func (a *UpkeepAction) String() string {
- disbanding := []*Unit{}
- for _, u := range a.player.gameState.Units {
- if u.Controller() != a.player {
- continue
- }
-
- if !slices.Contains(a.targets.ts[0].sel, interface{}(u)) {
- disbanding = append(disbanding, u)
- }
- }
-
- return fmt.Sprintf("upkeep disbanding: %v", disbanding)
+ return fmt.Sprintf("upkeep disbanding: %v", a.Target().sel)
}
func newUpkeepAction(p *Player) *UpkeepAction {
diff --git a/go/game/ai.go b/go/game/ai.go
index bda7356a..9b48c2f5 100644
--- a/go/game/ai.go
+++ b/go/game/ai.go
@@ -90,7 +90,7 @@ func selectRandomTargets(rand *rand.Rand, targets *Targets) error {
if len(options) > 1 {
idx = rand.Intn(len(options) - 1)
}
- t.AddSelection(options[idx])
+ _ = t.AddSelection(options[idx])
}
}
return nil
@@ -211,7 +211,7 @@ func moveTowardsNearestEnemyUnit(ai *UnitAI) Action {
}
a := NewMoveAction(ai.u)
- a.Targets().ts[0].AddSelection(ai.s.Map.TileAt(target))
+ _ = a.Target().AddSelection(ai.s.Map.TileAt(target))
return a
}
diff --git a/go/game/areaEffect.go b/go/game/areaEffect.go
index 8991bc3c..4cba29e9 100644
--- a/go/game/areaEffect.go
+++ b/go/game/areaEffect.go
@@ -25,3 +25,27 @@ func (e *dynamicAreaEffect) onLeaving(p Permanent) {
e._onLeaving(p)
}
}
+
+func newGrantFullActionEffect(cardPath string, f ActionFuncPrototype, desc, tag string,
+) *dynamicAreaEffect {
+ onEntering := func(p Permanent) {
+ if p.Card().Path() != cardPath {
+ return
+ }
+
+ u := p.(*Unit)
+ fa := NewFullAction(u, f, desc)
+ fa.tag = tag
+ u.FullActions = append(u.FullActions, fa)
+ }
+
+ onLeaving := func(p Permanent) {
+ if p.Card().Path() != cardPath {
+ return
+ }
+ u := p.(*Unit)
+ u.removeFullAction(tag)
+ }
+
+ return newDynamicAreaEffect(onEntering, onLeaving)
+}
diff --git a/go/game/cardImplementations.go b/go/game/cardImplementations.go
index a9da5fb1..038ce2d6 100644
--- a/go/game/cardImplementations.go
+++ b/go/game/cardImplementations.go
@@ -34,9 +34,9 @@ type missionaryImpl struct{ cardImplementationBase }
func (*missionaryImpl) fullActions(u *Unit) []*FullAction {
resolvePrototype := func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
- target := a.Targets().ts[0].sel[0].(*Unit)
+ target := a.Target().sel[0].(*Unit)
return func(s *State) {
- target.addMarks(UnitMarks.Faith, 2)
+ target.adjustMarks(UnitMarks.Faith, 2)
if target.Marks(UnitMarks.Faith) > target.Card().BuyCost {
target.controller = u.Controller()
}
@@ -66,59 +66,33 @@ func (*misinformationImpl) onPlay(s *State, c *Card, _ *Player, _ *Targets) {
// ====== Nautics Set ======
-type fishTrapAoE struct{}
-
-func (*fishTrapAoE) onEntering(p Permanent) {
- if p.Card().Name != "nautics/fisher" {
- return
- }
-
- fisher := p.(*Unit)
- fishTrapAction := func(a Action) ActionResolveFunc {
+var fishTrapAoE areaEffect = newGrantFullActionEffect("nautics/fisher",
+ func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
return func(s *State) {
controller := u.Controller()
controller.gainResource(2)
}
- }
-
- fa := NewFullAction(fisher, fishTrapAction, "gain 2 resource")
- fa.tag = "fishTrapAction"
- fisher.FullActions = append(fisher.FullActions, fa)
-}
-
-func (*fishTrapAoE) onLeaving(p Permanent) {
- if p.Card().Name != "nautics/fisher" {
- return
- }
-
- fisher := p.(*Unit)
- for i, fa := range fisher.FullActions {
- if fa.tag != "fishTrapAction" {
- continue
- }
- fisher.FullActions[i] = fisher.FullActions[len(fisher.FullActions)-1]
- fisher.FullActions = fisher.FullActions[:len(fisher.FullActions)-1]
- break
- }
-}
+ }, "gain 2 resource", "fishTrapAction")
type fishTrapImpl struct {
cardImplementation
- aoe fishTrapAoE
+ aoe areaEffect
}
+func (*fishTrapImpl) stateBasedActions(s *State, p Permanent) {}
+
func (i *fishTrapImpl) onEntering(t *Tile) {
s := t.Permanent.Controller().gameState
for _, tile := range TilesInRange(s.Map, t.Permanent, 1) {
- tile.addEffect(&i.aoe)
+ tile.addEffect(i.aoe)
}
}
func (i *fishTrapImpl) onLeaving(t *Tile) {
s := t.Permanent.Controller().gameState
for _, tile := range TilesInRange(s.Map, t.Permanent, 1) {
- tile.removeEffect(&i.aoe)
+ tile.removeEffect(i.aoe)
}
}
@@ -129,12 +103,12 @@ func (*fisherImpl) fullActions(u *Unit) []*FullAction {
resolvePrototype := func(a Action) ActionResolveFunc {
u := a.Source().(*Unit)
return func(s *State) {
- t := a.Targets().ts[0].sel[0].(*Tile)
+ t := a.Target().sel[0].(*Tile)
s.AddNewArtifact(NewCard("nautics/fish_trap"), t.Position, u.Controller())
}
}
a := NewFullAction(u, resolvePrototype, "create fish trap")
- a.targets = newTargets(newTarget(s, newTargetDesc("adjacent water tile"), a))
+ a.targets = newTargets(newTarget(s, newTargetDesc("adjacent free water tile"), a))
return []*FullAction{a}
}
@@ -167,7 +141,7 @@ func (*sailorImpl) onUnpile(containing Permanent) {
type tidesChangeImpl struct{ cardImplementation }
func (*tidesChangeImpl) onPlay(s *State, _ *Card, _ *Player, _ *Targets) {
- s.Map.redistributeStoreCards(s.Rand)
+ s.redistributeMapStoreCards()
}
type dejaVuImpl struct{ cardImplementation }
@@ -330,7 +304,7 @@ func (*unholyCannonballImpl) onPlay(s *State, _ *Card, kraken *Player, _ *Target
kraken.gainResource(5)
} else {
for _, u := range enemiesInRange2 {
- u.addMarks(UnitStates.Panic, 1)
+ u.adjustMarks(UnitStates.Panic, 1)
}
}
}
@@ -344,7 +318,7 @@ func init() {
"base/misinformation": &misinformationImpl{},
"nautics/captain": &captainImpl{},
- "nautics/fish_trap": &fishTrapImpl{},
+ "nautics/fish_trap": &fishTrapImpl{aoe: fishTrapAoE},
"nautics/fisher": &fisherImpl{},
"nautics/sailor": &sailorImpl{},
diff --git a/go/game/deck.go b/go/game/deck.go
index 569a952d..1f9539a9 100644
--- a/go/game/deck.go
+++ b/go/game/deck.go
@@ -59,6 +59,10 @@ func NewDeckFromCardPaths(cardPaths []string) *Deck {
func NewDeckFromDeckList(deckList string) *Deck {
cardPaths := []string{}
+
+ // trim last trailing newline
+ deckList = strings.Trim(deckList, "\n")
+
lines := strings.Split(deckList, "\n")
for _, line := range lines {
tokens := strings.SplitN(line, " ", 2)
diff --git a/go/game/map.go b/go/game/map.go
index b86debad..5ed1335e 100644
--- a/go/game/map.go
+++ b/go/game/map.go
@@ -290,14 +290,6 @@ func (m *Map) distributeStoreCards(cards PileOfCards, rand *rand.Rand) {
}
}
-func (m *Map) redistributeStoreCards(rand *rand.Rand) {
- poc := NewPileOfCards()
- for _, store := range m.Stores {
- store.MoveInto(poc)
- }
- m.distributeStoreCards(poc, rand)
-}
-
func (m *Map) HasStores() bool {
return len(m.Stores) > 0
}
diff --git a/go/game/marks.go b/go/game/marks.go
index a3ea0e32..0764b531 100644
--- a/go/game/marks.go
+++ b/go/game/marks.go
@@ -45,17 +45,19 @@ var (
Crack: crack,
}
- unitStates = []PermanentMark{paralysis, poison, panic_, rage}
+ // unitStates = []PermanentMark{paralysis, poison, panic_, rage}
UnitStates = struct {
Paralysis PermanentMark
Poison PermanentMark
Panic PermanentMark
Rage PermanentMark
+ Ward PermanentMark
}{
Paralysis: paralysis,
Poison: poison,
Panic: panic_,
Rage: rage,
+ Ward: ward,
}
UnitMarks = struct {
diff --git a/go/game/permanent.go b/go/game/permanent.go
index f25c95b3..ecc4459a 100644
--- a/go/game/permanent.go
+++ b/go/game/permanent.go
@@ -22,7 +22,7 @@ type Permanent interface {
HasEffect(string) bool
XEffect(string) (int, error)
Marks(PermanentMark) int
- addMarks(PermanentMark, int)
+ adjustMarks(PermanentMark, int)
String() string
Fight(p Permanent)
@@ -137,7 +137,7 @@ func (p *PermanentBase) Marks(mark PermanentMark) int {
return 0
}
-func (p *PermanentBase) addMarks(mark PermanentMark, amount int) {
+func (p *PermanentBase) adjustMarks(mark PermanentMark, amount int) {
if amount, found := p.marks[mark]; found {
p.marks[mark] += amount
} else {
@@ -195,8 +195,8 @@ func DropPile(containing Permanent) {
func enterTile(p Permanent, t *Tile) {
p.SetTile(t)
- p.Card().Impl.onEntering(t)
t.entering(p)
+ p.Card().Impl.onEntering(t)
}
func leaveTile(p Permanent) {
diff --git a/go/game/player.go b/go/game/player.go
index 1215479f..a69f2cd3 100644
--- a/go/game/player.go
+++ b/go/game/player.go
@@ -24,6 +24,7 @@ type Player struct {
gameState *State
Color color.Color
Ctrl PlayerControl
+ knownStores map[Position]bool
}
func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
@@ -42,6 +43,7 @@ func NewPlayer(id int, name string, deck *Deck, gameState *State) *Player {
Store: store,
gameState: gameState,
Ctrl: nil,
+ knownStores: make(map[Position]bool),
}
return &p
}
@@ -78,15 +80,26 @@ func (p *Player) Upkeep() {
// TODO: handle upkeep triggers
- for _, unit := range p.gameState.Units {
- if unit.controller != p {
- continue
+ // Skip upkeep prompt if player does not controll any units
+ controllsUnits := false
+ for _, u := range p.gameState.Units {
+ if u.Controller() == p {
+ controllsUnits = true
+ break
}
+ }
+ if !controllsUnits {
+ return
+ }
+
+ a := prompt(p.Ctrl, newUpkeepPrompt(p))
- // TODO: let players decide if they want to pay the upkeep
- p.Resource -= unit.UpkeepCost()
- unit.onUpkeep()
+ if _, ok := a.(*PassPriority); ok {
+ a = newUpkeepAction(p)
}
+
+ p.gameState.DeclareAction(a)
+ p.gameState.Stack.Resolve()
}
func (p *Player) ActionPhase() {
@@ -102,12 +115,15 @@ func (p *Player) ActionPhase() {
func (p *Player) BuyPhase() {
a := promptBuy(p.Ctrl)
- log.Println(a)
+
if _, ok := a.(*PassPriority); ok {
return
}
- p.gameState.DeclareAction(a)
- p.gameState.Stack.Resolve()
+
+ if a.Targets().HasSelections() {
+ p.gameState.DeclareAction(a)
+ p.gameState.Stack.Resolve()
+ }
}
func (p *Player) discardHand() {
@@ -121,6 +137,10 @@ func (p *Player) DiscardCard(c *Card) {
}
func (p *Player) DiscardPhase() {
+ if p.Hand.Size() == 0 {
+ return
+ }
+
cards := p.PromptHandCardSelection(0, p.Hand.Size())
for _, c := range cards {
p.DiscardCard(c)
@@ -136,7 +156,7 @@ func (p *Player) PromptHandCardSelection(min, max int) []*Card {
log.Fatalf("Invalid hand card selection: %v", err)
}
- return utils.InterfaceSliceToTypedSlice[*Card](a.Targets().ts[0].sel)
+ return utils.InterfaceSliceToTypedSlice[*Card](a.Target().sel)
case *PassPriority:
return nil
default:
@@ -163,3 +183,26 @@ func (p *Player) IsEnemy(other *Player) bool {
// TODO: support coop / teams
return p != other
}
+
+func (p *Player) AvailableStores() (stores []*Store) {
+ m := p.gameState.Map
+ for pos, store := range m.Stores {
+ t := m.TileAt(pos)
+ if t.Permanent != nil && t.Permanent.Controller() == p {
+ stores = append(stores, store)
+ }
+ }
+ return
+}
+
+func (p *Player) KnowsStore(pos Position) bool {
+ return p.knownStores[pos]
+}
+
+func (p *Player) addKnownStore(pos Position) {
+ p.knownStores[pos] = true
+}
+
+func (p *Player) clearKnownStore() {
+ p.knownStores = make(map[Position]bool)
+}
diff --git a/go/game/playerControl.go b/go/game/playerControl.go
index dda1a736..eea6a19b 100644
--- a/go/game/playerControl.go
+++ b/go/game/playerControl.go
@@ -8,12 +8,28 @@ import (
type PlayerNotificationType int
const (
- DeclaredActionNotification = iota
+ DeclaredActionNotification PlayerNotificationType = iota
ResolvedActionNotification
PriorityNotification
TargetSelectionPrompt
)
+func (n PlayerNotificationType) String() string {
+ switch n {
+ case DeclaredActionNotification:
+ return "DeclaredActionNotification"
+ case ResolvedActionNotification:
+ return "ResolvedActionNotification"
+ case PriorityNotification:
+ return "PriorityNotification"
+ case TargetSelectionPrompt:
+ return "TargetSelectionPrompt"
+ default:
+ log.Panicf("Unhandled notification %d", n)
+ return ""
+ }
+}
+
func (n PlayerNotification) IsPriorityNotification() bool {
return n.Notification == PriorityNotification
}
@@ -61,7 +77,6 @@ func newUpkeepPrompt(p *Player) PlayerNotification {
func newBuyPrompt(p *Player) PlayerNotification {
a := newBuyAction(p)
prompt := newTargetSelectionPrompt(a, "Select a card to buy")
- log.Println("sending buy prompt with ctx", prompt)
return prompt
}
diff --git a/go/game/stack.go b/go/game/stack.go
index c119e81c..78a5ff41 100644
--- a/go/game/stack.go
+++ b/go/game/stack.go
@@ -33,6 +33,11 @@ func (s *Stack) pop() {
err := s.gameState.ValidateAction(a)
if err == nil {
a.Resolve(s.gameState)
+ // Some action may be reused like full actions of permanents.
+ // Reset their target selection to allow possible reuse.
+ if t := a.Targets(); t != nil {
+ t.ClearSelections()
+ }
}
log.Println("Resolved", a, err)
s.gameState.broadcastNotification(newResolvedActionNotification(a, err))
diff --git a/go/game/state.go b/go/game/state.go
index 31c8ad01..5c2fd053 100644
--- a/go/game/state.go
+++ b/go/game/state.go
@@ -111,7 +111,7 @@ func (s *State) IsValidPlay(a *PlayAction) error {
}
if a.Card.IsPermanent() {
- spawn := a.Targets().ts[0].sel[0].(*Tile)
+ spawn := a.Target().sel[0].(*Tile)
if !slices.Contains(s.AvailableSpawnTiles(a.Source().(*Player), a.Card), spawn) {
return fmt.Errorf("Spawn %v is not a valid spawn tile for %s",
spawn.Position.String(), a.Card.Name)
@@ -127,7 +127,7 @@ func (s *State) IsValidMove(a *MoveAction) error {
}
u := a.Source().(*Unit)
- tile := relaxedTileTarget(a, a.Targets().ts[0].sel[0])
+ tile := relaxedTileTarget(a, a.Target().sel[0])
if !slices.Contains(u.MoveRangeTiles(s.Map), tile) {
return fmt.Errorf("Unit %s@%v can not move to %s",
@@ -152,7 +152,7 @@ func (s *State) IsValidAttack(a *AttackAction) error {
return fmt.Errorf("%s can not attack", unit.card.Name)
}
- target := a.Targets().ts[0].sel[0].(Permanent)
+ target := a.Target().sel[0].(Permanent)
if !IsPositionInRange(unit.tile.Position, target.Tile().Position, unit.Attack.MaxRange()) {
return fmt.Errorf("Attack target on %s is not in attack range %d of %s on %s",
target.Tile().Position.String(), unit.Attack.MaxRange(), unit.card.Name,
@@ -471,3 +471,15 @@ func (s *State) AvailableSpawnTiles(p *Player, c *Card) []*Tile {
return tiles
}
+
+func (s *State) redistributeMapStoreCards() {
+ poc := NewPileOfCards()
+ for _, store := range s.Map.Stores {
+ store.MoveInto(poc)
+ }
+ s.Map.distributeStoreCards(poc, s.Rand)
+
+ for _, p := range s.Players {
+ p.clearKnownStore()
+ }
+}
diff --git a/go/game/targets.go b/go/game/targets.go
index 99ba5085..440d8922 100644
--- a/go/game/targets.go
+++ b/go/game/targets.go
@@ -93,6 +93,19 @@ func (d *TargetDesc) requirement() TargetRequirement {
case "*":
return TargetRequirement{0, -1}
default:
+ if strings.Contains(d.req, "-") {
+ tokens := strings.SplitN(d.req, "-", 2)
+ min, err := strconv.Atoi(tokens[0])
+ if err != nil {
+ log.Fatalf("failed to parse target requirement %s: %v", d.req, err)
+ }
+ max, err := strconv.Atoi(tokens[1])
+ if err != nil {
+ log.Fatalf("failed to parse target requirement %s: %v", d.req, err)
+ }
+ return TargetRequirement{min, max}
+ }
+
x, err := strconv.Atoi(d.req)
if err != nil {
log.Fatalf("failed to parse target requirement %s: %v", d.req, err)
@@ -101,10 +114,17 @@ func (d *TargetDesc) requirement() TargetRequirement {
}
}
-func (t *Target) RequireSelection() bool {
- return len(t.sel) < t.requirement.min
+func (t *Target) AddSelection(sel interface{}) (err error) {
+ if err = t.constraint(sel); err == nil {
+ t.sel = append(t.sel, sel)
+ }
+ return
}
+func (t *Target) RequireSelection() bool { return len(t.sel) < t.requirement.min }
+func (t *Target) HasSelection() bool { return len(t.sel) > 0 }
+func (t *Target) ClearSelection() { t.sel = []interface{}{} }
+
func (t *Target) Options() (options []interface{}) {
candidates := targetCandidates(t.desc, t.s)
@@ -116,22 +136,6 @@ func (t *Target) Options() (options []interface{}) {
return options
}
-func (t *Targets) Options() (options []interface{}) {
- return t.ts[t.idx].Options()
-}
-
-func (t *Target) AddSelection(sel interface{}) {
- t.sel = append(t.sel, sel)
-}
-
-func (t *Targets) AddSelection(sel interface{}) {
- t.ts[t.idx].AddSelection(sel)
-}
-
-func (t *Targets) Next() {
- t.idx++
-}
-
func (t *Target) CheckSelection(s *State) error {
if t.RequireSelection() {
return fmt.Errorf("Number of selected targets and required ones does not match")
@@ -151,6 +155,25 @@ func (t *Target) CheckSelection(s *State) error {
return nil
}
+func (t *Targets) AddSelection(sel interface{}) error { return t.ts[t.idx].AddSelection(sel) }
+
+func (t *Targets) HasSelections() bool {
+ for _, t := range t.ts {
+ if !t.HasSelection() {
+ return false
+ }
+ }
+ return true
+}
+
+func (t *Targets) ClearSelections() {
+ for _, t := range t.ts {
+ t.ClearSelection()
+ }
+}
+func (t *Targets) Options() (options []interface{}) { return t.ts[t.idx].Options() }
+func (t *Targets) Next() { t.idx++ }
+
func (t *Targets) CheckTargets(s *State) error {
for _, target := range t.ts {
if err := target.CheckSelection(s); err != nil {
@@ -240,22 +263,26 @@ func attackableTargetConstraint(action Action) TargetConstraintFunc {
}
}
-func rangeTargetConstraint(source interface{}, r int) TargetConstraintFunc {
- var sourcePos Position
- switch source := source.(type) {
+func posFromTileOrPermanent(tileOrPermanent interface{}) Position {
+ switch obj := tileOrPermanent.(type) {
case *Tile:
- sourcePos = source.Position
+ return obj.Position
case Permanent:
- sourcePos = source.Tile().Position
+ return obj.Tile().Position
default:
- log.Fatalf("Unhandled source type %T in rangeTargetConstraint", source)
+ log.Fatalf("Unhandled source type %T in posFromTileOrPermanent", tileOrPermanent)
+ return INVALID_POSITION()
}
+}
+
+func rangeTargetConstraint(source interface{}, r int) TargetConstraintFunc {
+ sourcePos := posFromTileOrPermanent(source)
return func(t interface{}) (err error) {
- p, _ := t.(Permanent)
- if !IsPositionInRange(sourcePos, p.Tile().Position, r) {
+ targetPos := posFromTileOrPermanent(t)
+ if !IsPositionInRange(sourcePos, targetPos, r) {
err = fmt.Errorf("Position %v of target %v not in range %d of source's position %v",
- p.Tile().Position, t, r, sourcePos)
+ targetPos, t, r, sourcePos)
}
return
}
@@ -419,6 +446,16 @@ func parseTileTargetConstraint(desc string, s *State, action Action) []TargetCon
constraints = append(constraints, rangeTargetConstraint(action.Source(), 1))
}
+ if strings.Contains(desc, "free") {
+ constraints = append(constraints, func(t interface{}) (err error) {
+ tile := t.(*Tile)
+ if tile.IsFree() {
+ return nil
+ }
+ return fmt.Errorf("tile %v is occupied by %v", tile, tile.Permanent)
+ })
+ }
+
return constraints
}
@@ -461,7 +498,13 @@ func parseCardTargetConstraint(desc string, s *State, action Action) []TargetCon
case "hand":
pocs = append(pocs, p.Hand)
case "store":
- pocs = append(pocs, p.Store)
+ if p.gameState.Map.HasStores() {
+ for _, s := range p.AvailableStores() {
+ pocs = append(pocs, s)
+ }
+ } else {
+ pocs = append(pocs, p.Store)
+ }
case "discard pile":
pocs = append(pocs, p.DiscardPile)
}
diff --git a/go/game/tile.go b/go/game/tile.go
index 510497f8..ce7518cb 100644
--- a/go/game/tile.go
+++ b/go/game/tile.go
@@ -71,6 +71,15 @@ var TileNames = map[string]TileType{
"store": store,
}
+var farmEffect areaEffect = newGrantFullActionEffect("misc/farmer",
+ func(a Action) ActionResolveFunc {
+ u := a.Source().(*Unit)
+ return func(s *State) {
+ controller := u.Controller()
+ controller.gainResource(3)
+ }
+ }, "gain 3 resource", "farmAction")
+
type Tile struct {
Position Position
Permanent Permanent
@@ -88,8 +97,8 @@ func INVALID_TILE() Tile {
return Tile{Position: INVALID_POSITION()}
}
-func NewTileFromString(t string, pos Position) (Tile, error) {
- tile := strings.ToLower(t)
+func NewTileFromString(raw string, pos Position) (Tile, error) {
+ tile := strings.ToLower(raw)
tokens := strings.Split(tile, " ")
tileType, found := TileNames[tokens[0]]
if !found {
@@ -98,7 +107,14 @@ func NewTileFromString(t string, pos Position) (Tile, error) {
}
}
water := strings.Contains(tile, "water")
- return Tile{Position: pos, Type: tileType, Water: water, Raw: t}, nil
+
+ t := Tile{Position: pos, Type: tileType, Water: water, Raw: raw}
+
+ if tileType == TileTypes.Farm {
+ t.effects = append(t.effects, farmEffect)
+ }
+
+ return t, nil
}
func (t *Tile) IsFree() bool {
@@ -139,6 +155,11 @@ func (t *Tile) OnDiagonal(other *Tile) bool {
func (t *Tile) entering(p Permanent) {
t.Permanent = p
+
+ if t.Type == TileTypes.Store {
+ p.Controller().addKnownStore(t.Position)
+ }
+
for _, effect := range t.effects {
effect.onEntering(p)
}
@@ -153,6 +174,11 @@ func (t *Tile) leaving(p Permanent) {
func (t *Tile) addEffect(effect areaEffect) {
t.effects = append(t.effects, effect)
+
+ // Apply effect to the current Permanent
+ if t.Permanent != nil {
+ effect.onEntering(t.Permanent)
+ }
}
func (t *Tile) removeEffect(effect areaEffect) {
@@ -163,6 +189,11 @@ func (t *Tile) removeEffect(effect areaEffect) {
break
}
}
+
+ // Remove effect from the current Permanent
+ if t.Permanent != nil {
+ effect.onLeaving(t.Permanent)
+ }
}
func (t *Tile) neutralize() {
diff --git a/go/game/unit.go b/go/game/unit.go
index 94458c07..adb608ab 100644
--- a/go/game/unit.go
+++ b/go/game/unit.go
@@ -251,3 +251,23 @@ func (u *Unit) onUnpile(containing Permanent) {
func (u *Unit) onDrop(containing Permanent) {
u.onUnpile(containing)
}
+
+func (u *Unit) removeFullAction(tag string) {
+ for i, fa := range u.FullActions {
+ if fa.tag != tag {
+ continue
+ }
+ u.FullActions[i] = u.FullActions[len(u.FullActions)-1]
+ u.FullActions = u.FullActions[:len(u.FullActions)-1]
+ break
+ }
+}
+
+func (u *Unit) AddDamage(damage int) {
+ if u.Marks(UnitMarks.Ward) > 0 {
+ u.adjustMarks(UnitMarks.Ward, -1)
+ return
+ }
+
+ u.PermanentBase.AddDamage(damage)
+}
diff --git a/go/ui/cardGrid.go b/go/ui/cardGrid.go
index 59e25f2c..abe1d60b 100644
--- a/go/ui/cardGrid.go
+++ b/go/ui/cardGrid.go
@@ -98,6 +98,7 @@ func (w *CardGrid) render() *ebiten.Image {
op.GeoM.Translate(xOffset, yOffset)
if slices.Contains(w.highlights, card) {
+ log.Println("Highlighting card", card.Name)
op.ColorM.Scale(1, 0, 0, 0.5)
}
@@ -153,6 +154,8 @@ func (w *CardGrid) FindObjectAt(_x, _y int) interface{} {
func (w *CardGrid) setHighlights(cards []*game.Card) {
w.highlights = cards
+ // Reset the grid card count to force grid to be redrawn containing the highlights
+ w.gridCards = 0
w.ForceRedraw()
}
diff --git a/go/ui/mapView.go b/go/ui/mapView.go
index a6fd55c1..697eb774 100644
--- a/go/ui/mapView.go
+++ b/go/ui/mapView.go
@@ -266,6 +266,10 @@ func (vw *MapView) HighlightPermanents(permanents []game.Permanent) {
vw.ForceRedraw()
}
+func (vw *MapView) AddHighlightPermanent(p game.Permanent) {
+ vw.HighlightPermanents(append(vw.permanentsHighlights, p))
+}
+
func (vw *MapView) HighlightPermanent(p game.Permanent) {
vw.HighlightPermanents([]game.Permanent{p})
}
diff --git a/go/ui/prompt.go b/go/ui/prompt.go
index af5fca1d..b8d60a49 100644
--- a/go/ui/prompt.go
+++ b/go/ui/prompt.go
@@ -26,8 +26,12 @@ func NewPrompt(y, width int, action game.Action, promptText string) *Prompt {
return p
}
-func (p *Prompt) Add(obj interface{}) {
- p.action.Targets().AddSelection(obj)
+func (p *Prompt) Add(obj interface{}) error {
+ if handCard, ok := obj.(HandCard); ok {
+ obj = handCard.C
+ }
+
+ return p.action.Targets().AddSelection(obj)
}
func (p *Prompt) Confirm() game.Action {
diff --git a/go/ui/textBox.go b/go/ui/textBox.go
index 7f1ab98c..2635535c 100644
--- a/go/ui/textBox.go
+++ b/go/ui/textBox.go
@@ -153,15 +153,26 @@ func (w *PocList) setText() {
w.Y = w.bottomY - b.Dy() - w.YMargin
}
-func (w *PocList) FindObjectAt(x, y int) interface{} {
+func (w *PocList) FindObjectAt(_x, _y int) interface{} {
+ if !w.Contains(_x, _y) {
+ return nil
+ }
+
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]
+ y := _y - w.Y
+
+ b := text.BoundString(font.Font24, w.text)
+ i := y / (b.Dy() / w.poc.Size())
+
+ if i >= len(cards) {
+ return nil
+ }
+
+ return cards[i]
}
type TextInput struct {