package ui import ( "fmt" "log" "math" "strings" "unicode" "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 (mv *MapView) Height() int { return len(mv.gameState.Map.Tiles) * TILE_HEIGHT } 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 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.Fatalf("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.ColorM.ScaleWithColor(p.Controller().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 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) { vw.tileHighlights = pos vw.ForceRedraw() } func (vw *MapView) AddHighlightPosition(pos game.Position) { vw.HighlightPositions(append(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) AddHighlightTile(t *game.Tile) { vw.AddHighlightPosition(t.Position) } func (vw *MapView) ClearTileHighlights() { vw.HighlightTiles([]*game.Tile{}) } func (vw *MapView) HighlightPermanents(permanents []game.Permanent) { vw.permanentsHighlights = permanents vw.ForceRedraw() } func (vw *MapView) HighlightPermanent(p game.Permanent) { vw.HighlightPermanents([]game.Permanent{p}) } func (vw *MapView) ClearPermanentsHighlights() { vw.HighlightPermanents([]game.Permanent{}) } func (*MapView) Render(Widget) *ebiten.Image { return nil }