Add power scan

Zones are now scanned for whether or not they are connected to a power plant.
This commit is contained in:
Trevor Slocum 2022-01-20 18:25:09 -08:00
parent 69a012bedd
commit 6dbd53e9c4
14 changed files with 643 additions and 188 deletions

View File

@ -35,3 +35,4 @@ Please share issues and suggestions [here](https://code.rocketnine.space/tslocum
- [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

View File

@ -19,19 +19,21 @@ const sampleRate = 44100
//go:embed image map sound
var FS embed.FS
var ImgWhiteSquare = ebiten.NewImage(64, 64)
var ImgBlackSquare = ebiten.NewImage(64, 64)
var ImgBlank = ebiten.NewImage(1, 1)
var SoundMusic *audio.Player
var SoundSelect *audio.Player
var (
ImgBlank = ebiten.NewImage(1, 1)
ImgWhiteSquare = ebiten.NewImage(64, 64)
ImgBlackSquare = ebiten.NewImage(64, 64)
ImgPower = LoadImage("image/power.png")
)
var (
SoundPop1 *audio.Player
SoundPop2 *audio.Player
SoundPop3 *audio.Player
SoundPop4 *audio.Player
SoundPop5 *audio.Player
SoundMusic *audio.Player
SoundSelect *audio.Player
SoundPop1 *audio.Player
SoundPop2 *audio.Player
SoundPop3 *audio.Player
SoundPop4 *audio.Player
SoundPop5 *audio.Player
)
func init() {

BIN
asset/image/power.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

View File

@ -300,6 +300,11 @@ func (g *game) Draw(screen *ebiten.Image) {
continue
}
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] {
drawn += g.renderSprite(float64(x), float64(y), 0, -52, 0, 1, 1, 1, false, false, asset.ImgPower, screen)
}
}
}
}
@ -331,7 +336,6 @@ func (g *game) addSystems() {
g.renderSystem = system.NewRenderSystem()
ecs.AddSystem(g.renderSystem)
ecs.AddSystem(system.NewRenderHudSystem())
ecs.AddSystem(system.NewRenderMessageSystem())
ecs.AddSystem(system.NewRenderDebugTextSystem(world.World.Player))
ecs.AddSystem(system.NewProfileSystem(world.World.Player))
}

1
go.mod
View File

@ -4,6 +4,7 @@ go 1.17
require (
code.rocketnine.space/tslocum/gohan v0.0.0-20220106015515-0231e09ad78e
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482
github.com/hajimehoshi/ebiten/v2 v2.2.4
github.com/lafriks/go-tiled v0.7.0
golang.org/x/image v0.0.0-20211028202545-6944b10bf410

2
go.sum
View File

@ -1,6 +1,8 @@
code.rocketnine.space/tslocum/gohan v0.0.0-20220106015515-0231e09ad78e h1:n/4oueA0I1ilZpFGLlaCqaEDTX658fVQDtRkahdUDUI=
code.rocketnine.space/tslocum/gohan v0.0.0-20220106015515-0231e09ad78e/go.mod h1:nOvFBFvFPl5sDtkMy2Fn/7QZcWq5RE98/mK+INLqIWg=
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482 h1:p4g4uok3+r6Tg6fxXEQUAcMAX/WdK6WhkQW9s0jaT7k=
github.com/beefsack/go-astar v0.0.0-20200827232313-4ecf9e304482/go.mod h1:Cu3t5VeqE8kXjUBeNXWQprfuaP5UCIc5ggGjgMx9KFc=
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/disintegration/imaging v1.6.2/go.mod h1:44/5580QXChDfwIclfc/PCwrr44amcmDAg8hxG0Ewe4=

View File

@ -4,6 +4,7 @@ import (
"log"
"math/rand"
"os"
"strings"
"github.com/hajimehoshi/ebiten/v2/audio"
@ -159,7 +160,7 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) {
if s.scrollDragX == -1 && s.scrollDragY == -1 {
// TODO disabled due to possible ebiten bug
// TODO Disabled due to possible ebiten bug.
//ebiten.SetCursorMode(ebiten.CursorModeCaptured)
s.scrollDragX, s.scrollDragY = x, y
@ -186,6 +187,19 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
}
}
}
// Clamp viewport.
minCam := -256.0 * world.TileSize / 2
maxCam := 256.0 * world.TileSize / 2
if world.World.CamX < minCam {
world.World.CamX = minCam
} else if world.World.CamX > maxCam {
world.World.CamX = maxCam
}
if world.World.CamY < 0 {
world.World.CamY = 0
} else if world.World.CamY > maxCam {
world.World.CamY = maxCam
}
if x < world.SidebarWidth {
world.World.Level.ClearHoverSprites()
@ -198,6 +212,12 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
if button.StructureType == world.StructureToggleTransparentStructures {
world.World.TransparentStructures = !world.World.TransparentStructures
world.World.HUDUpdated = true
if world.World.TransparentStructures {
world.ShowMessage("Enabled transparency", 3)
} else {
world.ShowMessage("Disabled transparency", 3)
}
} else {
if world.World.HoverStructure == button.StructureType {
world.SetHoverStructure(0) // Deselect.
@ -222,8 +242,32 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
} else {
world.World.Level.ClearHoverSprites()
_, err := world.BuildStructure(world.World.HoverStructure, false, int(tileX), int(tileY))
// TODO draw hovers and build all roads in a line from drag start
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)
}
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),
}
world.World.Zones = append(world.World.Zones, zone)
}
if world.World.HoverStructure != world.StructureBulldozer {
sounds := []*audio.Player{
asset.SoundPop2,
@ -235,6 +279,16 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
}
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.HUDUpdated = true
}
world.BuildStructure(world.World.HoverStructure, true, int(tileX), int(tileY))

