package main import ( "fmt" "log" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/inpututil" "muhq.space/muhqs-game/go/game" "muhq.space/muhqs-game/go/ui" ) const ( DEFAULT_BUTTON_HEIGHT = 40 PASS_BUTTON_WIDTH = 100 RESOLVE_BUTTON_X = 500 STACK_BUFFER_WIDTH = 300 HOVER_THRESHOLD = 60 HAND_VIEW_WIDTH = 500 STATE_BAR_WIDTH = 300 POC_BUTTON_WIDTH = 150 ) type Game struct { selectedObject interface{} handLayer *ui.HandView stateBar *ui.StateBar mapView *ui.MapView choice *ui.Choice passButton *ui.SimpleButton discardPileView *ui.PocList discardPileButton *ui.SimpleButton storeView *ui.PocList storeButton *ui.SimpleButton stackBuffer *ui.Buffer prompt *ui.Prompt ui.Collection activePlayer *game.Player playerCtrl *game.ChanPlayerControl hasPriority bool gameState *game.State storesOnMap bool } func newGame(app *app) *Game { g := &Game{ Collection: ui.Collection{ Width: app.windowWidth, Height: app.windowHeight, }, } g.gameState = game.NewState() g.passButton = ui.NewSimpleButton(g.Width-PASS_BUTTON_WIDTH, g.Height-DEFAULT_BUTTON_HEIGHT, PASS_BUTTON_WIDTH, DEFAULT_BUTTON_HEIGHT, "Pass", func(*ui.SimpleButton) { if g.prompt != nil { a := g.prompt.Confirm() g.declareAction(a) g.RemoveWidget(g.prompt) g.prompt = nil } else if g.hasPriority { g.passPriority() } }) g.stackBuffer = ui.NewBuffer(g.Width-STACK_BUFFER_WIDTH, g.passButton.Y-400, STACK_BUFFER_WIDTH, 400) return g } func (g *Game) loadMap(mapName string) *Game { m, err := game.GetMap(mapName) if err != nil { log.Fatal(err) } g.gameState.Map = m g.mapView = ui.NewMapView(g.gameState) g.AddWidget(g.mapView) storeTiles := m.FilterTiles(func(t *game.Tile) bool { return t.Type == game.TileTypes.Store }) g.storesOnMap = len(storeTiles) > 0 return g } func (g *Game) addActivePlayer(name string, deckList string) *Game { deck := game.NewDeckFromDeckList(deckList) color := playerColors[len(g.gameState.Players)] g.gameState.AddNewPlayer(name, deck) p := g.getPlayer(name) p.Color = color return g.initPlayerUi(p) } func (g *Game) getPlayer(name string) *game.Player { for _, p := range g.gameState.Players { if p.Name == name { return p } } return nil } func (g *Game) initPlayerUi(player *game.Player) *Game { g.activePlayer = player g.playerCtrl = game.NewChanPlayerControl(player) player.Ctrl = g.playerCtrl var x, y int g.handLayer = ui.NewHandView(0, g.mapView.Height(), HAND_VIEW_WIDTH, g.Height-g.mapView.Height()-DEFAULT_BUTTON_HEIGHT, player.Hand, true) g.AddWidget(g.handLayer) g.stateBar = ui.NewStateBar(0, g.Height-DEFAULT_BUTTON_HEIGHT, STATE_BAR_WIDTH, DEFAULT_BUTTON_HEIGHT, g.gameState, player) g.AddWidget(g.stateBar) g.discardPileButton = ui.NewSimpleButton(g.stateBar.Width, g.Height-DEFAULT_BUTTON_HEIGHT, POC_BUTTON_WIDTH, DEFAULT_BUTTON_HEIGHT, "DiscardPile", func(*ui.SimpleButton) { if idx := g.FindWidget(g.discardPileView); idx == -1 { g.discardPileView.ForceRedraw() g.AddWidget(g.discardPileView) } else { g.RemoveWidget(g.discardPileView) } }) g.AddWidget(g.discardPileButton) x = g.discardPileButton.X + g.discardPileButton.Width/2 y = g.Height - DEFAULT_BUTTON_HEIGHT g.discardPileView = ui.NewPocList(x, y, g.activePlayer.DiscardPile) if !g.storesOnMap { x = g.stateBar.Width + g.discardPileButton.Width y = g.Height - DEFAULT_BUTTON_HEIGHT g.storeButton = ui.NewSimpleButton(x, y, POC_BUTTON_WIDTH, DEFAULT_BUTTON_HEIGHT, "Store", func(*ui.SimpleButton) { if idx := g.FindWidget(g.storeView); idx == -1 { g.showStore() } else { g.hideStore() } }) g.AddWidget(g.storeButton) x = g.storeButton.X + g.storeButton.Width/2 y = g.Height - DEFAULT_BUTTON_HEIGHT g.storeView = ui.NewPocList(x, y, g.activePlayer.Store) } return g } func (g *Game) showStore() { g.storeView.ForceRedraw() g.AddWidget(g.storeView) } func (g *Game) hideStore() { g.RemoveWidget(g.storeView) } 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.CurrentlyAvailActions()) > 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.CurrentlyAvailActions() if len(actions) > 0 { g.selectedObject = perm onClick := func(c *ui.Choice, x, y int) { g.removeChoice() a := actions[c.GetChoosen(x, y)] 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) } } func (g *Game) removeChoice() { g.RemoveWidget(g.choice) g.choice = nil } 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.Panicf("Unhandled highlight of type %T", obj) } } func (g *Game) clearMapHighlights() { g.mapView.ClearPermanentsHighlights() g.mapView.ClearTileHighlights() g.mapView.ForceRedraw() } func (g *Game) declareAction(a game.Action) { g.playerCtrl.SendAction(a) g.clearMapHighlights() g.handLayer.ClearHighlights() g.hasPriority = false g.hidePassButton() g.stateBar.ForceRedraw() } func (g *Game) showPassButton() { g.AddWidget(g.passButton) } func (g *Game) hidePassButton() { g.RemoveWidget(g.passButton) } func (g *Game) passPriority() { g.declareAction(game.NewPassPriority(g.activePlayer)) } func (g *Game) handlePlayerNotifications() { n := g.playerCtrl.RecvNotification() for n != nil { log.Println("Received", n) switch n.Notification { case game.PriorityNotification: g.showPassButton() g.hasPriority = true case game.DeclaredActionNotification: if n.Error != nil { log.Fatal(n.Error) } a := n.Context.(game.Action) g.stackBuffer.AddLine(a.String()) if !g.gameState.Stack.IsEmpty() && g.FindWidget(g.stackBuffer) == -1 { g.AddWidget(g.stackBuffer) } case game.ResolvedActionNotification: g.clearMapHighlights() g.stackBuffer.RemoveLast() if g.gameState.Stack.IsEmpty() { g.RemoveWidget(g.stackBuffer) } g.mapView.ForceRedraw() case game.TargetSelectionPrompt: ctx := n.Context.(game.TargetSelectionCtx) if _, ok := ctx.Action.(*game.BuyAction); ok { if !g.storesOnMap { g.showStore() } } g.prompt = ui.NewPrompt(g.Height/2, g.Width, ctx.Action, ctx.Prompt) g.AddWidget(g.prompt) g.showPassButton() } g.stateBar.ForceRedraw() g.handLayer.ForceRedraw() n = g.playerCtrl.RecvNotification() } } func (g *Game) findObjectAt(x, y int) interface{} { // Iterate the widget in reverse order to ensure that objects are found // in the newer widget possibly drawn over older ones widgets := g.Widgets() for i := len(widgets) - 1; i >= 0; i-- { w := widgets[i] if obj := w.FindObjectAt(x, y); obj != nil { return obj } } if obj := g.mapView.FindObjectAt(x, y); obj != nil { return obj } return nil } func (g *Game) handleTargetSelection(obj interface{}) { a := g.selectedObject.(game.Action) 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) g.selectedObject = nil } } func (g *Game) handleSelection(obj interface{}, x, y int) { if obj == nil { fmt.Printf("No object found at cursor position (%d, %d)\n", x, y) return } if _, ok := g.selectedObject.(game.Action); ok { g.handleTargetSelection(obj) } else { switch obj := obj.(type) { case *game.Tile: 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 g.addPermActionChoice(perm, x, y) case ui.HandCard: g.selectedObject = game.NewPlayAction(g.activePlayer, obj.C) g.handLayer.HighlightCard(obj.C) default: log.Panicf("Object of type %T not handled", obj) } } } func (g *Game) highlightTargets() { a, ok := g.selectedObject.(game.Action) if !ok { return } options := a.Targets().Options() for _, option := range options { g.AddHighlight(option) } } func (g *Game) Update() error { g.handlePlayerNotifications() g.highlightTargets() if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { x, y := ebiten.CursorPosition() obj := g.findObjectAt(x, y) if g.prompt != nil { 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) } } else if inpututil.IsKeyJustPressed(ebiten.KeySpace) { g.passButton.Click(0, 0) } else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { g.removeChoice() g.selectedObject = nil g.ResetHover() g.clearMapHighlights() g.handLayer.ClearHighlights() g.hideStore() g.RemoveWidget(g.discardPileView) if g.prompt != nil { g.prompt.ClearSelections() } } return g.Collection.Update() } func (g *Game) Draw(screen *ebiten.Image) { g.Collection.Draw(screen) ebitenutil.DebugPrint(screen, fmt.Sprintf("TPS: %0.2f FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS())) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return g.Width, g.Height } func (g *Game) Start() *Game { go func() { w := g.gameState.Loop() log.Println("Winners are", w) }() return g }