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.

473 lines
13 KiB

7 months ago
package system
import (
7 months ago
"errors"
"math/rand"
7 months ago
"os"
"strings"
7 months ago
"github.com/hajimehoshi/ebiten/v2/audio"
"code.rocketnine.space/tslocum/citylimits/asset"
7 months ago
"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/inpututil"
)
type playerMoveSystem struct {
player gohan.Entity
movement *MovementSystem
lastWalkDirL bool
rewindTicks int
nextRewindTick int
scrollDragX, scrollDragY int
scrollCamStartX, scrollCamStartY float64
7 months ago
}
func NewPlayerMoveSystem(player gohan.Entity, m *MovementSystem) *playerMoveSystem {
return &playerMoveSystem{
player: player,
movement: m,
scrollDragX: -1,
scrollDragY: -1,
7 months ago
}
}
func (_ *playerMoveSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.VelocityComponentID,
component.WeaponComponentID,
}
}
func (_ *playerMoveSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *playerMoveSystem) buildStructure(structureType int, tileX int, tileY int, playSound bool) (*world.Structure, error) {
7 months ago
cost := world.StructureCosts[structureType]
if world.World.Funds < cost {
world.ShowMessage("Insufficient funds", 3)
return nil, errors.New("insufficient funds")
}
structure, err := world.BuildStructure(world.World.HoverStructure, false, tileX, tileY)
7 months ago
if err == nil || world.World.HoverStructure == world.StructureBulldozer {
7 months ago
world.World.LastBuildX, world.World.LastBuildY = tileX, tileY
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()
}
7 months ago
if err == nil {
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
}
7 months ago
func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
if ebiten.IsKeyPressed(ebiten.KeyEscape) && !world.World.DisableEsc {
os.Exit(0)
return nil
}
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyV) {
v := 1
if ebiten.IsKeyPressed(ebiten.KeyShift) {
v = 2
}
if world.World.Debug == v {
world.World.Debug = 0
} else {
world.World.Debug = v
}
return nil
}
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyN) {
world.World.NoClip = !world.World.NoClip
return nil
}
if !world.World.GameStarted {
if ebiten.IsKeyPressed(ebiten.KeyEnter) || ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
world.StartGame()
}
return nil
}
if inpututil.IsKeyJustPressed(ebiten.KeyM) {
world.World.MuteMusic = !world.World.MuteMusic
if world.World.MuteMusic {
asset.SoundMusic.Pause()
} else {
asset.SoundMusic.Play()
}
7 months ago
}
if world.World.GameOver {
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
world.World.ResetGame = true
}
return nil
}
// Update target zoom level.
var scrollY float64
if ebiten.IsKeyPressed(ebiten.KeyC) || ebiten.IsKeyPressed(ebiten.KeyPageDown) {
scrollY = -0.25
} else if ebiten.IsKeyPressed(ebiten.KeyE) || ebiten.IsKeyPressed(ebiten.KeyPageUp) {
scrollY = .25
} else {
_, scrollY = ebiten.Wheel()
if scrollY < -1 {
scrollY = -1
} else if scrollY > 1 {
scrollY = 1
}
}
world.World.CamScaleTarget += scrollY * (world.World.CamScaleTarget / 7)
7 months ago
if world.World.CamScaleTarget < world.CameraMinZoom {
world.World.CamScaleTarget = world.CameraMinZoom
} else if world.World.CamScaleTarget > world.CameraMaxZoom {
world.World.CamScaleTarget = world.CameraMaxZoom
}
// Smooth zoom transition.
div := 10.0
if world.World.CamScaleTarget > world.World.CamScale {
world.World.CamScale += (world.World.CamScaleTarget - world.World.CamScale) / div
} else if world.World.CamScaleTarget < world.World.CamScale {
world.World.CamScale -= (world.World.CamScale - world.World.CamScaleTarget) / div
}
7 months ago
pressLeft := ebiten.IsKeyPressed(ebiten.KeyLeft) || ebiten.IsKeyPressed(ebiten.KeyA)
pressRight := ebiten.IsKeyPressed(ebiten.KeyRight) || ebiten.IsKeyPressed(ebiten.KeyD)
pressUp := ebiten.IsKeyPressed(ebiten.KeyUp) || ebiten.IsKeyPressed(ebiten.KeyW)
pressDown := ebiten.IsKeyPressed(ebiten.KeyDown) || ebiten.IsKeyPressed(ebiten.KeyS)
7 months ago
const camSpeed = 10
if (pressLeft && !pressRight) ||
(pressRight && !pressLeft) {
if pressLeft {
world.World.CamX -= camSpeed
} else {
world.World.CamX += camSpeed
}
}
if (pressUp && !pressDown) ||
(pressDown && !pressUp) {
if pressUp {
world.World.CamY -= camSpeed
} else {
world.World.CamY += camSpeed
}
}
7 months ago
const scrollEdgeSize = 1
7 months ago
x, y := ebiten.CursorPosition()
if !world.World.GotCursorPosition {
if x != 0 || y != 0 {
world.World.GotCursorPosition = true
} else {
return nil
}
}
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonMiddle) {
if s.scrollDragX == -1 && s.scrollDragY == -1 {
// TODO Disabled due to possible ebiten bug.
7 months ago
//ebiten.SetCursorMode(ebiten.CursorModeCaptured)
s.scrollDragX, s.scrollDragY = x, y
s.scrollCamStartX, s.scrollCamStartY = world.World.CamX, world.World.CamY
} else {
dx, dy := float64(x-s.scrollDragX)/world.World.CamScale, float64(y-s.scrollDragY)/world.World.CamScale
world.World.CamX, world.World.CamY = s.scrollCamStartX-dx, s.scrollCamStartY-dy
}
} else {
if s.scrollDragX != -1 && s.scrollDragY != -1 {
s.scrollDragX, s.scrollDragY = -1, -1
7 months ago
//ebiten.SetCursorMode(ebiten.CursorModeVisible)
} else if x >= -2 && y >= -2 && x < world.World.ScreenW+2 && y < world.World.ScreenH+2 {
// Pan via screen edge.
if x <= scrollEdgeSize {
world.World.CamX -= camSpeed
} else if x >= world.World.ScreenW-scrollEdgeSize-1 {
world.World.CamX += camSpeed
}
if y <= scrollEdgeSize {
world.World.CamY -= camSpeed
} else if y >= world.World.ScreenH-scrollEdgeSize-1 {
world.World.CamY += camSpeed
}
}
7 months ago
}
// 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
}
7 months ago
7 months ago
if x < world.SidebarWidth {
world.World.Level.ClearHoverSprites()
7 months ago
world.World.HoverX, world.World.HoverY = 0, 0
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
button := world.HUDButtonAt(x, y)
if button != nil {
if button.StructureType != 0 {
if button.StructureType == world.StructureToggleHelp {
if world.World.HelpPage != -1 {
world.SetHelpPage(-1)
} else {
world.SetHelpPage(0)
}
} else 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)
}
7 months ago
} else {
if world.World.HoverStructure == button.StructureType {
world.SetHoverStructure(0) // Deselect.
} else {
world.SetHoverStructure(button.StructureType)
}
7 months ago
}
asset.SoundSelect.Rewind()
asset.SoundSelect.Play()
7 months ago
}
7 months ago
} else if world.AltButtonAt(x, y) == 0 {
world.World.ShowRCIWindow = !world.World.ShowRCIWindow
world.World.HUDUpdated = true
asset.SoundSelect.Rewind()
asset.SoundSelect.Play()
7 months ago
}
}
return nil
}
7 months ago
world.HandleRCIWindowClick(x, y)
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
}
7 months ago
tileX, tileY := world.ScreenToCartesian(x, y)
7 months ago
if tileX >= 0 && tileY >= 0 && tileX < 256 && tileY < 256 {
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
7 months ago
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) || (multiUseStructure && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)) || (multiUseStructure && dragStarted) {
if !dragStarted && world.World.Funds >= world.StructureCosts[world.World.HoverStructure] {
world.World.BuildDragX, world.World.BuildDragY = int(tileX), int(tileY)
if world.World.HoverStructure == world.StructureBulldozer {
asset.SoundBulldoze.Play()
}
}
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 builtRoad bool
7 months ago
cost := world.StructureCosts[world.World.HoverStructure] * len(tiles) / 2
if cost <= world.World.Funds {
cost = 0
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
}
}
7 months ago
if cost > 0 {
world.ShowBuildCost(world.World.HoverStructure, cost)
}
} else {
world.ShowMessage("Insufficient funds", 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()
7 months ago
world.BuildStructure(world.World.HoverStructure, true, int(tileX), int(tileY))
var cost int
for _, tile := range tiles {
world.BuildStructure(world.World.HoverStructure, true, tile[0], tile[1])
7 months ago
cost += world.StructureCosts[world.World.HoverStructure]
}
7 months ago
world.World.HoverValid = cost <= world.World.Funds
}
return nil
} else if dragStarted && !ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
world.World.BuildDragX, world.World.BuildDragY = -1, -1
asset.SoundBulldoze.Pause()
}
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)
7 months ago
if !multiUseStructure {
world.World.HoverStructure = 0
world.World.BuildDragX, world.World.BuildDragY = -1, -1
world.World.LastBuildX, world.World.LastBuildY = -1, -1
}
}
7 months ago
if world.World.HoverStructure > 0 {
world.BuildStructure(world.World.HoverStructure, true, int(tileX), int(tileY))
}
7 months ago
}
} else {
world.World.Level.ClearHoverSprites()
world.BuildStructure(world.World.HoverStructure, true, int(tileX), int(tileY))
7 months ago
}
world.World.HoverX, world.World.HoverY = int(tileX), int(tileY)
7 months ago
}
}
return nil
}
func (s *playerMoveSystem) Draw(_ *gohan.Context, _ *ebiten.Image) error {
return gohan.ErrSystemWithoutDraw
}
func deltaXY(x1, y1, x2, y2 float64) (dx float64, dy float64) {
dx, dy = x1-x2, y1-y2
if dx < 0 {
dx *= -1
}
if dy < 0 {
dy *= -1
}
return dx, dy
}