package tile import "fmt" import "gitlab.com/beoran/al5go/al" // import "gitlab.com/beoran/ebsgo/engine/geometry/point" import "gitlab.com/beoran/ebsgo/engine/physics" import "gitlab.com/beoran/ebsgo/engine/fifi" import "gitlab.com/beoran/ebsgo/monolog" /* Tile blending direction constants. */ const ( BLEND_NORTHWEST = 0 BLEND_NORTH = 1 BLEND_NORTHEAST = 2 BLEND_WEST = 3 BLEND_EAST = 4 BLEND_SOUTHWEST = 5 BLEND_SOUTH = 6 BLEND_SOUTHEAST = 7 BLEND_DIRECTION_MAX = 8 ) /* Tile blending shapes */ const ( BLEND_CORNER = 0 BLEND_SIDE = 1 BLEND_SHAPE_MAX = 2 ) /* Tile blending types. */ const ( BLEND_SHARP = 0 BLEND_GRADUAL = 1 BLEND_FUZZY = 2 BLEND_FUZZY_GRADUAL = 3 BLEND_TYPE_MAX = 3 // XXX data for type FUZZY_GRADUAL is missing... ) const BLEND_BITMAPS_MAX = 255 /* A cache of generated generic blending masks. */ var BlendMasks [BLEND_TYPE_MAX][BLEND_SHAPE_MAX]*al.Bitmap /* Lookup array with info on how the mask should be applied, i. e. flipped. */ var BlendFlags [BLEND_DIRECTION_MAX]int = [BLEND_DIRECTION_MAX] int{ 0, 0, al.FLIP_HORIZONTAL, 0, al.FLIP_HORIZONTAL, al.FLIP_VERTICAL, al.FLIP_VERTICAL, al.FLIP_HORIZONTAL | al.FLIP_VERTICAL, } /* Lookup array with info on which "side" mask should be used. */ var BlendSides [BLEND_DIRECTION_MAX]int = [BLEND_DIRECTION_MAX]int { BLEND_CORNER, BLEND_SIDE , BLEND_CORNER, BLEND_SIDE , BLEND_SIDE , BLEND_CORNER, BLEND_SIDE , BLEND_CORNER, } /* Lookup array with info on how the mask should be turned, i. e. rotated. */ var BlendAngles [BLEND_DIRECTION_MAX]float32 = [BLEND_DIRECTION_MAX]float32 { 0.0, 0.0, 0.0, 3 * al.PI / 2.0, al.PI / 2.0, 0.0, 0.0, 0.0, } /* ## Implementation of automatic overlapping ## In the simple case where there are only 2 different kinds of tiles, then for a tile somewhere in the map, that tile has 8 neighbours, and thus there are 8**2 or 256 different possible layouts of these neighbours around that single tile, and 256 different blends would be needed. Of course, this is a staggering amount for hand drawn graphics, and even for automatically generated mask images, this takes up a hefty chunk of memory, so some kind of simplification is needed. The first simplification is the use of the tile's blend value as a blend *priority* value. The tile with the lower blend priority will be blended with the tile with the higher blend priority blending over it. Tiles with equal blend priority will simply not blend. This reduces the possibilities because in cases where 3 or more tyle types must blend, without priorities, the number of possible blends would become even larger. Let's consider the possibilities takng symmetry in account. If only 2 tiles that will blend are adjacent, there are 8 possibilities. However there are 4 symmetric rotations of the 2 cases of the 2 tiles being either side-to-side or corner to corner. In case of side-to side, the blend should go rougmly like this: (scaled down to 8x8 ..........OOOOOOOO ................OO ..........OOOOOOOO ..............OOOO ..........OOOOOOOO ............OOOOOO ..........OOOOOOOO ............OOOOOO ..........OOOOOOOO => ............OOOOOO ..........OOOOOOOO ............OOOOOO ..........OOOOOOOO ...........OOOOOOO ..........OOOOOOOO ..........OOOOOOOO And corner to corner: OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO => OOOOOOOO OOOOOOOO OOOOOOOO OOOOOOOO .OOOOOOO OOOOOOOO ..OOOOOO .......... .......... .......... .......... .......... .......... .......... .......... .......... => .......... .......... .......... .......... .......... .......... .......... If the masks are judiciouly chosen, and all align correctly, then it will suffice to have just 2 mask images which get rotated as needed, one mask for the side to side, and one for corner to corner. Each of the masks follows a strict pattern, that is, the side by side has a \__/ - like shape were the depth of the overlap is at most 1/4th of the tile height. (Or is it 1/3, let's check it out). Then, for every overlapping tile, an overlap bitmap can be generated when loading the tile layer it is in, and that bitmap can then be used to draw the tile in stead of the original bitmap. ## Implementation of automatic shadows ## Every cell in the tile pane contains a slice of shadow polygons, that is set up before rendering. This should in the future be used in stead of the current live polygons generated at draw time but this will require an approach where a camera transform is applied whilst drawing in stead of the current situation where we use the camera info directly to calculate the draw positions. */ func PrepareBlend(blend * al.Bitmap, tile * Tile, blentile * Tile, direction int ) * al.Bitmap { side := BlendSides[direction] angle := BlendAngles[direction] flags := BlendFlags[direction] maskid:= tile.BlendMask mask := BlendMasks[maskid][side] if mask == nil { return nil } else { tile.DrawMaskedTo(blend, mask, angle, flags) return blend } } /** * A Cell is an individual part of a pane. It contains a tile, * a blend bitmap, two shadow polygons, and rendering flags. */ type Cell struct { * Tile Blend * al.Bitmap Flags int Rotate float32 Shadows [][8]float32 // Slice of shadow polygons. } /** * A Pane is a layer of tiles for use in a tile map or tiled * background. A pane consists of individual tiles from the same * tile set. Different panes may have different tile sets. */ type Pane struct { Tileset * Set Cells [][]Cell GridW int GridH int PixelW int PixelH int // This bitmap is the atlas used for blends. XXX: Not implemented yet. Blends * al.Bitmap Name string }; /* First group id of the tile set of the plane. */ func (pane * Pane) FirstGID() int { if pane.Tileset == nil { return 0 } else { return pane.Tileset.FirstGID } } /** Initializes a tile pane. * The pane will not clean up the tile set itself! */ func (pane * Pane) Init(set * Set, gridw, gridh int, name string) (* Pane) { pane.Tileset = set pane.GridW = gridw pane.GridH = gridh pane.PixelW = pane.GridW * set.TileW pane.PixelH = pane.GridW * set.TileH pane.Name = name pane.Cells = make([][]Cell, pane.GridH) for i := range (pane.Cells) { pane.Cells[i] = make([]Cell, pane.GridW) } return pane } func NewPane(set * Set, gridw, gridh int, name string) (pane * Pane) { pane = &Pane{} return pane.Init(set, gridw, gridh, name) } func (pane * Pane) Outside(gridx, gridy int) bool { return ((gridx < 0) || (gridy < 0)) || ((gridx >= pane.GridW) || (gridy >= pane.GridH)) } func (pane * Pane) Cell(gridx, gridy int) * Cell { if pane.Outside(gridx, gridy) { return nil } return & pane.Cells[gridy][gridx] } func (pane * Pane) Tile(gridx, gridy int) * Tile { if pane.Outside(gridx, gridy) { return nil } return pane.Cells[gridy][gridx].Tile } func (pane * Pane) Blend(gridx, gridy int) * al.Bitmap { if pane.Outside(gridx, gridy) { return nil } return pane.Cells[gridy][gridx].Blend } func (pane * Pane) SetTile(gridx, gridy int, tile * Tile) * Tile { if pane.Outside(gridx, gridy) { return nil } pane.Cells[gridy][gridx].Tile = tile return tile } func (pane * Pane) SetTileIndex(gridx, gridy, index int) * Tile { return pane.SetTile(gridx, gridy, pane.Tileset.Tile(index)) } func (pane * Pane) SetBlend(gridx, gridy int, blend * al.Bitmap) * al.Bitmap { if pane.Outside(gridx, gridy) { return nil } if nil != pane.Cells[gridy][gridx].Blend { pane.Cells[gridy][gridx].Blend.Destroy() } pane.Cells[gridy][gridx].Blend = blend return blend } func (pane * Pane) SetFlags(gridx, gridy int, flags int) int { if pane.Outside(gridx, gridy) { return -1 } pane.Cells[gridy][gridx].Flags = flags return flags } func (pane * Pane) SetTileRect(gridx, gridy, gridw, gridh int, tile * Tile) * Tile { for jj := gridy ; jj < (gridy + gridh) ; jj++ { for ii := gridx ; ii < (gridx + gridh) ; ii++ { pane.SetTile(ii, jj, tile) } } return tile } func (pane * Pane) Fill(tile * Tile) * Tile { return pane.SetTileRect(0, 0, pane.GridW, pane.GridH, nil) } func (cell * Cell) AddShadowPolygon(polygon [8]float32) { cell.Shadows = append(cell.Shadows, polygon) } func (cell * Cell) DeleteShadowPolygons() { cell.Shadows = nil } type DrawIterator func (cell * Cell, x, y float32, tx, ty int) /* Draws the tiles of the pane using an iterator function. * This must be done in one call because changing the source bitmap is less optimal. * You must hold bitmap drawing yourself if applicable. */ func (pane * Pane) DrawIterate(camera * physics.Camera, iter DrawIterator) { gridwide := pane.GridW gridhigh := pane.GridH tilewide := pane.Tileset.TileW tilehigh := pane.Tileset.TileH x := int(camera.X) y := int(camera.Y) tx_start := x / tilewide ty_start := y / tilehigh tx_delta := 1 + (int(camera.W) / tilewide) ty_delta := 1 + (int(camera.H) / tilehigh) tx_stop := tx_start + tx_delta ty_stop := ty_start + ty_delta tx_index := 0 ty_index := 0 // realwide := pane.PixelW // realhigh := pane.PixelH // drawflag := 0 if tx_start < 0 { tx_start = 0; } if ty_start < 0 { ty_start = 0; } if tx_stop > gridwide { tx_stop = gridwide; } if ty_stop > gridhigh { ty_stop = gridhigh; } y_draw := float32(-y + (ty_start * tilehigh)) for ty_index = ty_start; ty_index < ty_stop; ty_index++ { y_draw += float32(tilehigh) x_draw := float32(-x + (tx_start * tilewide)) row := pane.Cells[ty_index] for tx_index = tx_start ; tx_index < tx_stop; tx_index++ { x_draw += float32(tilewide) cell := &row[tx_index] iter(cell, x_draw, y_draw, tx_index, ty_index) } } } /* Draws the tiles of the pane. This must be done in one call because * changing the source bitmap is less optimal. */ func (pane * Pane) DrawTiles(camera * physics.Camera) { gridwide := pane.GridW gridhigh := pane.GridH tilewide := pane.Tileset.TileW tilehigh := pane.Tileset.TileH x := int(camera.X) y := int(camera.Y) tx_start := x / tilewide ty_start := y / tilehigh tx_delta := 1 + (int(camera.W) / tilewide) ty_delta := 1 + (int(camera.H) / tilehigh) tx_stop := tx_start + tx_delta ty_stop := ty_start + ty_delta tx_index := 0 ty_index := 0 // realwide := pane.PixelW // realhigh := pane.PixelH // drawflag := 0 al.HoldBitmapDrawing(true) if tx_start < 0 { tx_start = 0; } if ty_start < 0 { ty_start = 0; } if tx_stop > gridwide { tx_stop = gridwide; } if ty_stop > gridhigh { ty_stop = gridhigh; } y_draw := float32(-y + (ty_start * tilehigh)) for ty_index = ty_start; ty_index < ty_stop; ty_index++ { y_draw += float32(tilehigh) x_draw := float32(-x + (tx_start * tilewide)) row := pane.Cells[ty_index] for tx_index = tx_start ; tx_index < tx_stop; tx_index++ { x_draw += float32(tilewide) cell := row[tx_index] if (cell.Tile != nil) { cell.Tile.Draw(x_draw, y_draw, cell.Flags) } } } // Let go of hold al.HoldBitmapDrawing(false) } /* Draws the blends of the pane. This must be done in one call because * changing the source bitmap is less optimal. */ func (pane * Pane) DrawBlends(camera * physics.Camera) { gridwide := pane.GridW gridhigh := pane.GridH tilewide := pane.Tileset.TileW tilehigh := pane.Tileset.TileH x := int(camera.X) y := int(camera.Y) tx_start := x / tilewide ty_start := y / tilehigh tx_delta := 1 + (int(camera.W) / tilewide) ty_delta := 1 + (int(camera.H) / tilehigh) tx_stop := tx_start + tx_delta ty_stop := ty_start + ty_delta tx_index := 0 ty_index := 0 // realwide := pane.PixelW // realhigh := pane.PixelH // drawflag := 0 // al.HoldBitmapDrawing(true) if tx_start < 0 { tx_start = 0; } if ty_start < 0 { ty_start = 0; } if tx_stop > gridwide { tx_stop = gridwide; } if ty_stop > gridhigh { ty_stop = gridhigh; } y_draw := float32(-y + (ty_start * tilehigh)) for ty_index = ty_start; ty_index < ty_stop; ty_index++ { y_draw += float32(tilehigh) x_draw := float32(-x + (tx_start * tilewide)) row := pane.Cells[ty_index] for tx_index = tx_start ; tx_index < tx_stop; tx_index++ { x_draw += float32(tilewide) cell := row[tx_index] if (cell.Blend != nil) { cell.Blend.Draw(x_draw, y_draw, cell.Flags) } } } // Let go of hold // al.HoldBitmapDrawing(false) } func (pane * Pane) DrawOneShadowOn(cell * Cell, pane_below * Pane, tx, ty int, x, y float32) { tile := cell.Tile if (tile == nil) { return; } if (tile.Shadow == 0) { return; } shadow_color := al.MapRGBA(0, 0, 0, 128) edge_tile := pane.Tile(tx + 1, ty - 1) aid_tile := pane.Tile(tx + 1, ty) shadow_trapezium := false if (edge_tile != nil) && edge_tile.IsWall() { shadow_trapezium = true // here the shadow is at trapezium, not a parallelogram. } /* Tile sizes... */ tw := float32(pane.Tileset.TileW) th := float32(pane.Tileset.TileH) /* Only cast a shadow to the east if no solid tile next to self. * Shadow is a parallelogram to simplify overlaps. */ if (aid_tile == nil) || !aid_tile.IsWall() { low_tile := pane_below.Tile(tx + 1, ty) if (low_tile != nil) && (low_tile.ShadowMask != 1) { polygon := [8]float32 { x + tw , y , x + tw , y + th , x + tw * 1.5 , y + th * 0.5 , x + tw * 1.5 , y - th * 0.5 , } if (shadow_trapezium) { polygon[7] = y } al.DrawFilledPolygon(polygon[0:8], 0, shadow_color) } } aid_tile = pane.Tile(tx, ty-1) /* Only cast a shadow to the north if no solid tile above to self. * Shadow is a parallelogram to simplify overlaps. */ if (aid_tile == nil) || !aid_tile.IsWall() { low_tile := pane_below.Tile(tx, ty - 1) if (low_tile != nil) && (low_tile.ShadowMask != 1) { polygon := [8]float32 { x + tw , y , x + tw , y , x + tw * 1.5 , y - th * 0.5 , x + tw * 0.5 , y - th * 0.5 , } if (shadow_trapezium) { polygon[4] = x + tw } al.DrawFilledPolygon(polygon[0:8], 0, shadow_color) } } } /** Draws the shadows that tile pane pane casts onto the pane pane_below * with the camera delimiting the view. * On a classic tile map the bottom is to the south, so this function draws * "classic" shadows cast as if the sun were in the south-west, * with the shadows pointing north-east. */ func (pane * Pane) DrawShadowsOn(pane_below * Pane, camera * physics.Camera) { gridwide := pane.GridW gridhigh := pane.GridH tilewide := pane.Tileset.TileW tilehigh := pane.Tileset.TileH x := int(camera.X) y := int(camera.Y) tx_start := x / tilewide ty_start := y / tilehigh tx_delta := 1 + (int(camera.W) / tilewide) ty_delta := 1 + (int(camera.H) / tilehigh) tx_stop := tx_start + tx_delta ty_stop := ty_start + ty_delta tx_index := 0 ty_index := 0 // realwide := pane.PixelW // realhigh := pane.PixelH // drawflag := 0 // al.HoldBitmapDrawing(true) if tx_start < 0 { tx_start = 0; } if ty_start < 0 { ty_start = 0; } if tx_stop > gridwide { tx_stop = gridwide; } if ty_stop > gridhigh { ty_stop = gridhigh; } y_draw := float32(-y + (ty_start * tilehigh)) for ty_index = ty_start; ty_index < ty_stop; ty_index++ { y_draw += float32(tilehigh) x_draw := float32(-x + (tx_start * tilewide)) row := pane.Cells[ty_index] for tx_index = tx_start ; tx_index < tx_stop; tx_index++ { x_draw += float32(tilewide) cell := &row[tx_index] pane.DrawOneShadowOn(cell, pane_below, tx_index, ty_index, x_draw, y_draw) } } // Let go of hold // al.HoldBitmapDrawing(false) } /** Updates the tile pane. Curently does nothing, but this may change. */ func (pane * Pane) Update(dt float64) { if pane.Tileset != nil { pane.Tileset.Update(dt) } } /** Gets a tile from a the tilepane's tile set by it's tile id. **/ func (pane * Pane) TileFromSet(index int) * Tile { set := pane.Tileset if nil == set { return nil; } return set.Tile(index) } type BlendOffset struct { tx int ty int } var BlendOffsets [8]BlendOffset = [8]BlendOffset{ {-1, -1}, /* TILE_BLEND_NORTHWEST 0 */ { 0, -1}, /* TILE_BLEND_NORTH 1 */ { 1, -1}, /* TILE_BLEND_NORTHEAST 2 */ {-1, 0}, /* TILE_BLEND_WEST 3 */ { 1, 0}, /* TILE_BLEND_EAST 4 */ {-1, 1}, /* TILE_BLEND_SOUTHWEST 5 */ { 0, 1}, /* TILE_BLEND_SOUTH 6 */ { 1, 1}, /* TILE_BLEND_SOUTHEAST 7 */ } func (pane * Pane) InitBlendTile(index, tx, ty int, tile * Tile) bool { // SetBlend calls Destoy() on the bitmap automagically pane.SetBlend(tx, ty, nil) target := al.TargetBitmap() tile_prio := tile.Blend for i:= 0; i < len(BlendOffsets) ; i++ { offset := BlendOffsets[i] txn := tx + offset.tx tyn := ty + offset.ty aid_tile := pane.Tile(txn, tyn) if aid_tile == nil { continue; } aid_prio := aid_tile.Blend if aid_prio <= tile_prio { continue; } blend_bmp := al.CreateBitmap(pane.Tileset.TileW, pane.Tileset.TileH /* , al.CONVERT_BITMAP */) if blend_bmp == nil { return false; } al.SetTargetBitmap(blend_bmp) al.ClearToColor(al.MapRGBA(0,0,0,0)) PrepareBlend(blend_bmp, tile, aid_tile, i); } al.SetTargetBitmap(target) return true } const ( MASK_SIDE_W = 16 MASK_SIDE_H = 16 MASK_CORNER_W = 16 MASK_CORNER_H = 16 MASK_W = 8 MASK_H = 8 ) func InitBlendMasks() { // load the masks into the pane for blend_type := 0; blend_type < BLEND_TYPE_MAX ; blend_type++ { fin1 := fmt.Sprintf("/image/masks/corner_mask_%d.png", blend_type) fin2 := fmt.Sprintf("/image/masks/side_mask_%d.png", blend_type) bmp1 := fifi.LoadBitmap(fin1) bmp2 := fifi.LoadBitmap(fin2) BlendMasks[blend_type][BLEND_CORNER] = bmp1 BlendMasks[blend_type][BLEND_SIDE] = bmp2 if bmp1 == nil { monolog.Error("Could not load blend mask %d %d", blend_type, BLEND_CORNER) } if bmp2 == nil { monolog.Error("Could not load blend mask %d %d", blend_type, BLEND_SIDE) } } } func (pane * Pane) InitBlend(index int) bool { if pane == nil { return false } if pane.Blends == nil { return false } w := pane.GridW h := pane.GridH for ty := 0 ; ty < h; ty++ { for tx := 0; tx < w; tx++ { tile := pane.Tile(tx, ty) if tile == nil { continue; } if tile.Blend < 1 { continue; } monolog.Info("Will it blend? %d %d %d", tx, ty, tile.Blend) pane.InitBlendTile(index, tx, ty, tile) } } return true } func (pane * Pane) Close() { // No need to close tile set, the map will close them. w := pane.GridW h := pane.GridH for ty := 0 ; ty < h; ty++ { for tx := 0; tx < w; tx++ { pane.SetBlend(tx, ty, nil) } } if pane.Blends != nil { pane.Blends.Destroy() pane.Blends = nil } }