diff --git a/asset/map/house1.tmx b/asset/map/house1.tmx index 15114cd..8acd5f8 100644 --- a/asset/map/house1.tmx +++ b/asset/map/house1.tmx @@ -1,18 +1,16 @@ - + - + -161,161,161, -161,161,161, -161,529,161 +161,161, +161,527 - + -375,375,375, -372,372,372, -375,375,375 +183,183, +188,188 diff --git a/game/game.go b/game/game.go index 7dc48c5..834000e 100644 --- a/game/game.go +++ b/game/game.go @@ -80,6 +80,45 @@ func (g *game) Update() error { if world.World.ResetGame { world.Reset() + err := world.LoadTileset() + if err != nil { + return err + } + + // Fill below ground layer. + dirtTile := uint32(9*32 + (0)) + grassTile := uint32(11*32 + (0)) + treeTileA := uint32(5*32 + (25)) + treeTileB := uint32(5*32 + (27)) + var img uint32 + for x := range world.World.Level.Tiles[0] { + for y := range world.World.Level.Tiles[0][x] { + img = dirtTile + if rand.Intn(128) == 0 { + img = grassTile + world.World.Level.Tiles[0][x][y].Sprite = world.World.TileImages[img+world.World.TileImagesFirstGID] + for offsetX := -2 - rand.Intn(7); offsetX < 2+rand.Intn(7); offsetX++ { + for offsetY := -2 - rand.Intn(7); offsetY < 2+rand.Intn(7); offsetY++ { + if x+offsetX >= 0 && y+offsetY >= 0 && x+offsetX < 256 && y+offsetY < 256 { + world.World.Level.Tiles[0][x+offsetX][y+offsetY].Sprite = world.World.TileImages[img+world.World.TileImagesFirstGID] + if rand.Intn(2) == 0 { + if rand.Intn(3) == 0 { + world.World.Level.Tiles[1][x+offsetX][y+offsetY].EnvironmentSprite = world.World.TileImages[treeTileA+world.World.TileImagesFirstGID] + } else { + world.World.Level.Tiles[1][x+offsetX][y+offsetY].EnvironmentSprite = world.World.TileImages[treeTileB+world.World.TileImagesFirstGID] + } + } + } + } + } + } else { + if world.World.Level.Tiles[0][x][y].Sprite != nil { + continue + } + world.World.Level.Tiles[0][x][y].Sprite = world.World.TileImages[img+world.World.TileImagesFirstGID] + } + } + } world.BuildStructure(world.StructureHouse1, false, 0, 0) @@ -124,7 +163,7 @@ func (g *game) Update() error { // renderSprite renders a sprite on the screen. func (g *game) renderSprite(x float64, y float64, offsetx float64, offsety float64, angle float64, geoScale float64, colorScale float64, alpha float64, hFlip bool, vFlip bool, sprite *ebiten.Image, target *ebiten.Image) int { - if alpha < .01 || colorScale < .01 { + if alpha < .01 { return 0 } @@ -135,21 +174,20 @@ func (g *game) renderSprite(x float64, y float64, offsetx float64, offsety float // Skip drawing tiles that are out of the screen. drawX, drawY := world.IsoToScreen(xi, yi) - if drawX+padding < 0 || drawY+padding < 0 || drawX > float64(world.World.ScreenW) || drawY > float64(world.World.ScreenH) { - //log.Println("SKIP", drawX, drawY, world.World.ScreenW, world.World.ScreenH) + if drawX+padding < 0 || drawY+padding < 0 || drawX-padding > float64(world.World.ScreenW) || drawY-padding > float64(world.World.ScreenH) { return 0 } g.op.GeoM.Reset() - /*if hFlip { - s.op.GeoM.Scale(-1, 1) - s.op.GeoM.Translate(TileWidth, 0) + if hFlip { + g.op.GeoM.Scale(-1, 1) + g.op.GeoM.Translate(world.TileSize, 0) } if vFlip { - s.op.GeoM.Scale(1, -1) - s.op.GeoM.Translate(0, TileWidth) - }*/ + g.op.GeoM.Scale(1, -1) + g.op.GeoM.Translate(0, world.TileSize) + } // Move to current isometric position. g.op.GeoM.Translate(xi, yi+offsety) @@ -197,18 +235,21 @@ func (g *game) Draw(screen *ebiten.Image) { continue } var sprite *ebiten.Image - alpha := 1.0 colorScale := 1.0 if tile.HoverSprite != nil { sprite = tile.HoverSprite - alpha = 0.8 colorScale = 0.6 + if !world.World.HoverValid { + colorScale = 0.1 + } } else if tile.Sprite != nil { sprite = tile.Sprite + } else if tile.EnvironmentSprite != nil { + sprite = tile.EnvironmentSprite } else { continue } - g.renderSprite(float64(x), float64(y), 0, float64(i*-80), 0, 1, colorScale, alpha, false, false, sprite, screen) + g.renderSprite(float64(x), float64(y), 0, float64(i*-80), 0, 1, colorScale, 1, false, false, sprite, screen) } } } diff --git a/go.mod b/go.mod index 88c4b5c..66cd766 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( code.rocketnine.space/tslocum/gohan v0.0.0-20220106015515-0231e09ad78e github.com/hajimehoshi/ebiten/v2 v2.2.3 github.com/lafriks/go-tiled v0.6.0 + golang.org/x/image v0.0.0-20211028202545-6944b10bf410 ) require ( @@ -15,8 +16,7 @@ require ( github.com/jfreymuth/oggvorbis v1.0.3 // indirect github.com/jfreymuth/vorbis v1.0.2 // indirect golang.org/x/exp v0.0.0-20220104160115-025e73f80486 // indirect - golang.org/x/image v0.0.0-20211028202545-6944b10bf410 // indirect golang.org/x/mobile v0.0.0-20220104184238-4a8be17bd2e3 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect - golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect + golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe // indirect ) diff --git a/go.sum b/go.sum index 4c19849..b28a165 100644 --- a/go.sum +++ b/go.sum @@ -72,8 +72,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe h1:W8vbETX/n8S6EmY0Pu4Ix7VvpsJUESTwl0oCK8MJOgk= +golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/goreleaser.yml b/goreleaser.yml new file mode 100644 index 0000000..f1e1536 --- /dev/null +++ b/goreleaser.yml @@ -0,0 +1,31 @@ +project_name: citylimits + +builds: + - + id: citylimits +# ldflags: +# - -s -w -X code.rocketnine.space/tslocum/citylimits/main.Version={{.Version}} + goos: + - js + - linux + - windows + goarch: + - amd64 + - wasm +archives: + - + id: citylimits + builds: + - citylimits + replacements: + 386: i386 + format_overrides: + - goos: js + format: zip + - goos: windows + format: zip + files: + - ./*.md + - LICENSE +checksum: + name_template: 'checksums.txt' diff --git a/system/input_move.go b/system/input_move.go index c2c23d0..78e4ef2 100644 --- a/system/input_move.go +++ b/system/input_move.go @@ -21,12 +21,17 @@ type playerMoveSystem struct { rewindTicks int nextRewindTick int + + scrollDragX, scrollDragY int + scrollCamStartX, scrollCamStartY float64 } func NewPlayerMoveSystem(player gohan.Entity, m *MovementSystem) *playerMoveSystem { return &playerMoveSystem{ - player: player, - movement: m, + player: player, + movement: m, + scrollDragX: -1, + scrollDragY: -1, } } @@ -98,6 +103,13 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error { } } world.World.CamScaleTarget += scrollY * (world.World.CamScaleTarget / 7) + const minZoom = .15 + const maxZoom = 2 + if world.World.CamScaleTarget < minZoom { + world.World.CamScaleTarget = minZoom + } else if world.World.CamScaleTarget > maxZoom { + world.World.CamScaleTarget = maxZoom + } // Smooth zoom transition. div := 10.0 @@ -131,6 +143,7 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error { } } + const scrollEdgeSize = 5 x, y := ebiten.CursorPosition() if !world.World.GotCursorPosition { if x != 0 || y != 0 { @@ -139,15 +152,32 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error { return nil } } - if x == 0 { - world.World.CamX -= camSpeed - } else if x == world.World.ScreenW-1 { - world.World.CamX += camSpeed - } - if y == 0 { - world.World.CamY -= camSpeed - } else if y == world.World.ScreenH-1 { - world.World.CamY += camSpeed + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) { + if s.scrollDragX == -1 && s.scrollDragY == -1 { + ebiten.SetCursorMode(ebiten.CursorModeCaptured) + s.scrollDragX, s.scrollDragY = x, y + s.scrollCamStartX, s.scrollCamStartY = world.World.CamX, world.World.CamY + } else { + dx, dy := float64(x-s.scrollDragX)/world.World.CamScale, float64(y-s.scrollDragY)/world.World.CamScale + world.World.CamX, world.World.CamY = s.scrollCamStartX-dx, s.scrollCamStartY-dy + } + } else { + if s.scrollDragX != -1 && s.scrollDragY != -1 { + s.scrollDragX, s.scrollDragY = -1, -1 + ebiten.SetCursorMode(ebiten.CursorModeVisible) + } else if x >= 0 && y >= 0 && x < world.World.ScreenW && y < world.World.ScreenH { + // Pan via screen edge. + if x <= scrollEdgeSize { + world.World.CamX -= camSpeed + } else if x >= world.World.ScreenW-scrollEdgeSize-1 { + world.World.CamX += camSpeed + } + if y <= scrollEdgeSize { + world.World.CamY -= camSpeed + } else if y >= world.World.ScreenH-scrollEdgeSize-1 { + world.World.CamY += camSpeed + } + } } if world.World.HoverStructure != 0 { diff --git a/system/render.go b/system/render.go index d351b65..371fe5a 100644 --- a/system/render.go +++ b/system/render.go @@ -83,21 +83,20 @@ func (s *RenderSystem) renderSprite(x float64, y float64, offsetx float64, offse // Skip drawing tiles that are out of the screen. drawX, drawY := world.IsoToScreen(xi, yi) - if drawX+padding < 0 || drawY+padding < 0 || drawX > float64(world.World.ScreenW) || drawY > float64(world.World.ScreenH) { - //log.Println("SKIP", drawX, drawY, world.World.ScreenW, world.World.ScreenH) + if drawX+padding < 0 || drawY+padding < 0 || drawX-padding > float64(world.World.ScreenW) || drawY-padding > float64(world.World.ScreenH) { return 0 } s.op.GeoM.Reset() - /*if hFlip { + if hFlip { s.op.GeoM.Scale(-1, 1) s.op.GeoM.Translate(TileWidth, 0) } if vFlip { s.op.GeoM.Scale(1, -1) s.op.GeoM.Translate(0, TileWidth) - }*/ + } // Move to current isometric position. s.op.GeoM.Translate(xi, yi) diff --git a/world/level.go b/world/level.go index 0360a16..bd87971 100644 --- a/world/level.go +++ b/world/level.go @@ -5,8 +5,9 @@ import ( ) type Tile struct { - Sprite *ebiten.Image - HoverSprite *ebiten.Image + Sprite *ebiten.Image + EnvironmentSprite *ebiten.Image + HoverSprite *ebiten.Image } type GameLevel struct { @@ -19,6 +20,10 @@ func NewLevel(size int) *GameLevel { l := &GameLevel{ size: size, } + const startingLayers = 2 + for i := 0; i < startingLayers; i++ { + l.AddLayer() + } return l } @@ -26,12 +31,9 @@ func (l *GameLevel) AddLayer() { tileMap := make([][]*Tile, l.size) for x := 0; x < l.size; x++ { tileMap[x] = make([]*Tile, l.size) - // TODO - /*for y := range tileMap[x] { - tileMap[x][y] = &Tile{ - Sprite: asset.ImgWhiteSquare, - } - }*/ + for y := 0; y < l.size; y++ { + tileMap[x][y] = &Tile{} + } } l.Tiles = append(l.Tiles, tileMap) } diff --git a/world/world.go b/world/world.go index 2e77dcf..23ee018 100644 --- a/world/world.go +++ b/world/world.go @@ -23,9 +23,11 @@ const ( StructurePoliceStation ) +const startingZoom = 0.5 + var World = &GameWorld{ - CamScale: 1, - CamScaleTarget: 1, + CamScale: startingZoom, + CamScaleTarget: startingZoom, CamMoving: true, PlayerWidth: 8, PlayerHeight: 32, @@ -69,6 +71,7 @@ type GameWorld struct { HoverStructure int HoverX, HoverY int HoverLastX, HoverLastY int + HoverValid bool Map *tiled.Map ObjectGroups []*tiled.ObjectGroup @@ -83,7 +86,8 @@ type GameWorld struct { BrokenPieceA, BrokenPieceB gohan.Entity - TileImages map[uint32]*ebiten.Image + TileImages map[uint32]*ebiten.Image + TileImagesFirstGID uint32 ResetGame bool @@ -116,9 +120,7 @@ func Reset() { World.MessageVisible = false } -func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Structure, error) { - World.Level.ClearHoverSprites() - +func LoadMap(structureType int) (*tiled.Map, error) { loader := tiled.Loader{ FileSystem: asset.FS, } @@ -139,28 +141,35 @@ func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Str log.Fatalf("error parsing world: %+v", err) } - if placeX-m.Width < 0 || placeY-m.Height < 0 || placeX > 256 || placeY > 256 { - return nil, errors.New("invalid location: building does not fit") + return m, err +} + +func LoadTileset() error { + m, err := LoadMap(StructureHouse1) + if err != nil { + return err } // Load tileset. - tileset := m.Tilesets[0] - if len(World.tilesets) == 0 { - imgPath := filepath.Join("./image/tileset/", tileset.Image.Source) - f, err := asset.FS.Open(filepath.FromSlash(imgPath)) - if err != nil { - panic(err) - } - defer f.Close() - - img, _, err := image.Decode(f) - if err != nil { - panic(err) - } - World.tilesets = append(World.tilesets, ebiten.NewImageFromImage(img)) + if len(World.tilesets) != 0 { + return nil // Already loaded. } + tileset := m.Tilesets[0] + imgPath := filepath.Join("./image/tileset/", tileset.Image.Source) + f, err := asset.FS.Open(filepath.FromSlash(imgPath)) + if err != nil { + panic(err) + } + defer f.Close() + + img, _, err := image.Decode(f) + if err != nil { + panic(err) + } + World.tilesets = append(World.tilesets, ebiten.NewImageFromImage(img)) + // Load tiles. for i := uint32(0); i < uint32(tileset.TileCount); i++ { @@ -168,6 +177,22 @@ func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Str World.TileImages[i+tileset.FirstGID] = World.tilesets[0].SubImage(rect).(*ebiten.Image) } + World.TileImagesFirstGID = tileset.FirstGID + return nil +} + +func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Structure, error) { + World.Level.ClearHoverSprites() + + m, err := LoadMap(structureType) + if err != nil { + return nil, err + } + + if placeX-m.Width < 0 || placeY-m.Height < 0 || placeX > 256 || placeY > 256 { + return nil, errors.New("invalid location: building does not fit") + } + createTileEntity := func(t *tiled.LayerTile, x float64, y float64) gohan.Entity { mapTile := ECS.NewEntity() ECS.AddComponent(mapTile, &component.PositionComponent{ @@ -195,6 +220,32 @@ func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Str // TODO Add entity + if hover { + World.HoverValid = true + for y := 0; y < m.Height; y++ { + for x := 0; x < m.Width; x++ { + tx, ty := (x+placeX)-m.Width, (y+placeY)-m.Height + if World.Level.Tiles[1][tx][ty].Sprite != nil { + World.HoverValid = false + break // TODO + } + } + } + } + + for y := 0; y < m.Height; y++ { + for x := 0; x < m.Width; x++ { + tx, ty := (x+placeX)-m.Width, (y+placeY)-m.Height + if hover { + World.Level.Tiles[0][tx][ty].HoverSprite = World.TileImages[World.TileImagesFirstGID] + } else { + World.Level.Tiles[0][tx][ty].Sprite = World.TileImages[World.TileImagesFirstGID] + World.Level.Tiles[0][tx][ty].EnvironmentSprite = nil + World.Level.Tiles[1][tx][ty].EnvironmentSprite = nil + } + } + } + var t *tiled.LayerTile for i, layer := range m.Layers { for y := 0; y < m.Height; y++ { @@ -209,18 +260,17 @@ func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Str continue } - for i > len(World.Level.Tiles)-1 { + layerNum := i + 1 + + for layerNum > len(World.Level.Tiles)-1 { World.Level.AddLayer() } tx, ty := (x+placeX)-m.Width, (y+placeY)-m.Height - if World.Level.Tiles[i][tx][ty] == nil { - World.Level.Tiles[i][tx][ty] = &Tile{} - } if hover { - World.Level.Tiles[i][tx][ty].HoverSprite = World.TileImages[t.Tileset.FirstGID+t.ID] + World.Level.Tiles[layerNum][tx][ty].HoverSprite = World.TileImages[t.Tileset.FirstGID+t.ID] } else { - World.Level.Tiles[i][tx][ty].Sprite = World.TileImages[t.Tileset.FirstGID+t.ID] + World.Level.Tiles[layerNum][tx][ty].Sprite = World.TileImages[t.Tileset.FirstGID+t.ID] } // TODO handle flipping @@ -371,6 +421,9 @@ func IsoToScreen(x, y float64) (float64, float64) { } func ScreenToIso(x, y int) (float64, float64) { + // Offset to first above ground layer. + y += int(float64(32) * World.CamScale) + cx, cy := float64(World.ScreenW/2), float64(World.ScreenH/2) return ((float64(x) - cx) / World.CamScale) + World.CamX, ((float64(y) - cy) / World.CamScale) + World.CamY }