package main import ( "fmt" "image/color" "log" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "muhq.space/muhqs-game/go/activities" "muhq.space/muhqs-game/go/game" "muhq.space/muhqs-game/go/ui" "muhq.space/muhqs-game/go/utils" ) 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 AUTO_PROGRESS_PROMPT = true ) type Game struct { app *app keyBindings keyBindings selectedObject interface{} handLayer *ui.HandView stateBar *ui.StateBar mapView *ui.MapView choice ui.Widget passButton *ui.SimpleButton resetButton *ui.SimpleButton discardPileView *ui.PocList discardPileButton *ui.SimpleButton storeVisible bool storeView *ui.PocList storeButton *ui.SimpleButton stackBuffer *ui.Buffer prompt *ui.Prompt triggers []*game.TriggeredAction ui.Collection activePlayerId int winners []*game.Player done bool playerCtrl *game.ChanPlayerControl hasPriority bool gameState game.State storesOnMap bool } func newGame(app *app, gameState game.State) *Game { g := &Game{ app: app, gameState: gameState, keyBindings: make(keyBindings), Collection: ui.Collection{ Width: app.windowWidth, Height: app.windowHeight, }, } // default key bindings g.keyBindings[ebiten.MouseButtonLeft] = selection g.keyBindings[ui.Tap] = selection g.keyBindings[ebiten.KeySpace] = pass g.keyBindings[ebiten.KeyEscape] = reset g.keyBindings[ebiten.KeyQ] = func(ui.InputEvent, *Game) (bool, error) { return false, ebiten.Termination } 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 { g.progressPrompt() } else if g.hasPriority { g.passPriority() } }) g.resetButton = ui.NewRoundSimpleButton( 10, g.Height/2, DEFAULT_BUTTON_HEIGHT, "X", func(*ui.SimpleButton) { g.reset() }) 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.SetMap(m) g.initMapUi() return g } func (g *Game) initMapUi() { g.mapView = ui.NewMapView(g.gameState) g.AddWidget(g.mapView) storeTiles := g.gameState.Map().FilterTiles(func(t *game.Tile) bool { return t.Type == game.TileTypes.Store }) g.storesOnMap = len(storeTiles) > 0 } func (g *Game) addActivePlayer(name string, deckList string) *Game { deck := game.NewDeckFromDeckList(deckList) g.gameState.AddNewPlayer(name, deck) p := g.gameState.PlayerByName(name) g.activePlayerId = p.Id return g.initPlayerUi(p) } func (g *Game) activePlayer() *game.Player { return g.gameState.PlayerById(g.activePlayerId) } func (g *Game) initPlayerUi(player *game.Player) *Game { 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 !g.storeVisible { 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) showWinners() { g.done = true msg := g.winners[0].Name for _, p := range g.winners[1:] { msg = msg + fmt.Sprintf(", %s", p.Name) } msg = msg + " won" g.AddWidget(ui.NewFixedTextBox(0, g.Height/2, g.Width, ui.PROMPT_HEIGHT, msg).Centering(true)) g.passButton.Bg = ui.HighlightSelectionColor g.passButton.UpdateLabel("back") g.passButton.RegisterHandler("click", func(int, int) { activities.PopActivity() }) } func (g *Game) showStore() { if g.storeVisible { return } g.storeVisible = true g.storeView.ForceRedraw() g.AddWidget(g.storeView) } func (g *Game) hideStore() { g.storeVisible = false 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} labels := []string{perm.Card().Name} for _, p := range perm.Pile() { if len(p.CurrentlyAvailActions()) > 0 { perms = append(perms, p) labels = append(labels, p.Card().Name) } } if len(perms) > 1 { onClick := func(c *ui.Choice, x, y int) { g.removeChoice() p := perms[c.GetChoosen(x, y)] g.addActionChoice(p, x, y) } g.addChoice(ui.NewChoice(x, y, utils.TypedSliceToInterfaceSlice(labels), onClick)) } else { g.addActionChoice(perms[0], x, y) } } func (g *Game) declareOrPromptTargets(a game.Action) { if a.Targets() != nil && a.Targets().AllowSelection() { g.addCancelablePrompt(a, fmt.Sprintf("Select targets for %v", a)) } else { g.declareAction(a) } } func (g *Game) addActionCostChoice(a game.Action, x, y int) { u := a.Source().(*game.Unit) if u.AvailMoveActions > 0 && u.AvailAttackActions == 0 { game.UseMoveAction(a) } else if u.AvailAttackActions > 0 && u.AvailMoveActions == 0 { game.UseAttackAction(a) } if !a.NeedsActionCostChoice() { g.declareOrPromptTargets(a) return } choices := []string{"move", "attack"} onClick := func(c *ui.Choice, x, y int) { g.removeChoice() switch choices[c.GetChoosen(x, y)] { case "move": game.UseMoveAction(a) case "attack": game.UseAttackAction(a) } g.declareOrPromptTargets(a) } g.addChoice(ui.NewChoice(x, y, utils.TypedSliceToInterfaceSlice(choices), onClick)) } 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.NeedsActionCostChoice() { g.addActionCostChoice(a, x, y) } else { g.declareOrPromptTargets(a) } } g.addChoice(ui.NewActionChoice(x, y, actions, onClick)) } } func (g *Game) addChoice(choice ui.Widget) { if g.choice != nil { g.removeChoice() } g.choice = choice g.AddWidget(choice) } func (g *Game) removeChoice() { g.RemoveWidget(g.choice) g.choice = nil } func (g *Game) addCancelablePrompt(action game.Action, prompt string) { g._addPrompt(action, prompt, true) } func (g *Game) addPrompt(action game.Action, prompt string) { g._addPrompt(action, prompt, false) } func (g *Game) _addPrompt(action game.Action, prompt string, cancelable bool) { if g.prompt != nil { g.removePrompt() } if cancelable { g.prompt = ui.NewCancelablePrompt(g.Height/2, g.Width, action, prompt) } else { g.prompt = ui.NewPrompt(g.Height/2, g.Width, action, prompt) } g.AddWidget(g.prompt) if cancelable { g.show(g.resetButton) } g.updateHighlight() } func (g *Game) progressPrompt() { a := g.prompt.Action() targets := a.Targets() if targets.RequireSelection() { targets.Next() g.updateHighlight() } else { g.declareAction(a) g.removePrompt() } } func (g *Game) removePrompt() { g.RemoveWidget(g.prompt) g.RemoveWidget(g.resetButton) g.prompt = nil } func (g *Game) addTriggers(actions []*game.TriggeredAction) { if g.triggers != nil { log.Panicf("There is already an active trigger ui") } g.triggers = actions } func (g *Game) addHighlight(obj interface{}, color color.Color) { switch obj := obj.(type) { case ui.HandCard: g.handLayer.AddHighlightCard(obj.C, color) case *game.Card: g.handLayer.AddHighlightCard(obj, color) case *game.Tile: g.mapView.AddHighlightTile(obj, color) case *game.Unit: g.mapView.AddHighlightPermanent(obj, color) default: log.Panicf("Unhandled highlight of type %T", obj) } } func (g *Game) addHighlights(objs []interface{}, color color.Color) { for _, obj := range objs { g.addHighlight(obj, color) } } func (g *Game) clearMapHighlights() { g.mapView.ClearPermanentsHighlights() g.mapView.ClearTileHighlights() g.mapView.ForceRedraw() } func (g *Game) clearHighlights() { g.clearMapHighlights() g.handLayer.ClearHighlights() } func (g *Game) declareAction(a game.Action) { if _, ok := a.(*game.BuyAction); ok { g.hideStore() } g.playerCtrl.SendAction(a) g.clearHighlights() g.hasPriority = false g.stateBar.ForceRedraw() } // show ensures a widget is visible by adding it to the collection or moving it to the last position. func (g *Game) show(w ui.Widget) { idx := g.FindWidget(w) if idx == -1 { g.AddWidget(w) } else { g.MoveIdxToLast(idx) } } func (g *Game) hide(w ui.Widget) { g.RemoveWidget(g.passButton) } func (g *Game) updateButtons() { if g.prompt == nil && !g.hasPriority { g.hide(g.passButton) } passButtonLabel := "pass" if g.prompt != nil { targets := g.prompt.Action().Targets() if targets.RequireSelection() { if !targets.Cur().HasSelection() { g.hide(g.passButton) g.show(g.resetButton) return } passButtonLabel = "next" } else { passButtonLabel = "confirm" } if AUTO_PROGRESS_PROMPT && !targets.Cur().AllowSelection() { g.progressPrompt() return } } g.show(g.passButton) g.passButton.UpdateLabel(passButtonLabel) } func (g *Game) reset() { g.removeChoice() g.selectedObject = nil g.ResetHover() g.clearHighlights() g.hideStore() g.RemoveWidget(g.discardPileView) g.RemoveWidget(g.resetButton) if g.prompt != nil { if g.prompt.Action().Targets().NoSelections() { g.removePrompt() } else { g.prompt.ClearSelections() } } } func (g *Game) passPriority() { g.declareAction(game.NewPassPriority(g.activePlayer())) } func (g *Game) handlePlayerNotifications() { n, err := g.playerCtrl.RecvNotification() if err != nil { // FIXME } for n.Valid() { log.Println("Received", n) switch n.Notification { case game.PriorityNotification: 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.addPrompt(ctx.Action, ctx.Prompt) case game.DeclareTriggeredActionsPrompt: triggeredActions := n.Context.([]*game.TriggeredAction) // TODO: present triggered action for ordering and target selection g.playerCtrl.SendAction( game.NewDeclareTriggeredActionsAction(triggeredActions)) } g.stateBar.ForceRedraw() g.handLayer.ForceRedraw() n, err = g.playerCtrl.RecvNotification() if err != nil { // FIXME } } } 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) progressPlayAction(a *game.PlayAction) { targets := a.Targets() if targets != nil && targets.AllowSelection() { g.addCancelablePrompt(a, fmt.Sprintf("Select targets to play %v", a.Card.Name)) g.handLayer.HighlightCard(a.Card, ui.HighlightSelectionColor) } else { g.declareAction(a) } } func (g *Game) handleSelection(obj interface{}, x, y int) { if obj == nil { log.Printf("No object found at cursor position (%d, %d)\n", x, y) return } 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: if obj.C.IsPermanent() && !g.gameState.Stack().IsEmpty() { return } if obj.C.PlayCosts.IsVariadic() { g.addChoice(ui.NewNumberChoice( g.Width-ui.NUMBER_CHOICE_WIDTH, g.Height-ui.NUMBER_CHOICE_HEIGHT, 0, func(nc *ui.NumberChoice) { a := game.NewPlayActionVariadicCosts( g.activePlayer(), obj.C, nc.GetChoosen()) g.progressPlayAction(a) })) } else { a := game.NewPlayAction(g.activePlayer(), obj.C) g.progressPlayAction(a) } default: log.Panicf("Object of type %T not handled", obj) } } func (g *Game) updateHighlight() { g.clearHighlights() if g.prompt == nil { return } a := g.prompt.Action() if pa, ok := a.(*game.PlayAction); ok { g.addHighlight(ui.HandCard{C: pa.Card}, ui.HighlightOptionColor) } targets := a.Targets() if len(targets.Targets()) == 0 { return } for _, t := range targets.Targets() { g.addHighlights(t.Selection(), ui.HighlightSelectionColor) } target := targets.Cur() if target.AllowSelection() { options := target.Options() g.addHighlights(options, ui.HighlightOptionColor) } } func (g *Game) Update() error { if err := ui.Update(); err != nil { return err } if err := g.Collection.Update(); err != nil { return err } // We are done. Skip the rest. // Especially updating the button resetting the new passButton label. if g.done { return nil } else if len(g.winners) > 0 { g.showWinners() return nil } g.handlePlayerNotifications() if err := g.handleKeyBindings(g.keyBindings); err != nil { return err } g.updateButtons() return nil } 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() { g.winners = g.gameState.Loop() }() return g }