diff --git a/README.md b/README.md index 9caf8cb..fa2c30c 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,5 @@ Please share issues and suggestions [here](https://code.rocketnine.space/tslocum ## Dependencies - [ebiten](https://github.com/hajimehoshi/ebiten) - Game engine -- [gohan](https://code.rocketnine.space/tslocum/gohan) - Entity Component System framework - [go-tiled](https://github.com/lafriks/go-tiled) - Tiled map file (.TMX) parser - [go-astar](https://github.com/beefsack/go-astar) - Pathfinding library diff --git a/asset/asset.go b/asset/asset.go index 4d4555e..d988082 100644 --- a/asset/asset.go +++ b/asset/asset.go @@ -27,13 +27,14 @@ var ( ) var ( - SoundMusic *audio.Player - SoundSelect *audio.Player - SoundPop1 *audio.Player - SoundPop2 *audio.Player - SoundPop3 *audio.Player - SoundPop4 *audio.Player - SoundPop5 *audio.Player + SoundMusic *audio.Player + SoundSelect *audio.Player + SoundBulldoze *audio.Player + SoundPop1 *audio.Player + SoundPop2 *audio.Player + SoundPop3 *audio.Player + SoundPop4 *audio.Player + SoundPop5 *audio.Player ) func init() { @@ -48,6 +49,9 @@ func LoadSounds(ctx *audio.Context) { SoundSelect = LoadWAV(ctx, "sound/select/select.wav") SoundSelect.SetVolume(0.6) + SoundBulldoze = LoadOGG(ctx, "sound/bulldozer/bulldozer.ogg", true) + SoundBulldoze.SetVolume(0.4) + const popVolume = 0.15 SoundPop1 = LoadWAV(ctx, "sound/pop/pop1.wav") SoundPop2 = LoadWAV(ctx, "sound/pop/pop2.wav") diff --git a/asset/map/power_solar.tmx b/asset/map/power_solar.tmx new file mode 100644 index 0000000..5cef820 --- /dev/null +++ b/asset/map/power_solar.tmx @@ -0,0 +1,22 @@ + + + + + +519,519,519,519,519, +519,519,519,519,519, +519,519,519,519,519, +519,519,519,4,4, +519,519,519,4,527 + + + + +0,0,0,0,0, +0,0,0,0,0, +0,0,0,0,0, +0,0,0,11,11, +0,0,0,11,6 + + + diff --git a/asset/sound/bulldozer/LICENSE b/asset/sound/bulldozer/LICENSE new file mode 100644 index 0000000..10f052d --- /dev/null +++ b/asset/sound/bulldozer/LICENSE @@ -0,0 +1,6 @@ +This sound clip was made available for use by kijjaz under the +Creative Commons 0 License. + +Source: https://freesound.org/people/kijjaz/sounds/389584/ + +License: https://creativecommons.org/publicdomain/zero/1.0/ diff --git a/asset/sound/bulldozer/bulldozer.ogg b/asset/sound/bulldozer/bulldozer.ogg new file mode 100644 index 0000000..2c964c6 Binary files /dev/null and b/asset/sound/bulldozer/bulldozer.ogg differ diff --git a/asset/sound/select/LICENSE b/asset/sound/select/LICENSE index f540151..c502641 100644 --- a/asset/sound/select/LICENSE +++ b/asset/sound/select/LICENSE @@ -1,4 +1,4 @@ -These sound clips were made available for use by soiboi under the +This sound clip was made available for use by soiboi under the Creative Commons 0 License. Source: https://freesound.org/people/soiboi/sounds/556823/ diff --git a/game/game.go b/game/game.go index c862764..6910ee0 100644 --- a/game/game.go +++ b/game/game.go @@ -5,7 +5,6 @@ import ( "math/rand" "os" "sync" - "time" "code.rocketnine.space/tslocum/citylimits/entity" @@ -87,8 +86,6 @@ func (g *game) Update() error { if world.World.ResetGame { world.Reset() - rand.Seed(time.Now().UnixNano()) - err := world.LoadTileset() if err != nil { return err @@ -166,6 +163,11 @@ func (g *game) Update() error { SpriteOffsetX: -20, SpriteOffsetY: 2, Sprite: world.DrawMap(world.StructurePowerPlantCoal), + }, { + StructureType: world.StructurePowerPlantSolar, + SpriteOffsetX: -20, + SpriteOffsetY: 2, + Sprite: world.DrawMap(world.StructurePowerPlantSolar), }, { StructureType: world.StructurePoliceStation, SpriteOffsetX: -19, @@ -175,6 +177,24 @@ func (g *game) Update() error { nil, nil, nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, + nil, { StructureType: world.StructureToggleTransparentStructures, Sprite: transparentImg, @@ -302,7 +322,7 @@ func (g *game) Draw(screen *ebiten.Image) { drawn += g.renderSprite(float64(x), float64(y), 0, float64(i*-40), 0, 1, colorScale, alpha, false, false, sprite, screen) // Draw power-outs. - if world.World.Ticks%(144*2) < int(144.0*1.5) && world.World.PowerOuts[x][y] { + if world.World.HavePowerOut && world.World.Ticks%(144*2) < int(144.0*1.5) && world.World.PowerOuts[x][y] { drawn += g.renderSprite(float64(x), float64(y), 0, -52, 0, 1, 1, 1, false, false, asset.ImgPower, screen) } } diff --git a/go.mod b/go.mod index feb35c0..e4b15e1 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect github.com/jfreymuth/oggvorbis v1.0.3 // indirect github.com/jfreymuth/vorbis v1.0.2 // indirect - golang.org/x/exp v0.0.0-20220114162006-9d54fb35363c // indirect + golang.org/x/exp v0.0.0-20220121174013-7b334a16533f // indirect golang.org/x/mobile v0.0.0-20220112015953-858099ff7816 // indirect golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect golang.org/x/sys v0.0.0-20220114195835-da31bd327af9 // indirect diff --git a/go.sum b/go.sum index 5baf97c..e3afecc 100644 --- a/go.sum +++ b/go.sum @@ -40,8 +40,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4= -golang.org/x/exp v0.0.0-20220114162006-9d54fb35363c h1:aT7yTyPzmwDVPLHo6C/scvxQYjn6J7/NNTKaiCe+qxQ= -golang.org/x/exp v0.0.0-20220114162006-9d54fb35363c/go.mod h1:M50CtfS+xv2iy/epuEazynj250ScQ0/DOjcsin9UE8k= +golang.org/x/exp v0.0.0-20220121174013-7b334a16533f h1:u4dL7EmDaaJ+1e0HD9rawKa15yKPQZWXQ/epCOPAU+A= +golang.org/x/exp v0.0.0-20220121174013-7b334a16533f/go.mod h1:M50CtfS+xv2iy/epuEazynj250ScQ0/DOjcsin9UE8k= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= diff --git a/main.go b/main.go index 104a020..f402a60 100644 --- a/main.go +++ b/main.go @@ -28,10 +28,6 @@ func main() { parseFlags() - /*if world.World.Debug == 0 { - world.SetMessage("MOVE: ARROW KEYS\nFIRE: Z KEY\nMUTE: M KEY", 144*4) - }*/ - sigc := make(chan os.Signal, 1) signal.Notify(sigc, syscall.SIGINT, @@ -42,7 +38,6 @@ func main() { g.Exit() }() - // TODO world.StartGame() err = ebiten.RunGame(g) diff --git a/system/input_move.go b/system/input_move.go index cd17301..53374b6 100644 --- a/system/input_move.go +++ b/system/input_move.go @@ -1,7 +1,6 @@ package system import ( - "log" "math/rand" "os" "strings" @@ -50,6 +49,61 @@ func (_ *playerMoveSystem) Uses() []gohan.ComponentID { return nil } +func (s *playerMoveSystem) buildStructure(structureType int, tileX int, tileY int, playSound bool) (*world.Structure, error) { + structure, err := world.BuildStructure(world.World.HoverStructure, false, tileX, tileY) + if err == nil { + if world.IsPowerPlant(world.World.HoverStructure) { + plant := &world.PowerPlant{ + Type: world.World.HoverStructure, + X: tileX, + Y: tileY, + } + world.World.PowerPlants = append(world.World.PowerPlants, plant) + } + + if world.IsZone(structureType) { + zone := &world.Zone{ + Type: world.World.HoverStructure, + X: tileX, + Y: tileY, + } + world.World.Zones = append(world.World.Zones, zone) + } + + if world.World.HoverStructure != world.StructureBulldozer && playSound { + sounds := []*audio.Player{ + asset.SoundPop2, + asset.SoundPop3, + } + sound := sounds[rand.Intn(len(sounds))] + sound.Rewind() + sound.Play() + } + + cost := world.StructureCosts[structureType] + world.World.Funds -= cost + + world.World.HUDUpdated = true + } else { + dX := tileX - world.World.LastBuildX + if dX < 0 { + dX *= -1 + } + dY := tileY - world.World.LastBuildY + if dY < 0 { + dY *= -1 + } + if (dX > 1 || dY > 1) && err != world.ErrNothingToBulldoze { + errMessage := err.Error() + if len(errMessage) > 0 { + errMessage = strings.ToUpper(errMessage[0:1]) + errMessage[1:] + } + world.ShowMessage(errMessage, 3) + } + } + return structure, err +} + func (s *playerMoveSystem) Update(ctx *gohan.Context) error { if ebiten.IsKeyPressed(ebiten.KeyEscape) && !world.World.DisableEsc { os.Exit(0) @@ -230,70 +284,132 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error { } } } - } else if world.World.HoverStructure != 0 { + return nil + } + + if x >= world.World.ScreenW-helpW && y >= world.World.ScreenH-helpH { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { + const ( + helpPrev = iota + helpClose + helpNext + ) + + helpButton := world.HelpButtonAt(x-(world.World.ScreenW-helpW), y-(world.World.ScreenH-helpH)) + var updated bool + switch helpButton { + case helpPrev: + if world.World.HelpPage > 0 { + world.World.HelpPage-- + updated = true + } + case helpClose: + world.World.HelpPage = -1 + updated = true + case helpNext: + if world.World.HelpPage < len(world.HelpText)-1 { + world.World.HelpPage++ + updated = true + } + } + if updated { + world.World.HelpUpdated = true + world.World.HUDUpdated = true + + asset.SoundSelect.Rewind() + asset.SoundSelect.Play() + } + } + return nil + } + + if world.World.HoverStructure != 0 { + roadTiles := func(fromX, fromY, toX, toY int) [][2]int { + var tiles [][2]int + fx, fy := float64(fromX), float64(fromY) + tx, ty := float64(toX), float64(toY) + dx, dy := tx-fx, ty-fy + for dx < -1 || dx > 1 || dy < -1 || dy > 1 { + dx /= 2 + dy /= 2 + } + tiles = append(tiles, [2]int{fromX, fromY}) + for fx != tx || fy != ty { + fx, fy = fx+dx, fy+dy + tiles = append(tiles, [2]int{int(fx), int(fy)}) + } + return tiles + } + tileX, tileY := world.ScreenToCartesian(x, y) if tileX >= 0 && tileY >= 0 && tileX < 256 && tileY < 256 { - multiUseStructure := world.World.HoverStructure == world.StructureBulldozer || world.World.HoverStructure == world.StructureRoad - if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) || (multiUseStructure && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)) { - cost := world.StructureCosts[world.World.HoverStructure] - if world.World.Funds < cost { - // TODO - log.Println("NOT ENOUGH FUNDS") - } else { - world.World.Level.ClearHoverSprites() - // TODO draw hovers and build all roads in a line from drag start + multiUseStructure := world.World.HoverStructure == world.StructureBulldozer || world.World.HoverStructure == world.StructureRoad || world.IsZone(world.World.HoverStructure) + dragStarted := world.World.BuildDragX != -1 || world.World.BuildDragY != -1 + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) || (multiUseStructure && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)) || dragStarted { + if !dragStarted { + world.World.BuildDragX, world.World.BuildDragY = int(tileX), int(tileY) - s, err := world.BuildStructure(world.World.HoverStructure, false, int(tileX), int(tileY)) - if err == nil { - tileX, tileY = float64(s.X), float64(s.Y) - - isPowerPlant := world.World.HoverStructure == world.StructurePowerPlantCoal - if isPowerPlant { - plant := &world.PowerPlant{ - Type: world.World.HoverStructure, - X: int(tileX), - Y: int(tileY), - } - world.World.PowerPlants = append(world.World.PowerPlants, plant) - } + if world.World.HoverStructure == world.StructureBulldozer { + asset.SoundBulldoze.Play() + } + } - isZone := world.World.HoverStructure == world.StructureResidentialZone || world.World.HoverStructure == world.StructureCommercialZone || world.World.HoverStructure == world.StructureIndustrialZone - if isZone { - zone := &world.Zone{ - Type: world.World.HoverStructure, - X: int(tileX), - Y: int(tileY), + if world.World.HoverStructure == world.StructureRoad { + tiles := roadTiles(world.World.BuildDragX, world.World.BuildDragY, int(tileX), int(tileY)) + + if dragStarted && !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + // TODO build all tiles + world.World.Level.ClearHoverSprites() + var cost int + var builtRoad bool + for _, tile := range tiles { + _, err := s.buildStructure(world.World.HoverStructure, tile[0], tile[1], !builtRoad) + if err == nil { + cost += world.StructureCosts[world.World.HoverStructure] + builtRoad = true } - world.World.Zones = append(world.World.Zones, zone) } - - if world.World.HoverStructure != world.StructureBulldozer { - sounds := []*audio.Player{ - asset.SoundPop2, - asset.SoundPop3, - } - sound := sounds[rand.Intn(len(sounds))] - sound.Rewind() - sound.Play() + if cost > 0 { + world.ShowBuildCost(world.World.HoverStructure, cost) } - world.World.Funds -= cost - - if world.World.HoverStructure == world.StructureResidentialZone { - world.ShowMessage(world.World.Printer.Sprintf("Zoned area for residential use (-$%d)", cost), 3) - } else if world.World.HoverStructure == world.StructureCommercialZone { - world.ShowMessage(world.World.Printer.Sprintf("Zoned area for commercial use (-$%d)", cost), 3) - } else if world.World.HoverStructure == world.StructureIndustrialZone { - world.ShowMessage(world.World.Printer.Sprintf("Zoned area for industrial use (-$%d)", cost), 3) - } else { - world.ShowMessage(world.World.Printer.Sprintf("Built %s (-$%d)", strings.ToLower(world.StructureTooltips[world.World.HoverStructure]), cost), 3) + + world.World.BuildDragX, world.World.BuildDragY = -1, -1 + dragStarted = false + } else { + // TODO draw hover sprites + // TODO move below into shared func + world.World.Level.ClearHoverSprites() + for _, tile := range tiles { + world.BuildStructure(world.World.HoverStructure, true, tile[0], tile[1]) } + } + return nil + } else if dragStarted && !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + world.World.BuildDragX, world.World.BuildDragY = -1, -1 + asset.SoundBulldoze.Pause() + } - world.World.HUDUpdated = true + cost := world.StructureCosts[world.World.HoverStructure] + if world.World.Funds < cost { + world.ShowMessage("Insufficient funds", 3) + } else { + world.World.Level.ClearHoverSprites() + + // TODO draw hovers and build all roads in a line from drag start + structure, err := s.buildStructure(world.World.HoverStructure, int(tileX), int(tileY), true) + if err == nil { + tileX, tileY = float64(structure.X), float64(structure.Y) + world.ShowBuildCost(world.World.HoverStructure, cost) } + world.BuildStructure(world.World.HoverStructure, true, int(tileX), int(tileY)) } - } else if int(tileX) != world.World.HoverX || int(tileY) != world.World.HoverY { + } else { + if world.World.LastBuildX != -1 || world.World.LastBuildY != -1 { + world.World.LastBuildX, world.World.LastBuildY = -1, -1 + } + world.World.Level.ClearHoverSprites() world.BuildStructure(world.World.HoverStructure, true, int(tileX), int(tileY)) diff --git a/system/populate.go b/system/populate.go index 2b7eb01..a0e82d9 100644 --- a/system/populate.go +++ b/system/populate.go @@ -33,7 +33,10 @@ func (s *PopulateSystem) Update(_ *gohan.Context) error { return nil } - const popDuration = 144 * 4 + const popDuration = 144 * 7 + if world.World.Ticks%popDuration != 0 { + return nil + } // Thresholds. const ( @@ -81,58 +84,56 @@ func (s *PopulateSystem) Update(_ *gohan.Context) error { } const maxPopulation = 10 - if world.World.Ticks%popDuration == 0 { - popR, popC, popI := world.Population() - targetR, targetC, targetI := world.TargetPopulation() - for _, zone := range world.World.Zones { - var offset int + popR, popC, popI := world.Population() + targetR, targetC, targetI := world.TargetPopulation() + for _, zone := range world.World.Zones { + var offset int + if zone.Type == world.StructureResidentialZone { + if popR < targetR { + offset = 1 + } else if popR > targetR { + offset = -1 + } + } else if zone.Type == world.StructureCommercialZone { + if popC < targetC { + offset = 1 + } else if popC > targetC { + offset = -1 + } + } else { // Industrial + if popI < targetI { + offset = 1 + } else if popI > targetI { + offset = -1 + } + } + if offset == -1 && zone.Population > 0 { + zone.Population-- if zone.Type == world.StructureResidentialZone { - if popR < targetR { - offset = 1 - } else if popR > targetR { - offset = -1 - } + popR-- } else if zone.Type == world.StructureCommercialZone { - if popC < targetC { - offset = 1 - } else if popC > targetC { - offset = -1 - } + popC-- } else { // Industrial - if popI < targetI { - offset = 1 - } else if popI > targetI { - offset = -1 - } + popI-- } - if offset == -1 && zone.Population > 0 { - zone.Population-- - if zone.Type == world.StructureResidentialZone { - popR-- - } else if zone.Type == world.StructureCommercialZone { - popC-- - } else { // Industrial - popI-- - } - } else if offset == 1 && zone.Population < maxPopulation && zone.Powered { - zone.Population++ - if zone.Type == world.StructureResidentialZone { - popR++ - } else if zone.Type == world.StructureCommercialZone { - popC++ - } else { // Industrial - popI++ - } + } else if offset == 1 && zone.Population < maxPopulation && zone.Powered { + zone.Population++ + if zone.Type == world.StructureResidentialZone { + popR++ + } else if zone.Type == world.StructureCommercialZone { + popC++ + } else { // Industrial + popI++ } - newType := buildStructureType(zone.Type, zone.Population) - // TODO only bulldoze when changed - for offsetX := 0; offsetX < 2; offsetX++ { - for offsetY := 0; offsetY < 2; offsetY++ { - world.BuildStructure(world.StructureBulldozer, false, zone.X-offsetX, zone.Y-offsetY) - } + } + newType := buildStructureType(zone.Type, zone.Population) + // TODO only bulldoze when changed + for offsetX := 0; offsetX < 2; offsetX++ { + for offsetY := 0; offsetY < 2; offsetY++ { + world.BuildStructure(world.StructureBulldozer, false, zone.X-offsetX, zone.Y-offsetY) } - world.BuildStructure(newType, false, zone.X, zone.Y) } + world.BuildStructure(newType, false, zone.X, zone.Y) } // TODO populate and de-populate zones by target population diff --git a/system/powerscan.go b/system/powerscan.go index 377677d..1f8f335 100644 --- a/system/powerscan.go +++ b/system/powerscan.go @@ -1,8 +1,6 @@ package system import ( - "log" - "github.com/beefsack/go-astar" "code.rocketnine.space/tslocum/citylimits/component" @@ -58,7 +56,6 @@ func (s *PowerScanSystem) Update(_ *gohan.Context) error { plantSize = 5 ) powerSourceTiles := make([][]*world.PowerMapTile, len(world.World.PowerPlants)) - log.Println("POWER TILES") for i, plant := range world.World.PowerPlants { for y := 0; y < plantSize; y++ { t := world.World.Power.GetTile(plant.X+1, plant.Y-y) @@ -132,23 +129,15 @@ func (s *PowerScanSystem) Update(_ *gohan.Context) error { } for _, powerSource := range powerSourceTiles[j] { + from := world.World.Power.GetTile(powerSource.X, powerSource.Y) + for _, to := range powerDestinationTiles { if to == nil { continue } - from := world.World.Power.GetTile(powerSource.X, powerSource.Y) - - log.Println("SEARCH", from.X, from.Y, "TO", to.X, to.Y) - - /*for _, n := range powerSource.PathNeighbors() { - t := n.(*world.PowerMapTile) - log.Println("NEIGHBOR", t.X, t.Y, t.CarriesPower) - }*/ - - p, dist, found := astar.Path(from, to) + _, _, found := astar.Path(from, to) if found { - log.Printf("Resulting path\n%+v %f", p, dist) powerRemaining[j] -= powerRequired powered = true break FINDPOWERPATH @@ -157,11 +146,10 @@ func (s *PowerScanSystem) Update(_ *gohan.Context) error { } } zone.Powered = powered - log.Println("ZONE", zone, zone.Powered) - if !powered { havePowerOut = true world.World.PowerOuts[zone.X][zone.Y] = true + world.World.HavePowerOut = true } totalPowerRequired += powerRequired diff --git a/system/renderhud.go b/system/renderhud.go index d2972d6..00c4b90 100644 --- a/system/renderhud.go +++ b/system/renderhud.go @@ -3,6 +3,7 @@ package system import ( "image" "image/color" + "math" "strings" "github.com/hajimehoshi/ebiten/v2/ebitenutil" @@ -13,11 +14,17 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) +const ( + helpW = 480 + helpH = 185 +) + type RenderHudSystem struct { op *ebiten.DrawImageOptions hudImg *ebiten.Image tmpImg *ebiten.Image tmpImg2 *ebiten.Image + helpImg *ebiten.Image sidebarColor color.RGBA } @@ -27,6 +34,7 @@ func NewRenderHudSystem() *RenderHudSystem { hudImg: ebiten.NewImage(1, 1), tmpImg: ebiten.NewImage(1, 1), tmpImg2: ebiten.NewImage(1, 1), + helpImg: ebiten.NewImage(helpW, helpH), } sidebarShade := uint8(111) @@ -58,12 +66,16 @@ func (s *RenderHudSystem) Draw(_ *gohan.Context, screen *ebiten.Image) error { s.drawSidebar() s.drawMessages() s.drawTooltip() + s.drawHelp() world.World.HUDUpdated = false } screen.DrawImage(s.hudImg, nil) return nil } +const columns = 3 +const buttonWidth = world.SidebarWidth / columns + func (s *RenderHudSystem) drawSidebar() { bounds := s.hudImg.Bounds() if bounds.Dx() != world.World.ScreenW || bounds.Dy() != world.World.ScreenH { @@ -75,10 +87,6 @@ func (s *RenderHudSystem) drawSidebar() { s.tmpImg.Clear() s.tmpImg2.Clear() } - w := world.SidebarWidth - if bounds.Dx() < w { - w = bounds.Dx() - } // Fill background. s.hudImg.SubImage(image.Rect(0, 0, world.SidebarWidth, world.World.ScreenH)).(*ebiten.Image).Fill(s.sidebarColor) @@ -86,9 +94,6 @@ func (s *RenderHudSystem) drawSidebar() { // Draw buttons. const paddingSize = 1 - const columns = 3 - - const buttonWidth = world.SidebarWidth / columns const buttonHeight = buttonWidth world.World.HUDButtonRects = make([]image.Rectangle, len(world.HUDButtons)) var lastButtonY int @@ -115,58 +120,25 @@ func (s *RenderHudSystem) drawSidebar() { } world.World.HUDButtonRects[i] = r - lastButtonY = y - } - - s.drawDate(lastButtonY + buttonHeight + 10) - s.drawFunds(lastButtonY + buttonHeight + 55) - - // Draw RCI indicator. - rciPadding := buttonWidth - 14 - const rciSize = 100 - rciX := buttonWidth - rciY := lastButtonY + buttonHeight + 55 + rciPadding - - const rciButtonHeight = 20 - - // Draw RCI bars. - colorR := color.RGBA{0, 255, 0, 255} - colorC := color.RGBA{0, 0, 255, 255} - colorI := color.RGBA{231, 231, 72, 255} - demandR, demandC, demandI := world.Demand() - drawDemandBar := func(demand float64, clr color.RGBA, i int) { - barOffsetSize := 12 - barOffset := -barOffsetSize + (i * barOffsetSize) - barWidth := 7 - barX := rciX + buttonWidth/2 - barWidth/2 + barOffset - barY := rciY + (rciSize / 2) - if demand < 0 { - barY += rciButtonHeight / 2 - } else { - barY -= rciButtonHeight / 2 + if button != nil && button.StructureType != world.StructureToggleTransparentStructures { + lastButtonY = y } - barHeight := int((float64(rciSize) / 2) * demand) - s.tmpImg.SubImage(image.Rect(barX, barY, barX+barWidth, barY-barHeight)).(*ebiten.Image).Fill(clr) } - drawDemandBar(demandR, colorR, 0) - drawDemandBar(demandC, colorC, 1) - drawDemandBar(demandI, colorI, 2) - // Draw RCI button. - const rciButtonPadding = 12 - const rciButtonLabelPaddingX = 6 - const rciButtonLabelPaddingY = 1 - rciButtonY := rciY + (rciSize / 2) - (rciButtonHeight / 2) - rciButtonRect := image.Rect(rciX+rciButtonPadding, rciButtonY, rciX+buttonWidth-rciButtonPadding, rciButtonY+rciButtonHeight) + dateY := lastButtonY + buttonHeight*2 - buttonHeight/2 - 16 + s.drawDate(dateY) + s.drawFunds(dateY + 50) - s.drawButtonBackground(s.tmpImg, rciButtonRect, false) // TODO - - // Draw RCI label. - ebitenutil.DebugPrintAt(s.tmpImg, "R C I", rciX+rciButtonPadding+rciButtonLabelPaddingX, rciButtonY+rciButtonLabelPaddingY) + indicatorY := dateY + 179 + // Draw RCI indicator. + s.drawDemand(buttonWidth/2, indicatorY) - s.drawButtonBorder(s.tmpImg, rciButtonRect, false) // TODO + // Draw PWR indicator. + s.drawPower(buttonWidth/2+buttonWidth, indicatorY) s.hudImg.DrawImage(s.tmpImg, nil) + + s.hudImg.SubImage(image.Rect(world.SidebarWidth-1, 0, world.SidebarWidth, world.World.ScreenH)).(*ebiten.Image).Fill(color.Black) } func (s *RenderHudSystem) drawButtonBackground(img *ebiten.Image, r image.Rectangle, selected bool) { @@ -243,6 +215,116 @@ func maxLen(v []string) int { return max } +func (s *RenderHudSystem) drawDemand(x, y int) { + const rciSize = 100 + rciX := x + rciY := y + + const rciButtonHeight = 20 + + colorR := color.RGBA{0, 255, 0, 255} + colorC := color.RGBA{0, 0, 255, 255} + colorI := color.RGBA{231, 231, 72, 255} + demandR, demandC, demandI := world.Demand() + drawDemandBar := func(demand float64, clr color.RGBA, i int) { + barOffsetSize := 12 + barOffset := -barOffsetSize + (i * barOffsetSize) + barWidth := 7 + barX := rciX + buttonWidth/2 - barWidth/2 + barOffset + barY := rciY + (rciSize / 2) + if demand < 0 { + barY += rciButtonHeight / 2 + } else { + barY -= rciButtonHeight / 2 + } + barHeight := int((float64(rciSize) / 2) * demand) + s.tmpImg.SubImage(image.Rect(barX, barY, barX+barWidth, barY-barHeight)).(*ebiten.Image).Fill(clr) + } + drawDemandBar(demandR, colorR, 0) + drawDemandBar(demandC, colorC, 1) + drawDemandBar(demandI, colorI, 2) + + // Draw button. + const rciButtonPadding = 12 + const rciButtonLabelPaddingX = 6 + const rciButtonLabelPaddingY = 1 + rciButtonY := rciY + (rciSize / 2) - (rciButtonHeight / 2) + rciButtonRect := image.Rect(rciX+rciButtonPadding, rciButtonY, rciX+buttonWidth-rciButtonPadding, rciButtonY+rciButtonHeight) + + s.drawButtonBackground(s.tmpImg, rciButtonRect, false) // TODO + + // Draw label. + ebitenutil.DebugPrintAt(s.tmpImg, "R C I", rciX+rciButtonPadding+rciButtonLabelPaddingX, rciButtonY+rciButtonLabelPaddingY) + + s.drawButtonBorder(s.tmpImg, rciButtonRect, false) // TODO +} + +func (s *RenderHudSystem) drawPower(x, y int) { + const rciSize = 100 + rciX := x + rciY := y + + const rciButtonHeight = 20 + + colorPowerNormal := color.RGBA{0, 255, 0, 255} + colorPowerOut := color.RGBA{255, 0, 0, 255} + colorPowerCapacity := color.RGBA{16, 16, 16, 255} + drawPowerBar := func(demand float64, clr color.RGBA, i int) { + barOffsetSize := 7 + barOffset := -barOffsetSize + (i * barOffsetSize) + barWidth := 7 + barX := rciX + buttonWidth/2 - barWidth/2 + barOffset + 4 + barY := rciY + (rciSize / 2) + if demand < 0 { + barY += rciButtonHeight / 2 + } else { + barY -= rciButtonHeight / 2 + } + barHeight := int((float64(rciSize) / 2) * demand) + s.tmpImg.SubImage(image.Rect(barX, barY, barX+barWidth, barY-barHeight)).(*ebiten.Image).Fill(clr) + } + + powerColor := colorPowerNormal + if world.World.HavePowerOut || world.World.PowerNeeded > world.World.PowerAvailable { + powerColor = colorPowerOut + } + + max := world.World.PowerNeeded + if world.World.PowerAvailable > max { + max = world.World.PowerAvailable + } + + pctUsage, pctCapacity := float64(world.World.PowerNeeded)/float64(max), float64(world.World.PowerAvailable)/float64(max) + clamp := func(v float64) float64 { + if math.IsNaN(v) { + return 0 + } + if v < -1 { + v = -1 + } else if v > 1 { + v = 1 + } + return v + } + + drawPowerBar(clamp(pctUsage), powerColor, 0) + drawPowerBar(clamp(pctCapacity), colorPowerCapacity, 1) + + // Draw button. + const rciButtonPadding = 12 + const rciButtonLabelPaddingX = 6 + const rciButtonLabelPaddingY = 1 + rciButtonY := rciY + (rciSize / 2) - (rciButtonHeight / 2) + rciButtonRect := image.Rect(rciX+rciButtonPadding, rciButtonY, rciX+buttonWidth-rciButtonPadding, rciButtonY+rciButtonHeight) + + s.drawButtonBackground(s.tmpImg, rciButtonRect, false) // TODO + + // Draw label. + ebitenutil.DebugPrintAt(s.tmpImg, "POWER", rciX+rciButtonPadding+rciButtonLabelPaddingX, rciButtonY+rciButtonLabelPaddingY) + + s.drawButtonBorder(s.tmpImg, rciButtonRect, false) // TODO +} + func (s *RenderHudSystem) drawMessages() { lines := len(world.World.Messages) if lines == 0 { @@ -264,10 +346,10 @@ func (s *RenderHudSystem) drawMessages() { max := len(label) lines = 1 - const padding = 12 + const padding = 10 scale := 2.0 - w, h := (max*6+10)*int(scale), 16*(int(scale))*lines+6 + w, h := (max*6+10)*int(scale), 16*(int(scale))*lines+10 x, y := world.World.ScreenW-w, 0 r := image.Rect(x, y, x+w, y+h) s.hudImg.SubImage(r).(*ebiten.Image).Fill(color.RGBA{0, 0, 0, 120}) @@ -276,7 +358,7 @@ func (s *RenderHudSystem) drawMessages() { ebitenutil.DebugPrint(s.tmpImg, label) op := &ebiten.DrawImageOptions{} op.GeoM.Scale(scale, scale) - op.GeoM.Translate(float64(x)+padding, 0) + op.GeoM.Translate(float64(x)+padding, 3) s.hudImg.DrawImage(s.tmpImg, op) } @@ -320,3 +402,59 @@ func (s *RenderHudSystem) drawFunds(y int) { op.GeoM.Translate(float64(x), float64(y)) s.hudImg.DrawImage(s.tmpImg2, op) } + +func (s *RenderHudSystem) drawHelp() { + if world.World.HelpPage < 0 { + return + } + + if world.World.HelpUpdated { + s.helpImg.Fill(s.sidebarColor) + + label := strings.TrimSpace(world.HelpText[world.World.HelpPage]) + + s.tmpImg.Clear() + ebitenutil.DebugPrint(s.tmpImg, label) + + op := &ebiten.DrawImageOptions{} + op.GeoM.Scale(2, 2) + op.GeoM.Translate(5, 0) + s.helpImg.DrawImage(s.tmpImg, op) + + s.helpImg.SubImage(image.Rect(0, 0, helpW, 1)).(*ebiten.Image).Fill(color.Black) + s.helpImg.SubImage(image.Rect(0, 0, 1, helpH)).(*ebiten.Image).Fill(color.Black) + + // Draw prev/next buttons. + buttonSize := 32 + buttonPadding := 4 + prevRect := image.Rect(buttonPadding+2, helpH-buttonSize-buttonPadding+1, buttonSize+buttonPadding+2, helpH-buttonPadding+1) + closeRect := image.Rect(helpW/2-buttonSize/2, helpH-buttonSize-buttonPadding+1, helpW/2+buttonSize/2, helpH-buttonPadding+1) + nextRect := image.Rect(helpW-buttonPadding, helpH-buttonSize-buttonPadding+1, helpW-buttonSize-buttonPadding, helpH-buttonPadding+1) + + drawButton := func(r image.Rectangle, l string) { + s.drawButtonBackground(s.helpImg, r, false) + ebitenutil.DebugPrintAt(s.helpImg, l, r.Min.X+buttonSize/2-4, r.Min.Y+buttonSize/2-10) + s.drawButtonBorder(s.helpImg, r, false) + } + + if world.World.HelpPage > 0 { + drawButton(prevRect, "<") + } + drawButton(closeRect, "X") + if world.World.HelpPage < len(world.HelpText)-1 { + drawButton(nextRect, ">") + } + + world.World.HelpButtonRects = []image.Rectangle{ + prevRect, + closeRect, + nextRect, + } + + world.World.HelpUpdated = false + } + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(world.World.ScreenW)-helpW, float64(world.World.ScreenH)-helpH) + s.hudImg.DrawImage(s.helpImg, op) +} diff --git a/world/help.go b/world/help.go new file mode 100644 index 0000000..761bbf4 --- /dev/null +++ b/world/help.go @@ -0,0 +1,20 @@ +package world + +// HelpText lines must be 39 characters or less. +var HelpText = []string{` +Welcome to City Limits! (1/3) +Blah blah blah...1231231231231231231234 +Testing, testing... Testing, testing... +How does this much text per page seem? +`, ` +Yadda yadda yadda (2/3) +Do you like the game? +I hoped you would. +I spent a lot of time making it. +`, ` +Oh no... More text?!? (3/3) +If I wanted to read, I would have +grabbed a book, not a game! I'm outta +here! *Door slam*... *Engine starting* +`, +} diff --git a/world/powermap.go b/world/powermap.go index 64f4d3d..f1ccd01 100644 --- a/world/powermap.go +++ b/world/powermap.go @@ -4,12 +4,6 @@ import ( "github.com/beefsack/go-astar" ) -const ( - powerEmptyTile = iota - powerSourceTile - powerDestinationTile -) - type PowerMapTile struct { X int Y int @@ -94,6 +88,7 @@ func ResetPowerOuts() { World.PowerOuts[x][y] = false } } + World.HavePowerOut = false } func (m PowerMap) GetTile(x, y int) *PowerMapTile { @@ -105,6 +100,9 @@ func (m PowerMap) GetTile(x, y int) *PowerMapTile { func (m PowerMap) SetTile(x, y int, carriesPower bool) { t := m[x][y] + if t.CarriesPower == carriesPower { + return + } t.CarriesPower = carriesPower World.PowerUpdated = true diff --git a/world/structure.go b/world/structure.go index 3ca33f0..26150c5 100644 --- a/world/structure.go +++ b/world/structure.go @@ -20,6 +20,7 @@ const ( StructureIndustrialHigh StructurePoliceStation StructurePowerPlantCoal + StructurePowerPlantSolar ) var StructureFilePaths = map[int]string{ @@ -39,6 +40,7 @@ var StructureFilePaths = map[int]string{ StructureIndustrialHigh: "map/industrial_high1.tmx", StructurePoliceStation: "map/policestation.tmx", StructurePowerPlantCoal: "map/power_coal.tmx", + StructurePowerPlantSolar: "map/power_solar.tmx", } type Structure struct { diff --git a/world/world.go b/world/world.go index 4c60fd9..2770c90 100644 --- a/world/world.go +++ b/world/world.go @@ -9,7 +9,9 @@ import ( "math/rand" "path/filepath" "strconv" + "strings" "sync" + "time" "golang.org/x/text/language" "golang.org/x/text/message" @@ -27,7 +29,7 @@ const startingYear = 1950 const maxPopulation = 100000 const ( - MonthTicks = 144 * 3 + MonthTicks = 144 * 7 YearTicks = MonthTicks * 12 ) @@ -39,7 +41,7 @@ const startingFunds = 10000 const startingZoom = 1.0 -const SidebarWidth = 198 +const SidebarWidth = 199 type HUDButton struct { Sprite *ebiten.Image @@ -50,7 +52,7 @@ type HUDButton struct { var HUDButtons []*HUDButton -var CameraMinZoom = 0.4 +var CameraMinZoom = 0.1 var CameraMaxZoom = 1.0 var World = &GameWorld{ @@ -65,6 +67,10 @@ var World = &GameWorld{ Printer: message.NewPrinter(language.English), Power: newPowerMap(), PowerOuts: newPowerOuts(), + BuildDragX: -1, + BuildDragY: -1, + LastBuildX: -1, + LastBuildY: -1, } type Zone struct { @@ -142,10 +148,15 @@ type GameWorld struct { HUDUpdated bool HUDButtonRects []image.Rectangle + HelpUpdated bool + HelpPage int + HelpButtonRects []image.Rectangle + PowerPlants []*PowerPlant Zones []*Zone - PowerOuts [][]bool + HavePowerOut bool + PowerOuts [][]bool Ticks int @@ -165,9 +176,17 @@ type GameWorld struct { PowerAvailable int PowerNeeded int + BuildDragX int + BuildDragY int + + LastBuildX int + LastBuildY int + resetTipShown bool } +var ErrNothingToBulldoze = errors.New("nothing to bulldoze") + func TileToGameCoords(x, y int) (float64, float64) { //return float64(x) * 32, float64(g.currentMap.Height*32) - float64(y)*32 - 32 return float64(x) * TileSize, float64(y) * TileSize @@ -179,6 +198,8 @@ func Reset() { } World.Player = 0 + rand.Seed(time.Now().UnixNano()) + World.Funds = startingFunds World.ObjectGroups = nil @@ -188,6 +209,10 @@ func Reset() { World.TriggerEntities = nil World.TriggerRects = nil World.TriggerNames = nil + + World.CamX = float64((32 * TileSize) - rand.Intn(64*TileSize)) + World.CamY = float64((32 * TileSize) + rand.Intn(32*TileSize)) + } func LoadMap(structureType int) (*tiled.Map, error) { @@ -283,6 +308,20 @@ func LoadTileset() error { return nil } +func ShowBuildCost(structureType int, cost int) { + if structureType == StructureBulldozer { + ShowMessage(World.Printer.Sprintf("Bulldozed area (-$%d)", cost), 3) + } else if structureType == StructureResidentialZone { + ShowMessage(World.Printer.Sprintf("Zoned area for residential use (-$%d)", cost), 3) + } else if structureType == StructureCommercialZone { + ShowMessage(World.Printer.Sprintf("Zoned area for commercial use (-$%d)", cost), 3) + } else if structureType == StructureIndustrialZone { + ShowMessage(World.Printer.Sprintf("Zoned area for industrial use (-$%d)", cost), 3) + } else { + ShowMessage(World.Printer.Sprintf("Built %s (-$%d)", strings.ToLower(StructureTooltips[World.HoverStructure]), cost), 3) + } +} + func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Structure, error) { // For previewing buildings /*v := rand.Intn(3) @@ -361,7 +400,7 @@ func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Str } } if !bulldozed { - return nil, errors.New("nothing to bulldoze") + return nil, ErrNothingToBulldoze } World.Power.SetTile(placeX, placeY, false) return structure, nil @@ -475,7 +514,11 @@ VALIDBUILD: if structureType == StructureRoad { World.Power.SetTile(tx, ty, true) } - World.PowerUpdated = true + + isZone := structureType == StructureResidentialZone || structureType == StructureCommercialZone || structureType == StructureIndustrialZone + if isZone || structureType == StructurePowerPlantCoal || structureType == StructureBulldozer { + World.PowerUpdated = true + } } // TODO handle flipping @@ -535,6 +578,9 @@ func StartGame() { if !World.MuteMusic { asset.SoundMusic.Play() } + + // Show initial help page. + SetHelpPage(0) } // CartesianToIso transforms cartesian coordinates into isometric coordinates. @@ -571,15 +617,25 @@ func ScreenToCartesian(x, y int) (float64, float64) { } func HUDButtonAt(x, y int) *HUDButton { - for i, colorRect := range World.HUDButtonRects { - point := image.Point{x, y} - if point.In(colorRect) { + point := image.Point{x, y} + for i, rect := range World.HUDButtonRects { + if point.In(rect) { return HUDButtons[i] } } return nil } +func HelpButtonAt(x, y int) int { + point := image.Point{x, y} + for i, rect := range World.HelpButtonRects { + if point.In(rect) { + return i + } + } + return -1 +} + func SetHoverStructure(structureType int) { World.HoverStructure = structureType World.HUDUpdated = true @@ -630,6 +686,7 @@ var StructureTooltips = map[int]string{ StructureRoad: "Road", StructurePoliceStation: "Police station", StructurePowerPlantCoal: "Coal power plant", + StructurePowerPlantSolar: "Solar power plant", StructureResidentialZone: "Residential zone", StructureCommercialZone: "Commercial zone", StructureIndustrialZone: "Industrial zone", @@ -640,6 +697,7 @@ var StructureCosts = map[int]int{ StructureRoad: 25, StructurePoliceStation: 1000, StructurePowerPlantCoal: 4000, + StructurePowerPlantSolar: 10000, StructureResidentialZone: 100, StructureCommercialZone: 200, StructureIndustrialZone: 100, @@ -726,7 +784,8 @@ func ValidXY(x, y int) bool { } var PowerPlantCapacities = map[int]int{ - StructurePowerPlantCoal: 60, + StructurePowerPlantCoal: 60, + StructurePowerPlantSolar: 40, } var ZonePowerRequirement = map[int]int{ @@ -734,3 +793,16 @@ var ZonePowerRequirement = map[int]int{ StructureCommercialZone: 1, StructureIndustrialZone: 1, } + +func SetHelpPage(page int) { + World.HelpPage = page + World.HelpUpdated = true +} + +func IsPowerPlant(structureType int) bool { + return structureType == StructurePowerPlantCoal || structureType == StructurePowerPlantSolar +} + +func IsZone(structureType int) bool { + return structureType == StructureResidentialZone || structureType == StructureCommercialZone || structureType == StructureIndustrialZone +}