package ui import ( "fmt" "image" "image/color" "log" "math" "strings" "unicode" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" "muhq.space/muhqs-game/go/assets" "muhq.space/muhqs-game/go/game" ) var ( // No idea why image.DrawTrianglesOptions needs this it is copied from // the ebiten vector example. whiteImage = ebiten.NewImage(3, 3) // whiteSubImage is an internal sub image of whiteImage. // Use whiteSubImage at DrawTriangles instead of whiteImage in order to avoid bleeding edges. whiteSubImage = whiteImage.SubImage(image.Rect(1, 1, 2, 2)).(*ebiten.Image) ) func init() { whiteImage.Fill(color.White) } const ( PERMANENT_WIDTH int = 40 PERMANENT_HEIGHT int = 40 TILE_WIDTH int = 50 TILE_HEIGHT int = 50 ) type MapView struct { hoverPermInfo gameState *game.State mapLayer *ebiten.Image permanentsLayer *ebiten.Image tileHighlights map[game.Position][]color.Color permanentsHighlights map[game.Permanent][]color.Color } func NewMapView(g *game.State) *MapView { vw := &MapView{ gameState: g, tileHighlights: make(map[game.Position][]color.Color), permanentsHighlights: make(map[game.Permanent][]color.Color), } vw.hoverPermInfo.init(vw) return vw } func (mv *MapView) Height() int { return len(mv.gameState.Map.Tiles) * TILE_HEIGHT } func (mv *MapView) Width() int { maxWidth := 0 for _, row := range mv.gameState.Map.Tiles { if n := len(row); n > maxWidth { maxWidth = n } } return maxWidth * TILE_WIDTH } 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 colors, found := vw.tileHighlights[pos]; found { for _, color := range colors { op.ColorScale.ScaleWithColor(color) } } 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 getPermanentSymbol(p game.Permanent, i int) *ebiten.Image { permanentSymbol := assets.GetSymbol(strings.ToLower(p.Card().Name)) if permanentSymbol == nil { symbol := fmt.Sprintf("%c%d", unicode.ToUpper(rune(p.Card().Type.String()[0])), i) permanentSymbol = assets.GetGenericSymbol(symbol) if permanentSymbol == nil { log.Panicf("Failed to generate generic symbol %s", symbol) } } return permanentSymbol } func (vw *MapView) drawPermanentsLayer(screen *ebiten.Image) { if vw.permanentsLayer == nil { vw.permanentsLayer = vw.newLayerImage() for i, p := range vw.gameState.Permanents { t := p.Tile() // Skip permanents with no containing tiles (e.g. piled ones) if t == nil { continue } permanentSymbol := getPermanentSymbol(p, i) 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.ColorScale.ScaleWithColor(p.Controller().Color) if colors, found := vw.permanentsHighlights[p]; found { for _, color := range colors { op.ColorScale.ScaleWithColor(color) } } vw.permanentsLayer.DrawImage(permanentSymbol, op) // Draw a pile hint if len(p.Pile()) > 0 { drawPileHint(permanentSymbol) } } } screen.DrawImage(vw.permanentsLayer, &ebiten.DrawImageOptions{}) } const ( PILE_HINT_STARTX = 20 PILE_HINT_LENGTH = 15 PILE_HINT_STARTY = 25 PILE_HINT_WIDTH = 2 PILE_HINT_SEP = 2 ) func drawPileHint(screen *ebiten.Image) { var path vector.Path var i float32 for i = 1.0; i < 4; i++ { y := PILE_HINT_STARTY + i*(PILE_HINT_WIDTH+PILE_HINT_SEP) path.MoveTo(PILE_HINT_STARTX, y) path.LineTo(PILE_HINT_STARTX+PILE_HINT_LENGTH, y) path.Close() } var vs []ebiten.Vertex var is []uint16 { op := &vector.StrokeOptions{Width: PILE_HINT_WIDTH} vs, is = path.AppendVerticesAndIndicesForStroke(nil, nil, op) } for i := range vs { vs[i].SrcX = 1 vs[i].SrcY = 1 vs[i].ColorR = 0 vs[i].ColorG = 0 vs[i].ColorB = 0 vs[i].ColorA = 1 } op := &ebiten.DrawTrianglesOptions{AntiAlias: true} screen.DrawTriangles(vs, is, whiteSubImage, op) } func (vw *MapView) Draw(screen *ebiten.Image) { vw.drawMapLayer(screen) vw.drawPermanentsLayer(screen) } func (vw *MapView) ForceRedraw() { vw.mapLayer = nil vw.permanentsLayer = nil } func isSymbolTransparentAt(p game.Permanent, relativeX, relativeY int) bool { permanentSymbol := getPermanentSymbol(p, 0) _, _, _, alpha := permanentSymbol.At(relativeX, relativeY).RGBA() return alpha == 0 } func (vw *MapView) FindObjectAt(screenX, screenY int) interface{} { x := screenX / TILE_WIDTH y := screenY / TILE_HEIGHT if x < 0 || y < 0 { return nil } relativeX := screenX % TILE_WIDTH relativeY := screenY % TILE_HEIGHT xMargin := (TILE_WIDTH - PERMANENT_WIDTH) / 2 yMargin := (TILE_HEIGHT - PERMANENT_HEIGHT) / 2 // detect if a permanent or the containing tile was selected if relativeX >= xMargin && relativeX <= TILE_WIDTH-xMargin && relativeY >= yMargin && relativeY <= TILE_HEIGHT-yMargin { for _, p := range vw.gameState.Permanents { t := p.Tile() if t == nil { continue } pos := t.Position if pos.X == x && pos.Y == y { if isSymbolTransparentAt(p, relativeX, relativeY) { break } return p } } } 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, col color.Color) { highlights := make(map[game.Position][]color.Color) for _, p := range pos { highlights[p] = []color.Color{col} } vw.tileHighlights = highlights vw.ForceRedraw() } func (vw *MapView) AddHighlightPosition(pos game.Position, col color.Color) { if colors, found := vw.tileHighlights[pos]; found { vw.tileHighlights[pos] = append(colors, col) } else { vw.tileHighlights[pos] = []color.Color{col} } vw.ForceRedraw() } func (vw *MapView) HighlightTiles(tiles []*game.Tile, color color.Color) { pos := make([]game.Position, 0, len(tiles)) for _, t := range tiles { pos = append(pos, t.Position) } vw.HighlightPositions(pos, color) } func (vw *MapView) HighlightTile(t *game.Tile, color color.Color) { vw.HighlightTiles([]*game.Tile{t}, color) } func (vw *MapView) AddHighlightTile(t *game.Tile, color color.Color) { vw.AddHighlightPosition(t.Position, color) } func (vw *MapView) ClearTileHighlights() { vw.HighlightTiles([]*game.Tile{}, nil) } func (vw *MapView) HighlightPermanents(permanents []game.Permanent, col color.Color) { highlights := make(map[game.Permanent][]color.Color) for _, p := range permanents { highlights[p] = []color.Color{col} } vw.permanentsHighlights = highlights vw.ForceRedraw() } func (vw *MapView) AddHighlightPermanent(p game.Permanent, col color.Color) { if colors, found := vw.permanentsHighlights[p]; found { vw.permanentsHighlights[p] = append(colors, col) } else { vw.permanentsHighlights[p] = []color.Color{col} } vw.ForceRedraw() } func (vw *MapView) HighlightPermanent(p game.Permanent, color color.Color) { vw.HighlightPermanents([]game.Permanent{p}, color) } func (vw *MapView) ClearPermanentsHighlights() { vw.HighlightPermanents([]game.Permanent{}, nil) } func (mv *MapView) Contains(x, y int) bool { w := mv.Width() h := mv.Height() return x >= 0 && y >= 0 && x <= w && y <= h } func (*MapView) Render(Widget) *ebiten.Image { return nil }