Files
LilGuy/internal/game/game.go
2025-11-25 11:59:59 -07:00

409 lines
8.6 KiB
Go

package game
import (
"time"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/inpututil"
"github.com/atridad/LilGuy/internal/save"
"github.com/atridad/LilGuy/internal/screens"
"github.com/atridad/LilGuy/internal/ui/menu"
"github.com/atridad/LilGuy/internal/maps"
)
const (
ScreenWidth = 960
ScreenHeight = 540
TargetTPS = 60
WindowTitle = "Lil Guy"
)
// Game states
type gameState int
const (
stateSplash gameState = iota
stateTitle
statePlaying
statePaused
)
// FPS cap options
type FPSCap int
const (
FPSCap60 FPSCap = iota
FPSCap120
FPSCapUncapped
fpsCapCount
)
func (f FPSCap) TPS() int {
switch f {
case FPSCap60:
return 60
case FPSCap120:
return 120
case FPSCapUncapped:
return -1
default:
return 60
}
}
func (f FPSCap) String() string {
switch f {
case FPSCap60:
return "60 FPS"
case FPSCap120:
return "120 FPS"
case FPSCapUncapped:
return "Uncapped"
default:
return "60 FPS"
}
}
func (f *FPSCap) Cycle() {
*f = (*f + 1) % fpsCapCount
}
// Player input
type controls struct {
Left bool
Right bool
Jump bool
Sprint bool
Shoot bool
}
func readControls() controls {
return controls{
Left: ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA),
Right: ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD),
Jump: ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW) || ebiten.IsKeyPressed(ebiten.KeySpace),
Sprint: ebiten.IsKeyPressed(ebiten.KeyShift),
Shoot: inpututil.IsKeyJustPressed(ebiten.KeyK),
}
}
type Game struct {
state *state
}
// Main game state
type state struct {
gameState gameState
lastTick time.Time
splashScreen *screens.SplashScreen
titleScreen *screens.TitleScreen
gameplayScreen *screens.GameplayScreen
pauseMenu *menu.PauseMenu
fpsEnabled bool
fpsCap FPSCap
portalVisibility bool
saveManager *save.Manager
lastAutoSave time.Time
autoSaveInterval time.Duration
}
func New() *Game {
return &Game{state: newState()}
}
func newState() *state {
now := time.Now()
s := &state{
gameState: stateSplash,
lastTick: now,
fpsEnabled: false,
fpsCap: FPSCap60,
portalVisibility: false,
lastAutoSave: now,
autoSaveInterval: 30 * time.Second,
}
saveManager, err := save.NewManager()
if err != nil {
saveManager = nil
}
s.saveManager = saveManager
if saveManager != nil {
if settings, err := saveManager.LoadSettings(); err == nil {
s.fpsEnabled = settings.FPSMonitor
s.portalVisibility = settings.PortalVisibility
switch settings.FPSCap {
case "60":
s.fpsCap = FPSCap60
case "120":
s.fpsCap = FPSCap120
case "uncapped":
s.fpsCap = FPSCapUncapped
default:
s.fpsCap = FPSCap60
}
}
}
// Initialize Map Manager
mapManager := maps.NewManager()
plains, desert := maps.CreateDefaultMaps(float64(ScreenWidth), float64(ScreenHeight))
// Bake maps for performance
plains.Bake()
desert.Bake()
mapManager.RegisterMap(plains)
mapManager.RegisterMap(desert)
mapManager.SetCurrentMap("plains")
// Initialize screens
s.splashScreen = screens.NewSplashScreen()
s.titleScreen = screens.NewTitleScreen()
s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, mapManager, &s.fpsEnabled, &s.portalVisibility)
s.pauseMenu = menu.NewPauseMenu()
// Wire up settings references
s.titleScreen.SetFPSMonitor(&s.fpsEnabled)
s.titleScreen.SetFPSCap(&s.fpsCap)
s.titleScreen.SetPortalVisibility(&s.portalVisibility)
s.pauseMenu.SetFPSMonitor(&s.fpsEnabled)
s.pauseMenu.SetFPSCap(&s.fpsCap)
s.pauseMenu.SetPortalVisibility(&s.portalVisibility)
if saveManager != nil {
s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame())
}
ebiten.SetTPS(s.fpsCap.TPS())
// Create initial save file
if saveManager != nil {
settings := &save.Settings{
FPSMonitor: s.fpsEnabled,
FPSCap: s.fpCapToStringHelper(s.fpsCap),
PortalVisibility: s.portalVisibility,
}
saveManager.SaveSettings(settings)
}
return s
}
func (s *state) fpCapToStringHelper(cap FPSCap) string {
switch cap {
case FPSCap60:
return "60"
case FPSCap120:
return "120"
case FPSCapUncapped:
return "uncapped"
default:
return "60"
}
}
func (g *Game) Update() error {
prevFPSEnabled := g.state.fpsEnabled
prevFPSCap := g.state.fpsCap
prevPortalVisibility := g.state.portalVisibility
currentTPS := g.state.fpsCap.TPS()
if currentTPS < 0 {
ebiten.SetTPS(ebiten.SyncWithFPS)
} else {
ebiten.SetTPS(currentTPS)
}
// Update current screen
var err error
switch g.state.gameState {
case stateSplash:
err = g.updateSplash()
case stateTitle:
err = g.updateTitle()
case statePlaying:
err = g.updatePlaying()
case statePaused:
err = g.updatePaused()
}
if prevFPSEnabled != g.state.fpsEnabled || prevFPSCap != g.state.fpsCap || prevPortalVisibility != g.state.portalVisibility {
g.saveSettings()
}
return err
}
// Screen update handlers
func (g *Game) updateSplash() error {
if g.state.splashScreen.Update() {
g.state.gameState = stateTitle
}
return nil
}
func (g *Game) updateTitle() error {
if selectedOption := g.state.titleScreen.Update(); selectedOption != nil {
switch *selectedOption {
case screens.TitleOptionContinue:
if g.state.saveManager != nil {
if gameState, err := g.state.saveManager.LoadGameState(); err == nil && gameState != nil {
g.state.gameplayScreen.LoadState(gameState)
g.state.gameState = statePlaying
g.state.lastTick = time.Now()
}
}
case screens.TitleOptionNewGame:
g.state.gameState = statePlaying
g.state.gameplayScreen.Reset()
g.state.lastTick = time.Now()
if g.state.saveManager != nil {
g.state.saveManager.DeleteGameState()
}
case screens.TitleOptionSettings:
case screens.TitleOptionQuit:
return ebiten.Termination
}
}
return nil
}
func (g *Game) updatePlaying() error {
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
g.state.gameState = statePaused
g.state.pauseMenu.Reset()
return nil
}
now := time.Now()
delta := now.Sub(g.state.lastTick)
g.state.lastTick = now
maxDelta := 100 * time.Millisecond
if delta > maxDelta {
delta = maxDelta
}
minDelta := time.Microsecond
if delta < minDelta {
delta = minDelta
}
input := readControls()
g.state.gameplayScreen.Update(screens.GameplayInput{
Left: input.Left,
Right: input.Right,
Jump: input.Jump,
Sprint: input.Sprint,
Shoot: input.Shoot,
}, delta)
if now.Sub(g.state.lastAutoSave) >= g.state.autoSaveInterval {
g.saveGame()
g.state.lastAutoSave = now
}
return nil
}
func (g *Game) updatePaused() error {
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
g.state.gameState = statePlaying
g.state.lastTick = time.Now()
return nil
}
if selectedOption := g.state.pauseMenu.Update(); selectedOption != nil {
switch *selectedOption {
case menu.OptionResume:
g.state.gameState = statePlaying
g.state.lastTick = time.Now()
case menu.OptionSave:
g.saveGame()
g.state.gameplayScreen.ShowSaveNotification()
g.state.gameState = statePlaying
g.state.lastTick = time.Now()
case menu.OptionMainMenu:
g.saveGame()
g.state.gameState = stateTitle
if g.state.saveManager != nil {
g.state.titleScreen.SetHasSaveGame(g.state.saveManager.HasSavedGame())
}
g.state.titleScreen.Reset()
case menu.OptionQuit:
g.saveGame()
return ebiten.Termination
}
}
return nil
}
// Save/load operations
func (g *Game) saveGame() {
if g.state.saveManager == nil {
return
}
if g.state.gameState == statePlaying || g.state.gameState == statePaused {
gameState := g.state.gameplayScreen.SaveState()
g.state.saveManager.SaveGameState(gameState)
}
g.saveSettings()
}
func (g *Game) saveSettings() {
if g.state.saveManager == nil {
return
}
settings := &save.Settings{
FPSMonitor: g.state.fpsEnabled,
FPSCap: g.fpCapToString(g.state.fpsCap),
PortalVisibility: g.state.portalVisibility,
}
g.state.saveManager.SaveSettings(settings)
}
func (g *Game) fpCapToString(cap FPSCap) string {
switch cap {
case FPSCap60:
return "60"
case FPSCap120:
return "120"
case FPSCapUncapped:
return "uncapped"
default:
return "60"
}
}
// Rendering
func (g *Game) Draw(screen *ebiten.Image) {
switch g.state.gameState {
case stateSplash:
g.state.splashScreen.Draw(screen, ScreenWidth, ScreenHeight)
case stateTitle:
g.state.titleScreen.Draw(screen, ScreenWidth, ScreenHeight)
case statePlaying:
g.state.gameplayScreen.Draw(screen)
case statePaused:
g.state.gameplayScreen.Draw(screen)
g.state.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight)
}
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return ScreenWidth, ScreenHeight
}