package ui import ( "log" "math" "strings" "golang.org/x/exp/slices" "github.com/hajimehoshi/ebiten/v2" "muhq.space/muhqs-game/go/assets" "muhq.space/muhqs-game/go/game" ) const ( PERMANENT_WIDTH int = 40 PERMANENT_HEIGHT int = 40 TILE_WIDTH int = 50 TILE_HEIGHT int = 50 ) type MapView struct { gameState *game.State mapLayer *ebiten.Image permanentsLayer *ebiten.Image tileHighlights []game.Position permanentsHighlights []game.Permanent } func NewMapView(g *game.State) *MapView { vw := MapView{g, nil, nil, []game.Position{}, []game.Permanent{}} return &vw } func rotateStreetImg(x, y int, radian float64, op *ebiten.DrawImageOptions) { // Move the image's center to the screen's upper-left corner. // This is a preparation for rotating. When geometry matrices are applied, // the origin point is the upper-left corner. op.GeoM.Translate(-float64(TILE_WIDTH)/2, -float64(TILE_HEIGHT)/2) // Rotate the image. As a result, the anchor point of this rotate is // the center of the image. op.GeoM.Rotate(radian) // Reset translation op.GeoM.Translate(float64(TILE_WIDTH)/2, float64(TILE_HEIGHT)/2) } func (vw *MapView) rotateStreet(x, y int, op *ebiten.DrawImageOptions) { connections, left, right, above, below := vw.gameState.Map.FindStreetConnections(x, y) if connections == 0 { // This street is not connected to another street -> // check any other non neutral tiles connections, left, right, above, below = vw.gameState.Map.FindAnyConnections(x, y) } // This street is not connected to anything. Seams odd! if connections == 0 { log.Printf("Street at (%d, %d) is not connected", x, y) } else if (connections == 1 || (connections == 2 && ((left && right) || (above && below)))) && (above || below) { rotateStreetImg(x, y, math.Pi/2, op) } else if connections == 2 { // normal orientation above and right if right && below { rotateStreetImg(x, y, 3*math.Pi/2, op) } else if left && below { rotateStreetImg(x, y, math.Pi, op) } else if above && left { rotateStreetImg(x, y, math.Pi/2, op) } } else if connections == 3 { // normal orientation left above right if above && right && below { rotateStreetImg(x, y, 3*math.Pi/2, op) } else if left && below && right { rotateStreetImg(x, y, math.Pi, op) } else if below && left && above { rotateStreetImg(x, y, math.Pi/2, op) } } } func (vw *MapView) newLayerImage() *ebiten.Image { // TODO: support non symetric maps maxWidth := len(vw.gameState.Map.Tiles[0]) * TILE_WIDTH maxHeight := len(vw.gameState.Map.Tiles) * TILE_HEIGHT return ebiten.NewImage(maxWidth, maxHeight) } func (vw *MapView) drawMapLayer(screen *ebiten.Image) { if vw.mapLayer == nil { vw.mapLayer = vw.newLayerImage() x_px, y_px := 0.0, 0.0 for y := 0; y < len(vw.gameState.Map.Tiles); y++ { for x := 0; x < len(vw.gameState.Map.Tiles[y]); x++ { pos := game.Position{X: x, Y: y} tile := vw.gameState.Map.TileAt(pos) tileImg := assets.GetTile(tile.Raw) op := &ebiten.DrawImageOptions{} // Rotate Street if tile.Type == game.TileTypes.Street { vw.rotateStreet(x, y, op) } op.GeoM.Translate(x_px, y_px) if slices.Contains(vw.tileHighlights, pos) { op.ColorM.Scale(1, 0, 0, 0.5) } vw.mapLayer.DrawImage(tileImg, op) x_px += float64(TILE_WIDTH) } x_px = 0 y_px += float64(TILE_HEIGHT) } } screen.DrawImage(vw.mapLayer, &ebiten.DrawImageOptions{}) } func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) { if vw.permanentsLayer == nil { vw.permanentsLayer = vw.newLayerImage() for _, p := range vw.gameState.Permanents { permanentSymbol := assets.GetSymbol(strings.ToLower(p.GetCard().Name)) // TODO: Implement generic symbols if permanentSymbol == nil { continue } t := p.GetTile() x_px := t.Position.X*TILE_WIDTH + (TILE_WIDTH-PERMANENT_WIDTH)/2 y_px := t.Position.Y*TILE_HEIGHT + (TILE_HEIGHT-PERMANENT_HEIGHT)/2 op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(x_px), float64(y_px)) op.ColorM.ScaleWithColor(p.GetController().Color) for _, h := range vw.permanentsHighlights { if p == h { op.ColorM.Scale(255, 0.5, 0.5, 1) break } } vw.permanentsLayer.DrawImage(permanentSymbol, op) } } screen.DrawImage(vw.permanentsLayer, &ebiten.DrawImageOptions{}) } func (vw *MapView) Draw(screen *ebiten.Image) { vw.drawMapLayer(screen) vw.drawPermanentsLayer(screen) } func (vw *MapView) ForceRedraw() { vw.mapLayer = nil vw.permanentsLayer = nil } func (vw *MapView) FindObjectAt(x_px, y_px int) interface{} { x := x_px / TILE_WIDTH y := y_px / TILE_HEIGHT if x < 0 || y < 0 { return nil } // TODO: detect if a permanent or the containing tile was selected for _, u := range vw.gameState.Permanents { pos := u.GetTile().Position if pos.X == x && pos.Y == y { return u } } if y < len(vw.gameState.Map.Tiles) && x < len(vw.gameState.Map.Tiles[y]) { return &vw.gameState.Map.Tiles[y][x] } return nil } func (vw *MapView) HighlightPositions(pos []game.Position) { vw.ForceRedraw() vw.tileHighlights = pos } func (vw *MapView) HighlightTiles(tiles []*game.Tile) { pos := make([]game.Position, 0, len(tiles)) for _, t := range tiles { pos = append(pos, t.Position) } vw.HighlightPositions(pos) } func (vw *MapView) HighlightTile(t *game.Tile) { vw.HighlightTiles([]*game.Tile{t}) } func (vw *MapView) ClearTileHighlights() { vw.HighlightTiles([]*game.Tile{}) } func (vw *MapView) HighlightPermanents(permanents []game.Permanent) { vw.ForceRedraw() vw.permanentsHighlights = permanents } func (vw *MapView) HighlightPermanent(p game.Permanent) { vw.HighlightPermanents([]game.Permanent{p}) } func (vw *MapView) ClearPermanentsHighlights() { vw.HighlightPermanents([]game.Permanent{}) }