package main import ( "fmt" "image/color" "log" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/inpututil" "muhq.space/muhqs-game/go/font" "muhq.space/muhqs-game/go/game" "muhq.space/muhqs-game/go/ui" ) const ( screenWidth = 800 screenHeight = 1024 DEFAULT_BUTTON_HEIGHT = 40 PASS_BUTTON_WIDTH = 100 RESOLVE_BUTTON_X = 500 STACK_BUFFER_WIDTH = 300 HOVER_THRESHOLD = 60 HAND_VIEW_HEIGHT = 500 HAND_VIEW_WIDTH = 500 STATE_BAR_WIDTH = 300 ) var playerColors = []color.RGBA{ {1, 0xff, 1, 0xff}, {1, 1, 0xff, 0xff}, } // A simple structure to detect if the cursor has not moved for HOVER_THRESHOLD // ticks. // hoverDetector.update must be called during each Update invocation to track the // frame and the current cursor position. type hoverDetector struct { last_x, last_y int ticks int // ResetFunc is called when the cursor has moved. // If resetFunc returns true the hoverDetector is reset // otherwise the reset is skipped. resetFunc func() bool } func (h *hoverDetector) update(x, y int) { if x == h.last_x && y == h.last_y { h.ticks++ } else { h.last_x, h.last_y = x, y h.reset() } } func (h *hoverDetector) reset() { if h.resetFunc != nil && !h.resetFunc() { return } h.ticks = 0 h.resetFunc = nil } func (h *hoverDetector) startedHovering() bool { return h.ticks == HOVER_THRESHOLD } type Game struct { selectedObject interface{} hoverDetector hoverDetector handLayer *ui.HandView stateBar *ui.StateBar mapView *ui.MapView actionChoice *ui.Choice passButton *ui.SimpleButton discardPileView *ui.PocList discardPileButton *ui.SimpleButton storeView *ui.PocList storeButton *ui.SimpleButton stackBuffer *ui.Buffer prompt *ui.Prompt widgets []ui.Widget activePlayer *game.Player playerCtrl *game.ChanPlayerControl hasPriority bool gameState *game.State storesOnMap bool } func NewGame() *Game { g := &Game{} g.widgets = []ui.Widget{} g.gameState = game.NewState() g.passButton = ui.NewSimpleButton(screenWidth-PASS_BUTTON_WIDTH, screenHeight-DEFAULT_BUTTON_HEIGHT, PASS_BUTTON_WIDTH, DEFAULT_BUTTON_HEIGHT, "Pass", func(*ui.SimpleButton) { if g.prompt != nil { a := g.prompt.Confirm() log.Println("Prompt confirmation returned", a) g.declareAction(a) g.prompt = nil g.removeWidget(g.prompt) } else if g.hasPriority { g.passPriority() } }) g.stackBuffer = ui.NewBuffer(screenWidth-STACK_BUFFER_WIDTH, g.passButton.Y-400, STACK_BUFFER_WIDTH, 400) return g } func NewGameFromMap(mapName string) *Game { g := NewGame() g.loadMap(mapName) return g } func (g *Game) loadMap(mapName string) { m, err := game.GetMap(mapName) if err != nil { log.Fatal(err) } g.gameState.Map = m g.mapView = ui.NewMapView(g.gameState) storeTiles := m.FilterTiles(func(t *game.Tile) bool { return t.Type == game.TileTypes.Store }) g.storesOnMap = len(storeTiles) > 0 } // func NewGameFromMapWithPlayers(mapName string, players []string, decks []*game.Deck) *Game { // g := NewGameFromMap(mapName) // activePlayer := g.addPlayer(players[0], decks[0], ) // g.initPlayerUi(activePlayer) // for i, p := range players[1:] { // g.addPlayer(p, decks[i+1]) // } // return g // } func (g *Game) addPlayer(name string, deck *game.Deck) *game.Player { color := playerColors[len(g.gameState.Players)] g.gameState.AddNewPlayer(name, deck, color) return g.getPlayer(name) } 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) { g.activePlayer = player g.playerCtrl = game.NewChanPlayerControl() player.Ctrl = g.playerCtrl var x, y int g.handLayer = ui.NewHandView(0, g.mapView.Height(), HAND_VIEW_WIDTH, screenHeight-g.mapView.Height()-DEFAULT_BUTTON_HEIGHT, player.Hand, true) g.addWidget(g.handLayer) g.stateBar = ui.NewStateBar(0, screenHeight-DEFAULT_BUTTON_HEIGHT, STATE_BAR_WIDTH, DEFAULT_BUTTON_HEIGHT, g.gameState, player) g.addWidget(g.stateBar) g.discardPileButton = ui.NewSimpleButton(g.stateBar.Width, screenHeight-DEFAULT_BUTTON_HEIGHT, 150, 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 = screenHeight - DEFAULT_BUTTON_HEIGHT g.discardPileView = ui.NewPocList(x, y, g.activePlayer.DiscardPile) if !g.storesOnMap { x = g.stateBar.Width + g.discardPileButton.Width y = screenHeight - DEFAULT_BUTTON_HEIGHT g.storeButton = ui.NewSimpleButton(x, y, 150, DEFAULT_BUTTON_HEIGHT, "Store", func(*ui.SimpleButton) { if idx := g.findWidget(g.storeView); idx == -1 { g.showStore() } else { g.hideStore() } }) g.addWidget(g.storeView) x = g.storeButton.X + g.storeButton.Width/2 y = screenHeight - DEFAULT_BUTTON_HEIGHT g.storeView = ui.NewPocList(x, y, g.activePlayer.Store) } } func (g *Game) showStore() { g.storeView.ForceRedraw() g.addWidget(g.storeView) } func (g *Game) hideStore() { g.removeWidget(g.storeView) } func (g *Game) addWidget(w ui.Widget) { g.widgets = append(g.widgets, w) } func (g *Game) findWidget(widget ui.Widget) int { for i, w := range g.widgets { if w != widget { continue } return i } return -1 } func (g *Game) removeWidget(widget ui.Widget) { idx := g.findWidget(widget) if idx != -1 { g.removeWidgetAt(idx) } } func (g *Game) removeWidgetAt(idx int) { nwidgets := len(g.widgets) g.widgets[idx] = g.widgets[nwidgets-1] g.widgets = g.widgets[:nwidgets-1] } func (g *Game) addActionChoice(perm game.Permanent, x, y int) { actions := perm.AvailActionPrototypes() if len(actions) > 0 { g.selectedObject = perm onClick := func(c *ui.Choice, x, y int) { g.removeActionChoice() g.selectedObject = actions[c.GetChoosen(x, y)] } g.actionChoice = ui.NewActionChoice(x, y, actions, onClick) g.addWidget(g.actionChoice) } } func (g *Game) removeActionChoice() { g.removeWidget(g.actionChoice) g.actionChoice = nil } func (g *Game) AddHighlight(obj interface{}) { switch obj := obj.(type) { case ui.HandCard: g.handLayer.AddHighlightCard(obj.C) case *game.Tile: g.mapView.AddHighlightTile(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 { 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()) case game.ResolvedActionNotification: g.clearMapHighlights() g.stackBuffer.RemoveLast() g.mapView.ForceRedraw() case game.BuyPrompt: if !g.storesOnMap { g.showStore() } case game.HandCardSelectionPrompt: ctx := n.Context.([]int) min, max := ctx[0], ctx[1] promptTxt := fmt.Sprintf("Select between %d and %d hand cards", min, max) g.prompt = ui.NewHandCardPrompt(screenHeight/2, screenWidth, promptTxt) 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 for i := len(g.widgets) - 1; i >= 0; i-- { w := g.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) handleSelection(obj interface{}, x, y int) { if obj == nil { fmt.Printf("No object found at cursor position (%d, %d)\n", x, y) return } switch obj := obj.(type) { case ui.Button: obj.Click(x, y) case *game.Tile: tile := obj if g.selectedObject != nil { var action game.Action switch obj := g.selectedObject.(type) { case *game.MoveActionPrototype: action = obj.Specialize(g.gameState, tile) case *game.Card: if obj.IsPermanent() { action = game.NewPlayAction(g.activePlayer, obj, tile.Position) } } if action != nil { g.declareAction(action) g.selectedObject = nil g.mapView.HighlightTile(tile) } } else { g.mapView.HighlightTile(tile) } case game.Permanent: perm := obj if g.selectedObject == nil { g.addActionChoice(perm, x, y) } else { switch obj := g.selectedObject.(type) { case *game.AttackActionPrototype: action := obj.Specialize(g.gameState, perm) g.selectedObject = nil g.declareAction(action) g.mapView.HighlightPermanent(perm) } } case ui.HandCard: g.selectedObject = obj.C case *game.Card: if g.gameState.IsActivePlayer(g.activePlayer) && g.gameState.ActivePhase == game.Phases.BuyPhase { g.declareAction(game.NewBuyAction(g.activePlayer, obj)) } default: log.Fatalf("Object of type %T not handled", obj) } } func (g *Game) hightlightSelection(obj interface{}) { switch obj := g.selectedObject.(type) { case *game.MoveActionPrototype: g.mapView.HighlightTiles(obj.Unit.MoveRangeTiles(g.gameState.Map)) case *game.AttackActionPrototype: perms := obj.Unit.AttackablePermanents(g.gameState.Map) g.mapView.HighlightPermanents(perms) case *game.Card: g.handLayer.HighlightCard(obj) if obj.IsPermanent() { spawnTiles := g.gameState.AvailableSpawnTiles(g.activePlayer, obj) g.mapView.HighlightTiles(spawnTiles) } } } func (g *Game) handleHover(obj interface{}, x, y int) { var hoverHint ui.Widget switch obj := obj.(type) { case *game.Card: wx, wy := x, y if x+500 > screenWidth || y+700 > screenHeight { wx = x - 500 wy = y - 700 } hoverHint = ui.NewScaledCardView(wx, wy, 500, 700, 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 } } } if hoverHint != nil { g.addWidget(hoverHint) // Set the default reset function removing the hoverHint if g.hoverDetector.resetFunc == nil { g.hoverDetector.resetFunc = func() bool { g.removeWidget(hoverHint) return true } } } } func (g *Game) Update() error { g.handlePlayerNotifications() x, y := ebiten.CursorPosition() g.hoverDetector.update(x, y) obj := g.findObjectAt(x, y) if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { if g.prompt != nil { g.prompt.Add(obj) g.AddHighlight(obj) } else { g.handleSelection(obj, x, y) } g.hightlightSelection(obj) } else if g.hoverDetector.startedHovering() && g.selectedObject == nil && obj != nil { g.handleHover(obj, x, y) } else if inpututil.IsKeyJustPressed(ebiten.KeySpace) { g.passButton.Click(0, 0) } else if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { g.removeActionChoice() g.selectedObject = nil g.hoverDetector.reset() g.clearMapHighlights() g.handLayer.ClearHighlights() } return nil } func (g *Game) Draw(screen *ebiten.Image) { g.mapView.Draw(screen) if !g.gameState.Stack.IsEmpty() { g.stackBuffer.Draw(screen) } for _, b := range g.widgets { b.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 screenWidth, screenHeight } func main() { font.InitFont() ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowTitle("Muhq's Game") g := NewGameFromMap("the-kraken") nautics := `1 nautics/barge 1 nautics/captain 1 nautics/fisher 1 nautics/galley` muhq := g.addPlayer("muhq", game.NewDeckFromDeckList(nautics)) g.initPlayerUi(muhq) krakenId := len(g.gameState.Players) kraken := game.NewKraken(krakenId, g.gameState, playerColors[krakenId]) g.gameState.Players = append(g.gameState.Players, kraken) go func() { w := g.gameState.Loop() log.Println("Winners are", w) }() if err := ebiten.RunGame(g); err != nil { log.Fatal(err) } }