City-building simulation video game
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

376 lines
9.4 KiB

package game
import (
"image/color"
"math/rand"
"os"
"sync"
"code.rocketnine.space/tslocum/citylimits/entity"
"code.rocketnine.space/tslocum/citylimits/asset"
. "code.rocketnine.space/tslocum/citylimits/ecs"
"code.rocketnine.space/tslocum/citylimits/system"
"code.rocketnine.space/tslocum/citylimits/world"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
)
const sampleRate = 44100
// game is an isometric demo game.
type game struct {
w, h int
audioContext *audio.Context
op *ebiten.DrawImageOptions
disableEsc bool
debugMode bool
cpuProfile *os.File
movementSystem *system.MovementSystem
renderSystem *system.RenderSystem
addedSystems bool
updateTicks int
sync.Mutex
}
// NewGame returns a new isometric demo game.
func NewGame() (*game, error) {
g := &game{
audioContext: audio.NewContext(sampleRate),
op: &ebiten.DrawImageOptions{},
}
err := g.loadAssets()
if err != nil {
panic(err)
}
const numEntities = 30000
ECS.Preallocate(numEntities)
return g, nil
}
// Layout is called when the game's layout changes.
func (g *game) Layout(w, h int) (int, int) {
if w != g.w || h != g.h {
world.World.ScreenW, world.World.ScreenH = w, h
g.w, g.h = w, h
world.World.HUDUpdated = true
}
return g.w, g.h
}
func (g *game) Update() error {
if ebiten.IsWindowBeingClosed() {
g.Exit()
return nil
}
const updateSidebarDelay = 144 * 3
g.updateTicks++
if g.updateTicks == updateSidebarDelay {
world.World.HUDUpdated = true
//g.updateTicks = 0
// TODO
}
if world.World.ResetGame {
world.Reset()
err := world.LoadTileset()
if err != nil {
return err
}
// Fill below ground layer.
var img uint32
for x := range world.World.Level.Tiles[0] {
for y := range world.World.Level.Tiles[0][x] {
img = world.DirtTile
if rand.Intn(150) == 0 {
img = world.GrassTile
world.World.Level.Tiles[0][x][y].EnvironmentSprite = world.World.TileImages[img+world.World.TileImagesFirstGID]
for offsetX := -2 - rand.Intn(7); offsetX < 2+rand.Intn(7); offsetX++ {
for offsetY := -2 - rand.Intn(7); offsetY < 2+rand.Intn(7); offsetY++ {
if x+offsetX >= 0 && y+offsetY >= 0 && x+offsetX < 256 && y+offsetY < 256 {
world.World.Level.Tiles[0][x+offsetX][y+offsetY].EnvironmentSprite = world.World.TileImages[img+world.World.TileImagesFirstGID]
if rand.Intn(4) == 0 {
if rand.Intn(3) == 0 {
world.World.Level.Tiles[1][x+offsetX][y+offsetY].EnvironmentSprite = world.World.TileImages[world.TreeTileA+world.World.TileImagesFirstGID]
} else {
world.World.Level.Tiles[1][x+offsetX][y+offsetY].EnvironmentSprite = world.World.TileImages[world.TreeTileB+world.World.TileImagesFirstGID]
}
}
}
}
}
} else {
if world.World.Level.Tiles[0][x][y].EnvironmentSprite != nil {
continue
}
world.World.Level.Tiles[0][x][y].EnvironmentSprite = world.World.TileImages[img+world.World.TileImagesFirstGID]
}
}
}
// Load HUD sprites.
transparentBuilding := world.DrawMap(world.StructureCommercialHigh)
transparentImg := ebiten.NewImage(transparentBuilding.Bounds().Dx(), transparentBuilding.Bounds().Dy())
op := &ebiten.DrawImageOptions{}
op.ColorM.Scale(1, 1, 1, 0.4)
transparentImg.DrawImage(transparentBuilding, op)
world.HUDButtons = []*world.HUDButton{
{
StructureType: world.StructureBulldozer,
Sprite: world.DrawMap(world.StructureBulldozer),
SpriteOffsetX: 12,
SpriteOffsetY: -48,
},
nil,
{
StructureType: world.StructureRoad,
Sprite: world.DrawMap(world.StructureRoad),
SpriteOffsetX: -12,
SpriteOffsetY: -28,
},
{
StructureType: world.StructureResidentialZone,
Sprite: world.DrawMap(world.StructureResidentialLow),
SpriteOffsetX: -12,
SpriteOffsetY: -28,
}, {
StructureType: world.StructureCommercialZone,
Sprite: world.DrawMap(world.StructureCommercialLow),
SpriteOffsetX: -10,
SpriteOffsetY: -28,
}, {
StructureType: world.StructureIndustrialZone,
Sprite: world.DrawMap(world.StructureIndustrialLow),
SpriteOffsetX: -10,
SpriteOffsetY: -28,
}, {
StructureType: world.StructurePowerPlantCoal,
SpriteOffsetX: -20,
SpriteOffsetY: 2,
Sprite: world.DrawMap(world.StructurePowerPlantCoal),
}, {
StructureType: world.StructurePowerPlantSolar,
SpriteOffsetX: -20,
SpriteOffsetY: 2,
Sprite: world.DrawMap(world.StructurePowerPlantSolar),
}, {
StructureType: world.StructurePowerPlantNuclear,
SpriteOffsetX: -20,
SpriteOffsetY: 2,
Sprite: world.DrawMap(world.StructurePowerPlantNuclear),
},
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
nil,
{
StructureType: world.StructureToggleHelp,
Sprite: asset.ImgHelp,
SpriteOffsetX: 0,
SpriteOffsetY: -1,
},
{
StructureType: world.StructureToggleTransparentStructures,
Sprite: transparentImg,
SpriteOffsetX: -12,
SpriteOffsetY: -0,
},
}
// TODO
if world.World.Player == 0 {
world.World.Player = entity.NewPlayer()
}
if !g.addedSystems {
g.addSystems()
g.addedSystems = true // TODO
}
world.World.ResetGame = false
world.World.GameOver = false
}
err := ECS.Update()
if err != nil {
return err
}
return nil
}
// renderSprite renders a sprite on the screen.
func (g *game) renderSprite(x float64, y float64, offsetx float64, offsety float64, angle float64, geoScale float64, colorScale float64, alpha float64, hFlip bool, vFlip bool, sprite *ebiten.Image, target *ebiten.Image) int {
if alpha < .01 {
return 0
}
xi, yi := world.CartesianToIso(float64(x), float64(y))
padding := float64(world.TileSize) * world.World.CamScale
cx, cy := float64(world.World.ScreenW/2), float64(world.World.ScreenH/2)
// Skip drawing tiles that are out of the screen.
drawX, drawY := world.IsoToScreen(xi, yi)
if drawX+padding < 0 || drawY+padding < 0 || drawX-padding > float64(world.World.ScreenW) || drawY-padding > float64(world.World.ScreenH) {
return 0
}
g.op.GeoM.Reset()
if hFlip {
g.op.GeoM.Scale(-1, 1)
g.op.GeoM.Translate(world.TileSize, 0)
}
if vFlip {
g.op.GeoM.Scale(1, -1)
g.op.GeoM.Translate(0, world.TileSize)
}
// Move to current isometric position.
g.op.GeoM.Translate(xi, yi+offsety)
// Translate camera position.
g.op.GeoM.Translate(-world.World.CamX, -world.World.CamY)
// Zoom.
g.op.GeoM.Scale(world.World.CamScale, world.World.CamScale)
// Center.
g.op.GeoM.Translate(cx, cy)
g.op.ColorM.Reset()
g.op.ColorM.Scale(colorScale, colorScale, colorScale, alpha)
target.DrawImage(sprite, g.op)
g.op.ColorM.Reset()
/*s.op.GeoM.Scale(geoScale, geoScale)
// Rotate
s.op.GeoM.Translate(offsetx, offsety)
s.op.GeoM.Rotate(angle)
// Move to current isometric position.
s.op.GeoM.Translate(x, y)
// Translate camera position.
s.op.GeoM.Translate(-world.World.CamX, -world.World.CamY)
// Zoom.
s.op.GeoM.Scale(s.camScale, s.camScale)
// Center.
//s.op.GeoM.Translate(float64(s.ScreenW/2.0), float64(s.ScreenH/2.0))
s.op.ColorM.Scale(colorScale, colorScale, colorScale, alpha)
target.DrawImage(sprite, s.op)
s.op.ColorM.Reset()*/
return 1
}
func (g *game) Draw(screen *ebiten.Image) {
// Handle background rendering separately to simplify design.
var drawn int
for i := range world.World.Level.Tiles {
for x := range world.World.Level.Tiles[i] {
for y, tile := range world.World.Level.Tiles[i][x] {
if tile == nil {
continue
}
var sprite *ebiten.Image
colorScale := 1.0
alpha := 1.0
if tile.HoverSprite != nil {
sprite = tile.HoverSprite
colorScale = 0.6
if !world.World.HoverValid {
colorScale = 0.2
}
} else if tile.Sprite != nil {
sprite = tile.Sprite
if world.World.TransparentStructures && i > 1 {
alpha = 0.2
}
} else if tile.EnvironmentSprite != nil {
sprite = tile.EnvironmentSprite
} else {
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.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)
}
}
}
}
world.World.EnvironmentSprites = drawn
err := ECS.Draw(screen)
if err != nil {
panic(err)
}
}
func (g *game) addSystems() {
ecs := ECS
// Simulation systems.
ecs.AddSystem(system.NewTickSystem())
ecs.AddSystem(system.NewPowerScanSystem())
ecs.AddSystem(system.NewPopulateSystem())
ecs.AddSystem(system.NewTaxSystem())
// Input systems.
g.movementSystem = system.NewMovementSystem()
ecs.AddSystem(system.NewPlayerMoveSystem(world.World.Player, g.movementSystem))
ecs.AddSystem(system.NewplayerFireSystem())
ecs.AddSystem(g.movementSystem)
// Render systems.
ecs.AddSystem(system.NewCreepSystem())
ecs.AddSystem(system.NewCameraSystem())
g.renderSystem = system.NewRenderSystem()
ecs.AddSystem(g.renderSystem)
ecs.AddSystem(system.NewRenderHudSystem())
ecs.AddSystem(system.NewRenderDebugTextSystem(world.World.Player))
ecs.AddSystem(system.NewProfileSystem(world.World.Player))
}
func (g *game) loadAssets() error {
asset.ImgWhiteSquare.Fill(color.White)
asset.LoadSounds(g.audioContext)
return nil
}
func (g *game) Exit() {
os.Exit(0)
}