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