View File

@ -33,6 +33,8 @@ func (s *PopulateSystem) Update(_ *gohan.Context) error {
return nil
}
const popDuration = 144 * 4
// Thresholds.
const (
lowDensity = 3
@ -42,6 +44,8 @@ func (s *PopulateSystem) Update(_ *gohan.Context) error {
switch structureType {
case world.StructureResidentialZone:
switch {
case population == 0:
return world.StructureResidentialZone
case population <= lowDensity:
return world.StructureResidentialLow
case population <= mediumDensity:
@ -51,6 +55,8 @@ func (s *PopulateSystem) Update(_ *gohan.Context) error {
}
case world.StructureCommercialZone:
switch {
case population == 0:
return world.StructureCommercialZone
case population <= lowDensity:
return world.StructureCommercialLow
case population <= mediumDensity:
@ -60,6 +66,8 @@ func (s *PopulateSystem) Update(_ *gohan.Context) error {
}
case world.StructureIndustrialZone:
switch {
case population == 0:
return world.StructureIndustrialZone
case population <= lowDensity:
return world.StructureIndustrialLow
case population <= mediumDensity:
@ -73,12 +81,51 @@ func (s *PopulateSystem) Update(_ *gohan.Context) error {
}
const maxPopulation = 10
if world.World.Ticks%144 == 0 {
if world.World.Ticks%popDuration == 0 {
popR, popC, popI := world.Population()
targetR, targetC, targetI := world.TargetPopulation()
for _, zone := range world.World.Zones {
if zone.Population < maxPopulation {
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 {
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)

View File

@ -1,6 +1,10 @@
package system
import (
"log"
"github.com/beefsack/go-astar"
"code.rocketnine.space/tslocum/citylimits/component"
"code.rocketnine.space/tslocum/citylimits/world"
"code.rocketnine.space/tslocum/gohan"
@ -33,9 +37,142 @@ func (s *PowerScanSystem) Update(_ *gohan.Context) error {
return nil
}
const scanTicks = 144 * 2
if world.World.Ticks%scanTicks != 0 {
return nil
}
if !world.World.PowerUpdated {
return nil
}
var totalPowerAvailable int
powerRemaining := make([]int, len(world.World.PowerPlants))
for i, plant := range world.World.PowerPlants {
powerRemaining[i] = world.PowerPlantCapacities[plant.Type]
totalPowerAvailable += world.PowerPlantCapacities[plant.Type]
}
const (
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)
if t != nil {
powerSourceTiles[i] = append(powerSourceTiles[i], t)
}
t = world.World.Power.GetTile(plant.X-plantSize, plant.Y-y)
if t != nil {
powerSourceTiles[i] = append(powerSourceTiles[i], t)
}
}
for x := 0; x < plantSize; x++ {
t := world.World.Power.GetTile(plant.X-x, plant.Y+1)
if t != nil {
powerSourceTiles[i] = append(powerSourceTiles[i], t)
}
t = world.World.Power.GetTile(plant.X, plant.Y-plantSize)
if t != nil {
powerSourceTiles[i] = append(powerSourceTiles[i], t)
}
}
}
var totalPowerRequired int
var havePowerOut bool
world.ResetPowerOuts()
// TODO use a consistent procedure to check each building that needs power
// as connected via road to a power plant, and power-out buildings without enough power
// "citizens report brown-outs"
for _, zone := range world.World.Zones {
// TODO lock, set powered status on build immediately
powerRequired := world.ZonePowerRequirement[zone.Type]
_ = powerRequired
const zoneSize = 2
var powerDestinationTiles []*world.PowerMapTile
for y := 0; y < zoneSize; y++ {
t := world.World.Power.GetTile(zone.X+1, zone.Y-y)
if t != nil {
powerDestinationTiles = append(powerDestinationTiles, t)
}
t = world.World.Power.GetTile(zone.X-zoneSize, zone.Y-y)
if t != nil {
powerDestinationTiles = append(powerDestinationTiles, t)
}
}
for x := 0; x < zoneSize; x++ {
t := world.World.Power.GetTile(zone.X-x, zone.Y+1)
if t != nil {
powerDestinationTiles = append(powerDestinationTiles, t)
}
t = world.World.Power.GetTile(zone.X, zone.Y-zoneSize)
if t != nil {
powerDestinationTiles = append(powerDestinationTiles, t)
}
}
var powered bool
FINDPOWERPATH:
for j := range powerRemaining {
if powerRemaining[j] < powerRequired {
continue
}
for _, powerSource := range powerSourceTiles[j] {
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)
if found {
log.Printf("Resulting path\n%+v %f", p, dist)
powerRemaining[j] -= powerRequired
powered = true
break FINDPOWERPATH
}
}
}
}
zone.Powered = powered
log.Println("ZONE", zone, zone.Powered)
if !powered {
havePowerOut = true
world.World.PowerOuts[zone.X][zone.Y] = true
}
totalPowerRequired += powerRequired
}
if !havePowerOut {
world.World.PowerUpdated = false
}
world.World.PowerAvailable, world.World.PowerNeeded = totalPowerAvailable, totalPowerRequired
return nil
}

View File

@ -56,6 +56,7 @@ func (s *RenderHudSystem) Draw(_ *gohan.Context, screen *ebiten.Image) error {
if world.World.HUDUpdated {
s.hudImg.Clear()
s.drawSidebar()
s.drawMessages()
s.drawTooltip()
world.World.HUDUpdated = false
}
@ -67,7 +68,7 @@ func (s *RenderHudSystem) drawSidebar() {
bounds := s.hudImg.Bounds()
if bounds.Dx() != world.World.ScreenW || bounds.Dy() != world.World.ScreenH {
s.hudImg = ebiten.NewImage(world.World.ScreenW, world.World.ScreenH)
s.tmpImg = ebiten.NewImage(world.SidebarWidth, world.World.ScreenH)
s.tmpImg = ebiten.NewImage(world.World.ScreenW, world.World.ScreenH)
s.tmpImg2 = ebiten.NewImage(world.SidebarWidth, world.World.ScreenH)
} else {
s.hudImg.Clear()
@ -126,6 +127,8 @@ func (s *RenderHudSystem) drawSidebar() {
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}
@ -136,8 +139,14 @@ func (s *RenderHudSystem) drawSidebar() {
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, rciY+(rciSize/2), barX+barWidth, rciY+(rciSize/2)-barHeight)).(*ebiten.Image).Fill(clr)
s.tmpImg.SubImage(image.Rect(barX, barY, barX+barWidth, barY-barHeight)).(*ebiten.Image).Fill(clr)
}
drawDemandBar(demandR, colorR, 0)
drawDemandBar(demandC, colorC, 1)
@ -145,7 +154,6 @@ func (s *RenderHudSystem) drawSidebar() {
// Draw RCI button.
const rciButtonPadding = 12
const rciButtonHeight = 20
const rciButtonLabelPaddingX = 6
const rciButtonLabelPaddingY = 1
rciButtonY := rciY + (rciSize / 2) - (rciButtonHeight / 2)
@ -208,10 +216,11 @@ func (s *RenderHudSystem) drawTooltip() {
}
lines := 1 + strings.Count(label, "\n")
max := maxLen(strings.Split(label, "\n"))
scale := 3.0
x, y := world.SidebarWidth, 0
w, h := (len(label)*6+10)*int(scale), 22*(int(scale))*lines
w, h := (max*6+10)*int(scale), 16*(int(scale))*lines+10
r := image.Rect(x, y, x+w, y+h)
s.hudImg.SubImage(r).(*ebiten.Image).Fill(color.RGBA{0, 0, 0, 120})
@ -219,7 +228,55 @@ func (s *RenderHudSystem) drawTooltip() {
ebitenutil.DebugPrint(s.tmpImg, label)
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(scale, scale)
op.GeoM.Translate(world.SidebarWidth+(4*scale), 4)
op.GeoM.Translate(world.SidebarWidth+(4*scale), 2)
s.hudImg.DrawImage(s.tmpImg, op)
}
func maxLen(v []string) int {
max := 0
for _, line := range v {
l := len(line)
if l > max {
max = l
}
}
return max
}
func (s *RenderHudSystem) drawMessages() {
lines := len(world.World.Messages)
if lines == 0 {
return
}
/*var label string
max := maxLen(world.World.Messages)
for i := lines - 1; i >= 0; i-- {
if i != lines-1 {
label += "\n"
}
for j := max - len(world.World.Messages[i]); j > 0; j-- {
label += " "
}
label += world.World.Messages[i]
}*/
label := world.World.Messages[len(world.World.Messages)-1]
max := len(label)
lines = 1
const padding = 12
scale := 2.0
w, h := (max*6+10)*int(scale), 16*(int(scale))*lines+6
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})
s.tmpImg.Clear()
ebitenutil.DebugPrint(s.tmpImg, label)
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(scale, scale)
op.GeoM.Translate(float64(x)+padding, 0)
s.hudImg.DrawImage(s.tmpImg, op)
}

View File

@ -1,100 +0,0 @@
package system
import (
"image/color"
"strings"
"code.rocketnine.space/tslocum/citylimits/component"
"code.rocketnine.space/tslocum/citylimits/world"
"code.rocketnine.space/tslocum/gohan"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
)
type RenderMessageSystem struct {
op *ebiten.DrawImageOptions
logoImg *ebiten.Image
msgImg *ebiten.Image
tmpImg *ebiten.Image
}
func NewRenderMessageSystem() *RenderMessageSystem {
s := &RenderMessageSystem{
op: &ebiten.DrawImageOptions{},
logoImg: ebiten.NewImage(1, 1),
msgImg: ebiten.NewImage(1, 1),
tmpImg: ebiten.NewImage(200, 200),
}
return s
}
func (s *RenderMessageSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.VelocityComponentID,
component.WeaponComponentID,
}
}
func (s *RenderMessageSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *RenderMessageSystem) Update(_ *gohan.Context) error {
if !world.World.GameStarted || world.World.GameOver || !world.World.MessageVisible {
return nil
}
world.World.MessageTicks++
if world.World.MessageTicks == world.World.MessageDuration {
world.World.MessageVisible = false
return nil
}
return nil
}
func (s *RenderMessageSystem) Draw(_ *gohan.Context, screen *ebiten.Image) error {
if !world.World.GameStarted || !world.World.MessageVisible {
return nil
}
// Draw message.
if world.World.MessageUpdated {
s.drawMessage()
}
bounds := s.msgImg.Bounds()
x := (float64(world.World.ScreenW) / 2) - (float64(bounds.Dx()) / 2)
y := (float64(world.World.ScreenH) / 2) - (float64(bounds.Dy()) / 2)
s.op.GeoM.Reset()
s.op.GeoM.Translate(x, y)
screen.DrawImage(s.msgImg, s.op)
return nil
}
func (s *RenderMessageSystem) drawMessage() {
split := strings.Split(world.World.MessageText, "\n")
width := 0
for _, line := range split {
lineSize := len(line) * 12
if lineSize > width {
width = lineSize
}
}
height := len(split) * 32
const padding = 8
width, height = width+padding*2, height+padding*2
s.msgImg = ebiten.NewImage(width, height)
s.msgImg.Fill(color.RGBA{17, 17, 17, 255})
s.tmpImg.Clear()
s.tmpImg = ebiten.NewImage(width*2, height*2)
s.op.GeoM.Reset()
s.op.GeoM.Scale(2, 2)
s.op.GeoM.Translate(float64(padding), float64(padding))
ebitenutil.DebugPrint(s.tmpImg, world.World.MessageText)
s.msgImg.DrawImage(s.tmpImg, s.op)
s.op.ColorM.Reset()
}

View File

@ -38,6 +38,9 @@ func (s *TickSystem) Update(_ *gohan.Context) error {
if world.World.Ticks%world.MonthTicks == 0 {
world.World.HUDUpdated = true
}
if world.World.Ticks%144 == 0 {
world.TickMessages()
}
return nil
}

153
world/powermap.go Normal file
View File

@ -0,0 +1,153 @@
package world
import (
"github.com/beefsack/go-astar"
)
const (
powerEmptyTile = iota
powerSourceTile
powerDestinationTile
)
type PowerMapTile struct {
X int
Y int
CarriesPower bool // Set to true for roads and all building tiles (even power plants)
}
func (t *PowerMapTile) Up() *PowerMapTile {
tx, ty := t.X, t.Y-1
if !ValidXY(tx, ty) {
return nil
}
n := World.Power[tx][ty]
if !n.CarriesPower {
return nil
}
return n
}
func (t *PowerMapTile) Down() *PowerMapTile {
tx, ty := t.X, t.Y+1
if !ValidXY(tx, ty) {
return nil
}
n := World.Power[tx][ty]
if !n.CarriesPower {
return nil
}
return n
}
func (t *PowerMapTile) Left() *PowerMapTile {
tx, ty := t.X-1, t.Y
if !ValidXY(tx, ty) {
return nil
}
n := World.Power[tx][ty]
if !n.CarriesPower {
return nil
}
return n
}
func (t *PowerMapTile) Right() *PowerMapTile {
tx, ty := t.X+1, t.Y
if !ValidXY(tx, ty) {
return nil
}
n := World.Power[tx][ty]
if !n.CarriesPower {
return nil
}
return n
}
type PowerMap [][]*PowerMapTile
func newPowerMap() PowerMap {
m := make(PowerMap, 256)
for x := 0; x < 256; x++ {
m[x] = make([]*PowerMapTile, 256)
for y := 0; y < 256; y++ {
m[x][y] = &PowerMapTile{
X: x,
Y: y,
}
}
}
return m
}
func newPowerOuts() [][]bool {
m := make([][]bool, 256)
for x := 0; x < 256; x++ {
m[x] = make([]bool, 256)
}
return m
}
func ResetPowerOuts() {
for x := 0; x < 256; x++ {
for y := 0; y < 256; y++ {
World.PowerOuts[x][y] = false
}
}
}
func (m PowerMap) GetTile(x, y int) *PowerMapTile {
if !ValidXY(x, y) {
return nil
}
return m[x][y]
}
func (m PowerMap) SetTile(x, y int, carriesPower bool) {
t := m[x][y]
t.CarriesPower = carriesPower
World.PowerUpdated = true
}
func (t *PowerMapTile) PathNeighbors() []astar.Pather {
var neighbors []astar.Pather
n := t.Up()
if n != nil {
neighbors = append(neighbors, n)
}
n = t.Down()
if n != nil {
neighbors = append(neighbors, n)
}
n = t.Left()
if n != nil {
neighbors = append(neighbors, n)
}
n = t.Right()
if n != nil {
neighbors = append(neighbors, n)
}
return neighbors
}
func (t *PowerMapTile) PathNeighborCost(to astar.Pather) float64 {
toT := to.(*PowerMapTile)
if !toT.CarriesPower {
return 0
}
return 1
}
func (t *PowerMapTile) PathEstimatedCost(to astar.Pather) float64 {
toT := to.(*PowerMapTile)
absX := toT.X - t.X
if absX < 0 {
absX = -absX
}
absY := toT.Y - t.Y
if absY < 0 {
absY = -absY
}
return float64(absX + absY)
}

View File

@ -5,9 +5,11 @@ import (
"fmt"
"image"
"log"
"math"
"math/rand"
"path/filepath"
"strconv"
"sync"
"golang.org/x/text/language"
"golang.org/x/text/message"
@ -22,6 +24,8 @@ import (
const startingYear = 1950
const maxPopulation = 100000
const (
MonthTicks = 144 * 3
YearTicks = MonthTicks * 12
@ -59,12 +63,20 @@ var World = &GameWorld{
ResetGame: true,
Level: NewLevel(256),
Printer: message.NewPrinter(language.English),
Power: newPowerMap(),
PowerOuts: newPowerOuts(),
}
type Zone struct {
Type int // StructureResidentialZone, StructureCommercialZone or StructureIndustrialZone
X, Y int
Population int
Powered bool
}
type PowerPlant struct {
Type int
X, Y int
}
type GameWorld struct {
@ -83,12 +95,6 @@ type GameWorld struct {
GameStartedTicks int
GameOver bool
MessageVisible bool
MessageTicks int
MessageDuration int
MessageUpdated bool
MessageText string
PlayerX, PlayerY float64
CamX, CamY float64
@ -136,7 +142,10 @@ type GameWorld struct {
HUDUpdated bool
HUDButtonRects []image.Rectangle
Zones []*Zone
PowerPlants []*PowerPlant
Zones []*Zone
PowerOuts [][]bool
Ticks int
@ -148,6 +157,14 @@ type GameWorld struct {
TransparentStructures bool
Messages []string
MessagesTicks []int
Power PowerMap
PowerUpdated bool
PowerAvailable int
PowerNeeded int
resetTipShown bool
}
@ -171,8 +188,6 @@ func Reset() {
World.TriggerEntities = nil
World.TriggerRects = nil
World.TriggerNames = nil
World.MessageVisible = false
}
func LoadMap(structureType int) (*tiled.Map, error) {
@ -269,32 +284,6 @@ func LoadTileset() error {
}
func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Structure, error) {
if structureType == StructureBulldozer && !hover {
// TODO bulldoze entire structure, remove from zones
var bulldozed bool
for i := range World.Level.Tiles {
if World.Level.Tiles[i][placeX][placeY].Sprite != nil {
World.Level.Tiles[i][placeX][placeY].Sprite = nil
bulldozed = true
}
var img *ebiten.Image
if i == 0 {
img = World.TileImages[DirtTile+World.TileImagesFirstGID]
}
if World.Level.Tiles[i][placeX][placeY].EnvironmentSprite != img {
World.Level.Tiles[i][placeX][placeY].EnvironmentSprite = img
bulldozed = true
}
}
if !bulldozed {
return nil, errors.New("nothing to bulldoze")
}
return nil, nil
}
initialType := structureType
// For previewing buildings
/*v := rand.Intn(3)
if structureType == StructureResidentialZone {
@ -343,10 +332,41 @@ func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Str
w := m.Width - 1
h := m.Height - 1
if placeX-w < 0 || placeY-h < 0 || placeX > 256 || placeY > 256 {
if placeX-w < 0 || placeY-h < 0 || placeX >= 256 || placeY >= 256 {
return nil, errors.New("invalid location: building does not fit")
}
structure := &Structure{
Type: structureType,
X: placeX,
Y: placeY,
}
if structureType == StructureBulldozer && !hover {
// TODO bulldoze entire structure, remove from zones
var bulldozed bool
for i := range World.Level.Tiles {
if World.Level.Tiles[i][placeX][placeY].Sprite != nil {
World.Level.Tiles[i][placeX][placeY].Sprite = nil
bulldozed = true
}
var img *ebiten.Image
if i == 0 {
img = World.TileImages[DirtTile+World.TileImagesFirstGID]
}
if World.Level.Tiles[i][placeX][placeY].EnvironmentSprite != img {
World.Level.Tiles[i][placeX][placeY].EnvironmentSprite = img
bulldozed = true
}
}
if !bulldozed {
return nil, errors.New("nothing to bulldoze")
}
World.Power.SetTile(placeX, placeY, false)
return structure, nil
}
createTileEntity := func(t *tiled.LayerTile, x float64, y float64) gohan.Entity {
mapTile := ECS.NewEntity()
ECS.AddComponent(mapTile, &component.PositionComponent{
@ -366,12 +386,6 @@ func BuildStructure(structureType int, hover bool, placeX int, placeY int) (*Str
}
_ = createTileEntity
structure := &Structure{
Type: structureType,
X: placeX,
Y: placeY,
}
// TODO Add entity
tileOccupied := func(tx int, ty int) bool {
@ -457,6 +471,11 @@ VALIDBUILD:
}
} else {
World.Level.Tiles[layerNum][tx][ty].Sprite = World.TileImages[t.Tileset.FirstGID+t.ID]
if structureType == StructureRoad {
World.Power.SetTile(tx, ty, true)
}
World.PowerUpdated = true
}
// TODO handle flipping
@ -464,16 +483,6 @@ VALIDBUILD:
}
}
isZone := initialType == StructureResidentialZone || initialType == StructureCommercialZone || initialType == StructureIndustrialZone
if !hover && isZone {
zone := &Zone{
Type: initialType,
X: placeX,
Y: placeY,
}
World.Zones = append(World.Zones, zone)
}
return structure, nil
}
@ -528,14 +537,6 @@ func StartGame() {
}
}
func SetMessage(message string, duration int) {
World.MessageText = message
World.MessageVisible = true
World.MessageUpdated = true
World.MessageDuration = duration
World.MessageTicks = 0
}
// CartesianToIso transforms cartesian coordinates into isometric coordinates.
func CartesianToIso(x, y float64) (float64, float64) {
ix := (x - y) * float64(TileSize/2)
@ -584,14 +585,46 @@ func SetHoverStructure(structureType int) {
World.HUDUpdated = true
}
func Demand() (r, c, i float64) {
r = (rand.Float64() * 2) - 1
c = (rand.Float64() * 2) - 1
i = (rand.Float64() * 2) - 1
return r, c, i
func Satisfaction() (r, c, i float64) {
return 0.01, 0.0, 0.02
}
var structureTooltips = map[int]string{
func TargetPopulation() (r, c, i int) {
currentMax := maxPopulation * ((1 + float64(World.Ticks/(MonthTicks*7))) / 108)
satisfactionR, satisfactionC, satisfactionI := Satisfaction()
return int(satisfactionR * currentMax), int(satisfactionC * currentMax), int(satisfactionI * currentMax)
}
func Demand() (r, c, i float64) {
targetR, targetC, targetI := TargetPopulation()
populationR, populationC, populationI := Population()
r, c, i = float64(targetR)-float64(populationR), float64(targetC)-float64(populationC), float64(targetI)-float64(populationI)
max := r
if c > max {
max = c
}
if i > max {
max = i
}
barPeak := 100.0
r, c, i = r/barPeak, c/barPeak, i/barPeak
clamp := func(v float64) float64 {
if math.IsNaN(v) {
return 0
}
if v < -1 {
v = -1
} else if v > 1 {
v = 1
}
return v
}
return clamp(r), clamp(c), clamp(i)
}
var StructureTooltips = map[int]string{
StructureToggleTransparentStructures: "Transparent buildings",
StructureBulldozer: "Bulldozer",
StructureRoad: "Road",
@ -613,7 +646,7 @@ var StructureCosts = map[int]int{
}
func Tooltip() string {
tooltipText := structureTooltips[World.HoverStructure]
tooltipText := StructureTooltips[World.HoverStructure]
cost := StructureCosts[World.HoverStructure]
if cost > 0 {
tooltipText += World.Printer.Sprintf("\n$%d", cost)
@ -640,3 +673,64 @@ func Date() (month string, year string) {
y, m := World.Ticks/YearTicks, (World.Ticks%YearTicks)/MonthTicks
return monthNames[m], strconv.Itoa(startingYear + y)
}
func Population() (r, c, i int) {
for _, zone := range World.Zones {
switch zone.Type {
case StructureResidentialZone:
r += zone.Population
case StructureCommercialZone:
c += zone.Population
case StructureIndustrialZone:
i += zone.Population
}
}
return r, c, i
}
var messageLock = &sync.Mutex{}
const messageDuration = 144 * 3
func TickMessages() {
messageLock.Lock()
defer messageLock.Unlock()
var removed int
for j := 0; j < len(World.MessagesTicks); j++ {
i := j - removed
if World.MessagesTicks[i] == 0 {
World.Messages = append(World.Messages[:i], World.Messages[i+1:]...)
World.MessagesTicks = append(World.MessagesTicks[:i], World.MessagesTicks[i+1:]...)
removed++
World.HUDUpdated = true
} else if World.MessagesTicks[i] > 0 {
World.MessagesTicks[i]--
}
}
}
func ShowMessage(message string, duration int) {
messageLock.Lock()
defer messageLock.Unlock()
World.Messages = append(World.Messages, message)
World.MessagesTicks = append(World.MessagesTicks, duration)
World.HUDUpdated = true
}
func ValidXY(x, y int) bool {
return x >= 0 && y >= 0 && x < 256 && y < 256
}
var PowerPlantCapacities = map[int]int{
StructurePowerPlantCoal: 60,
}
var ZonePowerRequirement = map[int]int{
StructureResidentialZone: 1,
StructureCommercialZone: 1,
StructureIndustrialZone: 1,
}