Browse Source

Add bulldozer sound effect

main
Trevor Slocum 8 months ago
parent
commit
55a106e84e
  1. 1
      README.md
  2. 18
      asset/asset.go
  3. 22
      asset/map/power_solar.tmx
  4. 6
      asset/sound/bulldozer/LICENSE
  5. BIN
      asset/sound/bulldozer/bulldozer.ogg
  6. 2
      asset/sound/select/LICENSE
  7. 28
      game/game.go
  8. 2
      go.mod
  9. 4
      go.sum
  10. 5
      main.go
  11. 220
      system/input_move.go
  12. 93
      system/populate.go
  13. 20
      system/powerscan.go
  14. 248
      system/renderhud.go
  15. 20
      world/help.go
  16. 10
      world/powermap.go
  17. 2
      world/structure.go
  18. 92
      world/world.go

1
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

18
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")

22
asset/map/power_solar.tmx

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.2" orientation="isometric" renderorder="right-down" width="5" height="5" tilewidth="64" tileheight="32" infinite="0" nextlayerid="6" nextobjectid="1">
<tileset firstgid="1" source="../image/tileset/MRMO_BRIK.tsx"/>
<layer id="1" name="1" width="5" height="5">
<data encoding="csv">
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
</data>
</layer>
<layer id="2" name="2" width="5" height="5" offsetx="0" offsety="-40">
<data encoding="csv">
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
</data>
</layer>
</map>

6
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/

BIN
asset/sound/bulldozer/bulldozer.ogg

Binary file not shown.

2
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/

28
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)
}
}

2
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

4
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=

5
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)

220
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))

93
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

20
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

248
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)
}

20
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*
`,
}

10
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

2
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 {

92
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) {