diff options
| author | Florian Fischer <florian.fischer@muhq.space> | 2023-02-15 13:13:06 +0100 |
|---|---|---|
| committer | Florian Fischer <florian.fischer@muhq.space> | 2025-01-27 16:43:49 +0100 |
| commit | f9ab2cfdb25eb3fcd85868a2e375326a9c8b6022 (patch) | |
| tree | b96f16e6cf4a6dcf93b9557a7219179ab2f46c8d | |
| parent | 805cbf397589e5cb861f866713c2eb4bec9ce40b (diff) | |
| download | muhqs-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/TODO | 3 | ||||
| -rw-r--r-- | go/client/game.go | 95 | ||||
| -rw-r--r-- | go/game/action.go | 77 | ||||
| -rw-r--r-- | go/game/ai.go | 4 | ||||
| -rw-r--r-- | go/game/areaEffect.go | 24 | ||||
| -rw-r--r-- | go/game/cardImplementations.go | 56 | ||||
| -rw-r--r-- | go/game/deck.go | 4 | ||||
| -rw-r--r-- | go/game/map.go | 8 | ||||
| -rw-r--r-- | go/game/marks.go | 4 | ||||
| -rw-r--r-- | go/game/permanent.go | 6 | ||||
| -rw-r--r-- | go/game/player.go | 63 | ||||
| -rw-r--r-- | go/game/playerControl.go | 19 | ||||
| -rw-r--r-- | go/game/stack.go | 5 | ||||
| -rw-r--r-- | go/game/state.go | 18 | ||||
| -rw-r--r-- | go/game/targets.go | 99 | ||||
| -rw-r--r-- | go/game/tile.go | 37 | ||||
| -rw-r--r-- | go/game/unit.go | 20 | ||||
| -rw-r--r-- | go/ui/cardGrid.go | 3 | ||||
| -rw-r--r-- | go/ui/mapView.go | 4 | ||||
| -rw-r--r-- | go/ui/prompt.go | 8 | ||||
| -rw-r--r-- | go/ui/textBox.go | 19 |
21 files changed, 393 insertions, 183 deletions
@@ -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 { |
