diff --git a/internal/game/game.go b/internal/game/game.go index dc3ba14..3b41267 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -11,8 +11,6 @@ import ( "github.com/atridad/LilGuy/internal/ui/menu" ) -// Window and display configuration. - const ( ScreenWidth = 960 ScreenHeight = 540 @@ -20,29 +18,17 @@ const ( WindowTitle = "Lil Guy" ) -// Game states define the different screens and modes the game can be in. -// To add a new state: -// 1. Add a new constant to the gameState enum below -// 2. Create a new screen type in internal/screens/ (see splash.go, title.go, gameplay.go as examples) -// 3. Add the screen instance to the 'state' struct -// 4. Handle state transitions in Update() method -// 5. Handle rendering in Draw() method -// -// State Flow: -// stateSplash -> stateTitle -> statePlaying <-> statePaused -// ^____________| - +// Game states type gameState int const ( - stateSplash gameState = iota // Initial splash screen with game logo - stateTitle // Main menu (Play/Quit options) - statePlaying // Active gameplay - statePaused // Game paused (overlay menu) + stateSplash gameState = iota + stateTitle + statePlaying + statePaused ) -// FPS cap options for performance tuning. - +// FPS cap options type FPSCap int const ( @@ -82,8 +68,7 @@ func (f *FPSCap) Cycle() { *f = (*f + 1) % fpsCapCount } -// Input state for player controls. - +// Player input type controls struct { Left bool Right bool @@ -106,34 +91,28 @@ type Game struct { state *state } -// state holds all game state including screens, settings, and current mode. +// Main game state type state struct { - // Current state gameState gameState lastTick time.Time - // Screens - each screen manages its own UI and logic splashScreen *screens.SplashScreen titleScreen *screens.TitleScreen gameplayScreen *screens.GameplayScreen pauseMenu *menu.PauseMenu - // Settings fpsEnabled bool fpsCap FPSCap saveManager *save.Manager - // Auto-save lastAutoSave time.Time autoSaveInterval time.Duration } -// New creates a new game instance. func New() *Game { return &Game{state: newState()} } -// newState initializes a fresh game state. func newState() *state { now := time.Now() s := &state{ @@ -142,19 +121,15 @@ func newState() *state { fpsEnabled: false, fpsCap: FPSCap60, lastAutoSave: now, - autoSaveInterval: 30 * time.Second, // Auto-save every 30 seconds + autoSaveInterval: 30 * time.Second, } - // Initialize save manager saveManager, err := save.NewManager() if err != nil { - // If save manager fails, continue without it (settings won't persist) - // TODO: Show error to user saveManager = nil } s.saveManager = saveManager - // Load settings if available if saveManager != nil { if settings, err := saveManager.LoadSettings(); err == nil { s.fpsEnabled = settings.FPSMonitor @@ -171,27 +146,25 @@ func newState() *state { } } - // Initialize all screens + // Initialize screens s.splashScreen = screens.NewSplashScreen() s.titleScreen = screens.NewTitleScreen() s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, &s.fpsEnabled) s.pauseMenu = menu.NewPauseMenu() - // Configure settings references for title screen and pause menu + // Wire up settings references s.titleScreen.SetFPSMonitor(&s.fpsEnabled) s.titleScreen.SetFPSCap(&s.fpsCap) s.pauseMenu.SetFPSMonitor(&s.fpsEnabled) s.pauseMenu.SetFPSCap(&s.fpsCap) - // Check if saved game exists if saveManager != nil { s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame()) } - // Set initial TPS ebiten.SetTPS(s.fpsCap.TPS()) - // Save initial settings to create data.toml on first launch + // Create initial save file if saveManager != nil { settings := &save.Settings{ FPSMonitor: s.fpsEnabled, @@ -203,7 +176,6 @@ func newState() *state { return s } -// Helper function for converting FPSCap to string (used in initialization) func (s *state) fpCapToStringHelper(cap FPSCap) string { switch cap { case FPSCap60: @@ -217,14 +189,10 @@ func (s *state) fpCapToStringHelper(cap FPSCap) string { } } -// Update is called every frame and handles state transitions and input. - func (g *Game) Update() error { - // Track previous FPS settings to detect changes prevFPSEnabled := g.state.fpsEnabled prevFPSCap := g.state.fpsCap - // Update TPS if FPS cap changed currentTPS := g.state.fpsCap.TPS() if currentTPS < 0 { ebiten.SetTPS(ebiten.SyncWithFPS) @@ -232,7 +200,7 @@ func (g *Game) Update() error { ebiten.SetTPS(currentTPS) } - // Handle state-specific updates + // Update current screen var err error switch g.state.gameState { case stateSplash: @@ -245,7 +213,6 @@ func (g *Game) Update() error { err = g.updatePaused() } - // Auto-save settings if they changed if prevFPSEnabled != g.state.fpsEnabled || prevFPSCap != g.state.fpsCap { g.saveSettings() } @@ -253,7 +220,8 @@ func (g *Game) Update() error { return err } -// updateSplash handles the splash screen state. +// Screen update handlers + func (g *Game) updateSplash() error { if g.state.splashScreen.Update() { g.state.gameState = stateTitle @@ -261,12 +229,10 @@ func (g *Game) updateSplash() error { return nil } -// updateTitle handles the title screen state. func (g *Game) updateTitle() error { if selectedOption := g.state.titleScreen.Update(); selectedOption != nil { switch *selectedOption { case screens.TitleOptionContinue: - // Load saved game if g.state.saveManager != nil { if gameState, err := g.state.saveManager.LoadGameState(); err == nil && gameState != nil { g.state.gameplayScreen.LoadState(gameState) @@ -278,13 +244,10 @@ func (g *Game) updateTitle() error { g.state.gameState = statePlaying g.state.gameplayScreen.Reset() g.state.lastTick = time.Now() - // Delete old save if it exists if g.state.saveManager != nil { g.state.saveManager.DeleteGameState() } case screens.TitleOptionSettings: - // Settings are handled within the title screen itself - // No state change needed case screens.TitleOptionQuit: return ebiten.Termination } @@ -292,21 +255,17 @@ func (g *Game) updateTitle() error { return nil } -// updatePlaying handles the active gameplay state. func (g *Game) updatePlaying() error { - // Check for pause if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { g.state.gameState = statePaused g.state.pauseMenu.Reset() return nil } - // Calculate delta time now := time.Now() delta := now.Sub(g.state.lastTick) g.state.lastTick = now - // Update gameplay input := readControls() g.state.gameplayScreen.Update(screens.GameplayInput{ Left: input.Left, @@ -316,7 +275,6 @@ func (g *Game) updatePlaying() error { Shoot: input.Shoot, }, delta) - // Periodic auto-save if now.Sub(g.state.lastAutoSave) >= g.state.autoSaveInterval { g.saveGame() g.state.gameplayScreen.ShowSaveNotification() @@ -326,29 +284,24 @@ func (g *Game) updatePlaying() error { return nil } -// updatePaused handles the pause menu state. func (g *Game) updatePaused() error { - // Allow ESC to resume if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { g.state.gameState = statePlaying g.state.lastTick = time.Now() return nil } - // Handle pause menu selection 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: - // Save game immediately g.saveGame() g.state.gameplayScreen.ShowSaveNotification() g.state.gameState = statePlaying g.state.lastTick = time.Now() case menu.OptionMainMenu: - // Save game before returning to main menu g.saveGame() g.state.gameState = stateTitle if g.state.saveManager != nil { @@ -356,7 +309,6 @@ func (g *Game) updatePaused() error { } g.state.titleScreen.Reset() case menu.OptionQuit: - // Save game before quitting g.saveGame() return ebiten.Termination } @@ -365,23 +317,21 @@ func (g *Game) updatePaused() error { return nil } -// saveGame saves the current game state and settings. +// Save/load operations + func (g *Game) saveGame() { if g.state.saveManager == nil { return } - // Save game state if in playing mode if g.state.gameState == statePlaying || g.state.gameState == statePaused { gameState := g.state.gameplayScreen.SaveState() g.state.saveManager.SaveGameState(gameState) } - // Save settings g.saveSettings() } -// saveSettings saves only the settings. func (g *Game) saveSettings() { if g.state.saveManager == nil { return @@ -394,7 +344,6 @@ func (g *Game) saveSettings() { g.state.saveManager.SaveSettings(settings) } -// fpCapToString converts FPSCap to string for saving. func (g *Game) fpCapToString(cap FPSCap) string { switch cap { case FPSCap60: @@ -408,7 +357,7 @@ func (g *Game) fpCapToString(cap FPSCap) string { } } -// Draw renders the current game state to the screen. +// Rendering func (g *Game) Draw(screen *ebiten.Image) { switch g.state.gameState { @@ -419,13 +368,11 @@ func (g *Game) Draw(screen *ebiten.Image) { case statePlaying: g.state.gameplayScreen.Draw(screen) case statePaused: - // Draw gameplay in background, then overlay pause menu g.state.gameplayScreen.Draw(screen) g.state.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight) } } -// Layout returns the game's logical screen size. func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { return ScreenWidth, ScreenHeight } diff --git a/internal/hero/hero.go b/internal/hero/hero.go index 5bc6ede..c46e268 100644 --- a/internal/hero/hero.go +++ b/internal/hero/hero.go @@ -8,6 +8,7 @@ import ( "github.com/atridad/LilGuy/internal/projectile" ) +// Hero defaults const ( defaultRadius = 24.0 defaultSpeed = 200.0 @@ -36,6 +37,8 @@ const ( airFriction = 0.95 ) +// Input and bounds + type Input struct { Left bool Right bool @@ -64,6 +67,8 @@ const ( DirRight ) +// Hero state + type Hero struct { X float64 Y float64 @@ -155,6 +160,8 @@ func (h *Hero) Update(input Input, dt float64, bounds Bounds) { h.updateAnimation(dt) } +// Movement and physics + func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) { h.VelocityY += gravity * dt if h.VelocityY > maxFallSpeed { @@ -236,6 +243,8 @@ func (h *Hero) updateStamina(input Input, dt float64) { } } +// Animation + func (h *Hero) updateAnimation(dt float64) { isMoving := h.isMoving key := animationKey{direction: h.direction, state: animIdle} @@ -275,6 +284,8 @@ func (h *Hero) getVisualState() VisualState { return StateIdle } +// Rendering + func (h *Hero) Draw(screen *ebiten.Image) { sprite := h.getCurrentSprite() diff --git a/internal/hero/sprites.go b/internal/hero/sprites.go index 6d54af7..5538213 100644 --- a/internal/hero/sprites.go +++ b/internal/hero/sprites.go @@ -11,10 +11,12 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) +// Asset paths const ( heroDir = "assets/hero" ) +// Animation state type animState int const ( @@ -27,6 +29,7 @@ type animationKey struct { state animState } +// Sprite cache var ( knightAnimations map[animationKey][]*ebiten.Image ) @@ -47,6 +50,8 @@ func getKnightSprite(direction Direction, moving bool, frameIndex int) *ebiten.I return frameFromSet(direction, state, frameIndex) } +// Asset loading + func loadKnightAnimations() error { frames, err := loadAnimationFrames(heroDir) if err != nil { @@ -113,6 +118,8 @@ func loadImage(path string) (*ebiten.Image, error) { return ebiten.NewImageFromImage(img), nil } +// Image manipulation + func flipImageHorizontally(img *ebiten.Image) *ebiten.Image { bounds := img.Bounds() w, h := bounds.Dx(), bounds.Dy() diff --git a/internal/projectile/projectile.go b/internal/projectile/projectile.go index eeb5b29..e14d090 100644 --- a/internal/projectile/projectile.go +++ b/internal/projectile/projectile.go @@ -7,6 +7,8 @@ import ( "github.com/hajimehoshi/ebiten/v2/vector" ) +// Projectile configuration + type ProjectileConfig struct { Speed float64 Radius float64 @@ -58,6 +60,8 @@ func (p *Projectile) Draw(screen *ebiten.Image) { ) } +// Projectile manager + type Manager struct { Projectiles []*Projectile } diff --git a/internal/save/save.go b/internal/save/save.go index cdfee1c..7e455e9 100644 --- a/internal/save/save.go +++ b/internal/save/save.go @@ -9,47 +9,43 @@ import ( "github.com/BurntSushi/toml" ) -// File path for save data. const ( dataFile = "data.toml" ) -// Stores all persistent game data in a single file. +// Save file structure + type Data struct { Settings Settings `toml:"settings"` GameState GameState `toml:"game_state"` } -// Stores user preferences. type Settings struct { FPSMonitor bool `toml:"fps_monitor"` - FPSCap string `toml:"fps_cap"` // "60", "120", or "uncapped" + FPSCap string `toml:"fps_cap"` } -// Stores the current game progress. type GameState struct { HasSave bool `toml:"has_save"` SavedAt time.Time `toml:"saved_at"` HeroX float64 `toml:"hero_x"` HeroY float64 `toml:"hero_y"` HeroStamina float64 `toml:"hero_stamina"` - PlayTimeMS int64 `toml:"play_time_ms"` // Total play time in milliseconds + PlayTimeMS int64 `toml:"play_time_ms"` } -// Handles saving and loading of settings and game state. +// Save manager + type Manager struct { dataPath string } -// Creates a new save manager. func NewManager() (*Manager, error) { - // Get executable path exePath, err := os.Executable() if err != nil { return nil, fmt.Errorf("failed to get executable path: %w", err) } - // Get directory containing the executable exeDir := filepath.Dir(exePath) return &Manager{ @@ -57,11 +53,10 @@ func NewManager() (*Manager, error) { }, nil } -// Loads all data from disk. +// Data operations + func (m *Manager) LoadData() (*Data, error) { - // Check if file exists if _, err := os.Stat(m.dataPath); os.IsNotExist(err) { - // Return default data return &Data{ Settings: Settings{ FPSMonitor: false, @@ -81,7 +76,6 @@ func (m *Manager) LoadData() (*Data, error) { return &data, nil } -// Writes all data to disk. func (m *Manager) SaveData(data *Data) error { file, err := os.Create(m.dataPath) if err != nil { @@ -97,7 +91,8 @@ func (m *Manager) SaveData(data *Data) error { return nil } -// Loads user settings from disk. +// Settings operations + func (m *Manager) LoadSettings() (*Settings, error) { data, err := m.LoadData() if err != nil { @@ -106,11 +101,9 @@ func (m *Manager) LoadSettings() (*Settings, error) { return &data.Settings, nil } -// Writes user settings to disk. func (m *Manager) SaveSettings(settings *Settings) error { data, err := m.LoadData() if err != nil { - // If load fails, create new data with these settings data = &Data{ Settings: *settings, GameState: GameState{ @@ -124,7 +117,8 @@ func (m *Manager) SaveSettings(settings *Settings) error { return m.SaveData(data) } -// Loads game state from disk. +// Game state operations + func (m *Manager) LoadGameState() (*GameState, error) { data, err := m.LoadData() if err != nil { @@ -132,17 +126,15 @@ func (m *Manager) LoadGameState() (*GameState, error) { } if !data.GameState.HasSave { - return nil, nil // No save exists + return nil, nil } return &data.GameState, nil } -// Writes game state to disk. func (m *Manager) SaveGameState(state *GameState) error { data, err := m.LoadData() if err != nil { - // If load fails, create new data with this game state data = &Data{ Settings: Settings{ FPSMonitor: false, @@ -160,7 +152,6 @@ func (m *Manager) SaveGameState(state *GameState) error { return m.SaveData(data) } -// Checks if a saved game exists. func (m *Manager) HasSavedGame() bool { data, err := m.LoadData() if err != nil { @@ -169,7 +160,6 @@ func (m *Manager) HasSavedGame() bool { return data.GameState.HasSave } -// Removes the saved game. func (m *Manager) DeleteGameState() error { data, err := m.LoadData() if err != nil { @@ -183,7 +173,8 @@ func (m *Manager) DeleteGameState() error { return m.SaveData(data) } -// Returns the path to the save file. +// Utilities + func (m *Manager) GetSaveFilePath() string { return m.dataPath } diff --git a/internal/screens/gameplay.go b/internal/screens/gameplay.go index e6c24ef..5dd4423 100644 --- a/internal/screens/gameplay.go +++ b/internal/screens/gameplay.go @@ -17,6 +17,7 @@ import ( "github.com/atridad/LilGuy/internal/world" ) +// Screen and hero defaults var ( backgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255} saveNotificationColor = color.NRGBA{R: 50, G: 200, B: 50, A: 255} @@ -38,13 +39,12 @@ var ( heroColor = color.NRGBA{R: 210, G: 220, B: 255, A: 255} ) -// HUD settings. +// HUD positioning and colors const ( - hudX = 960 - 220 // ScreenWidth - 220 + hudX = 960 - 220 hudY = 20 ) -// HUD colors. var ( staminaNormalColor = color.NRGBA{R: 0, G: 255, B: 180, A: 255} staminaLowColor = color.NRGBA{R: 255, G: 60, B: 60, A: 255} @@ -53,6 +53,7 @@ var ( fpsPoorColor = color.NRGBA{R: 255, G: 120, B: 120, A: 255} ) +// FPS monitoring thresholds const ( staminaLowThreshold = 0.2 fpsWarnThreshold = 0.85 @@ -61,7 +62,6 @@ const ( targetTPS = 60 ) -// Player input for the gameplay screen. type GameplayInput struct { Left bool Right bool @@ -70,8 +70,6 @@ type GameplayInput struct { Shoot bool } -// Manages the main gameplay state including the hero, HUD, and game world. -// This is where the actual game logic and rendering happens during active play. type GameplayScreen struct { hero *hero.Hero hud hud.Overlay @@ -90,7 +88,6 @@ type GameplayScreen struct { showSaveNotification bool } -// Creates a new gameplay screen instance. func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *GameplayScreen { w := world.NewWorld() @@ -133,7 +130,6 @@ func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *Gamepla } } -// Processes gameplay logic with the given input and delta time. func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) { dt := delta.Seconds() @@ -166,7 +162,7 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) { } } -// Calculates the current FPS if FPS monitoring is enabled. +// FPS tracking func (g *GameplayScreen) trackFPS(delta time.Duration) { if g.fpsEnabled == nil || !*g.fpsEnabled { return @@ -182,7 +178,7 @@ func (g *GameplayScreen) trackFPS(delta time.Duration) { } } -// Renders the gameplay screen. +// Rendering func (g *GameplayScreen) Draw(screen *ebiten.Image) { screen.Fill(backgroundColor) @@ -205,7 +201,6 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) { meters := []status.Meter{staminaMeter} if g.fpsEnabled != nil && *g.fpsEnabled { - // Color based on target FPS (60). ratio := g.fpsValue / float64(targetTPS) fpsColor := fpsGoodColor switch { @@ -217,7 +212,7 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) { fpsMeter := status.Meter{ Label: fmt.Sprintf("Framerate: %3.0f FPS", g.fpsValue), - Base: -1, // Negative base means text-only display. + Base: -1, Level: 0, Color: fpsColor, } @@ -266,7 +261,7 @@ func (g *GameplayScreen) ShowSaveNotification() { g.saveNotificationTimer = saveNotificationDuration } -// Resets the gameplay screen to its initial state. +// State management func (g *GameplayScreen) Reset() { screenWidth := int(g.bounds.Width) screenHeight := int(g.bounds.Height) @@ -307,7 +302,6 @@ func (g *GameplayScreen) Reset() { g.fpsValue = 0 } -// SaveState converts the current gameplay state to a saveable format. func (g *GameplayScreen) SaveState() *save.GameState { return &save.GameState{ HeroX: g.hero.X, @@ -317,7 +311,6 @@ func (g *GameplayScreen) SaveState() *save.GameState { } } -// LoadState restores gameplay state from saved data. func (g *GameplayScreen) LoadState(state *save.GameState) { screenWidth := int(g.bounds.Width) screenHeight := int(g.bounds.Height) diff --git a/internal/screens/settings.go b/internal/screens/settings.go index cf1e5d3..56e8437 100644 --- a/internal/screens/settings.go +++ b/internal/screens/settings.go @@ -8,39 +8,32 @@ import ( "github.com/hajimehoshi/ebiten/v2/text/v2" ) -// An interface for managing FPS cap settings. +// Settings interface type FPSCapSetting interface { String() string Cycle() } -// Displays and manages game settings like FPS monitor and FPS cap. -// This screen can be accessed from both the title screen and pause menu. type SettingsScreen struct { selectedIndex int fpsMonitorValue *bool fpsCapValue FPSCapSetting } -// Creates a new settings screen instance. func NewSettingsScreen() *SettingsScreen { return &SettingsScreen{ selectedIndex: 0, } } -// Sets the FPS monitor toggle reference. func (s *SettingsScreen) SetFPSMonitor(enabled *bool) { s.fpsMonitorValue = enabled } -// Sets the FPS cap setting reference. func (s *SettingsScreen) SetFPSCap(cap FPSCapSetting) { s.fpsCapValue = cap } -// Processes settings screen input. -// Returns true if the user wants to go back. func (s *SettingsScreen) Update() bool { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { return true @@ -71,20 +64,19 @@ func (s *SettingsScreen) Update() bool { return false } -// Renders the settings screen. +// Rendering + func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int, title string) { - // Draw background screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255}) - // Draw title titleX := (screenWidth / 2) - (len(title) * 7 / 2) titleY := screenHeight/3 - 50 s.drawText(screen, title, color.White, titleX, titleY) - // Draw settings options startY := screenHeight/2 - 20 leftMargin := screenWidth/2 - 120 + // FPS monitor toggle fpsMonitorText := "FPS Monitor: " if s.fpsMonitorValue != nil && *s.fpsMonitorValue { fpsMonitorText += "ON" @@ -100,6 +92,7 @@ func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight in s.drawText(screen, fpsMonitorText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, startY) } + // FPS cap setting fpsCapText := "FPS Cap: " if s.fpsCapValue != nil { fpsCapText += s.fpsCapValue.String() @@ -116,7 +109,7 @@ func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight in s.drawText(screen, fpsCapText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, capY) } - // Draw hint text + // Instructions hintText := "Enter/Space to toggle, ESC to go back" hintX := (screenWidth / 2) - (len(hintText) * 7 / 2) hintY := screenHeight - 50 @@ -130,7 +123,6 @@ func (s *SettingsScreen) drawText(screen *ebiten.Image, txt string, clr color.Co text.Draw(screen, txt, basicFace, op) } -// Resets the settings screen to its initial state. func (s *SettingsScreen) Reset() { s.selectedIndex = 0 } diff --git a/internal/screens/splash.go b/internal/screens/splash.go index 60ce95e..ed61644 100644 --- a/internal/screens/splash.go +++ b/internal/screens/splash.go @@ -15,14 +15,14 @@ var ( basicFaceAscent = basicFace.Metrics().HAscent ) +// Timing + const ( splashDuration = 2 * time.Second fadeInDuration = 500 * time.Millisecond fadeOutDuration = 500 * time.Millisecond ) -// Displays the game title with fade in/out effects. -// This is typically the first screen shown when the game starts. type SplashScreen struct { startTime time.Time fadeInEnd time.Time @@ -30,7 +30,6 @@ type SplashScreen struct { endTime time.Time } -// Creates a new splash screen instance. func NewSplashScreen() *SplashScreen { now := time.Now() return &SplashScreen{ @@ -41,15 +40,11 @@ func NewSplashScreen() *SplashScreen { } } -// Processes splash screen logic. -// Returns true when the splash screen should end and transition to the next screen. func (s *SplashScreen) Update() bool { - // Return true if splash is complete if time.Now().After(s.endTime) { return true } - // Allow skipping with any key if inpututil.IsKeyJustPressed(ebiten.KeySpace) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeyEscape) { @@ -59,14 +54,14 @@ func (s *SplashScreen) Update() bool { return false } -// Renders the splash screen. +// Rendering + func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) { screen.Fill(color.RGBA{R: 0, G: 0, B: 0, A: 255}) now := time.Now() alpha := 1.0 - // Calculate fade in/out if now.Before(s.fadeInEnd) { elapsed := now.Sub(s.startTime) alpha = float64(elapsed) / float64(fadeInDuration) @@ -81,10 +76,9 @@ func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) alpha = 1 } - // Draw large game title + // Draw title titleText := "LIL GUY" - // Calculate size for large text (scale up the basic font) scale := 4.0 charWidth := 7.0 * scale textWidth := float64(len(titleText)) * charWidth diff --git a/internal/screens/title.go b/internal/screens/title.go index 419ff13..81d07a6 100644 --- a/internal/screens/title.go +++ b/internal/screens/title.go @@ -9,7 +9,7 @@ import ( "github.com/hajimehoshi/ebiten/v2/vector" ) -// Represents the options available on the title screen. +// Menu options type TitleMenuOption int const ( @@ -20,6 +20,7 @@ const ( titleOptionCount ) +// Screen modes type titleScreenMode int const ( @@ -27,8 +28,6 @@ const ( titleModeSettings ) -// Displays the main menu with Continue, New Game, Settings, and Quit options. -// This is shown after the splash screen and when returning from the pause menu. type TitleScreen struct { selectedIndex int currentMode titleScreenMode @@ -36,7 +35,6 @@ type TitleScreen struct { hasSaveGame bool } -// Creates a new title screen instance. func NewTitleScreen() *TitleScreen { return &TitleScreen{ selectedIndex: 0, @@ -46,28 +44,22 @@ func NewTitleScreen() *TitleScreen { } } -// Sets the FPS monitor toggle reference for settings. func (t *TitleScreen) SetFPSMonitor(enabled *bool) { t.settingsScreen.SetFPSMonitor(enabled) } -// Sets the FPS cap setting reference for settings. func (t *TitleScreen) SetFPSCap(cap FPSCapSetting) { t.settingsScreen.SetFPSCap(cap) } -// Sets whether a saved game exists. func (t *TitleScreen) SetHasSaveGame(hasSave bool) { t.hasSaveGame = hasSave - // If no save game, skip Continue option if !hasSave && t.selectedIndex == 0 { - t.selectedIndex = 1 // Move to New Game + t.selectedIndex = 1 } } -// Processes title screen input and returns the selected option if any. func (t *TitleScreen) Update() *TitleMenuOption { - // Handle settings screen if t.currentMode == titleModeSettings { if t.settingsScreen.Update() { t.currentMode = titleModeMain @@ -76,13 +68,11 @@ func (t *TitleScreen) Update() *TitleMenuOption { return nil } - // Handle main menu if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { t.selectedIndex-- if t.selectedIndex < 0 { t.selectedIndex = int(titleOptionCount) - 1 } - // Skip Continue if no save exists if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame { t.selectedIndex-- if t.selectedIndex < 0 { @@ -96,7 +86,6 @@ func (t *TitleScreen) Update() *TitleMenuOption { if t.selectedIndex >= int(titleOptionCount) { t.selectedIndex = 0 } - // Skip Continue if no save exists if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame { t.selectedIndex++ if t.selectedIndex >= int(titleOptionCount) { @@ -118,18 +107,15 @@ func (t *TitleScreen) Update() *TitleMenuOption { return nil } -// Renders the title screen. +// Rendering func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) { - // If in settings mode, draw settings screen if t.currentMode == titleModeSettings { t.settingsScreen.Draw(screen, screenWidth, screenHeight, "SETTINGS") return } - // Draw main menu screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255}) - // Draw game title titleText := "LIL GUY" scale := 3.0 charWidth := 7.0 * scale @@ -144,12 +130,11 @@ func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) op.ColorScale.ScaleWithColor(color.RGBA{R: 210, G: 220, B: 255, A: 255}) text.Draw(screen, titleText, basicFace, op) - // Draw menu options + // Draw menu options := []string{"Continue", "New Game", "Settings", "Quit"} startY := screenHeight/2 + 10 for i, option := range options { - // Skip Continue option if no save exists if i == int(TitleOptionContinue) && !t.hasSaveGame { continue } @@ -163,12 +148,10 @@ func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) } if i == t.selectedIndex { - // Draw selection indicator indicatorX := optionX - 20 t.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, optionY) t.drawText(screen, option, color.RGBA{R: 255, G: 255, B: 100, A: 255}, optionX, optionY) - // Draw selection box boxPadding := float32(10.0) boxWidth := float32(len(option)*7) + boxPadding*2 boxHeight := float32(20) @@ -182,7 +165,7 @@ func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) } } - // Draw hint text + // Instructions hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select" hintX := (screenWidth / 2) - (len(hintText) * 7 / 2) hintY := screenHeight - 50 @@ -196,9 +179,7 @@ func (t *TitleScreen) drawText(screen *ebiten.Image, txt string, clr color.Color text.Draw(screen, txt, basicFace, op) } -// Resets the title screen to its initial state. func (t *TitleScreen) Reset() { - // Start at Continue if available, otherwise New Game if t.hasSaveGame { t.selectedIndex = 0 } else { diff --git a/internal/status/status.go b/internal/status/status.go index f16a49f..71d0669 100644 --- a/internal/status/status.go +++ b/internal/status/status.go @@ -5,7 +5,8 @@ import ( "image/color" ) -// HUD resource entry. +// Meter types + type Meter struct { Label string Base float64 @@ -13,19 +14,18 @@ type Meter struct { Color color.NRGBA } -// Meter template values. type Config struct { Label string Base float64 Color color.NRGBA } -// Collection of meters. +// Meter manager + type Manager struct { meters []Meter } -// Builds meters from configs. func NewManager(cfgs []Config) *Manager { meters := make([]Meter, len(cfgs)) for i, cfg := range cfgs { @@ -40,20 +40,20 @@ func NewManager(cfgs []Config) *Manager { return &Manager{meters: meters} } -// Resets levels to base. func (m *Manager) Update() { for i := range m.meters { m.meters[i].Level = m.meters[i].Base } } -// Meters exposes a copy of the internal slice to prevent mutation. func (m *Manager) Meters() []Meter { out := make([]Meter, len(m.meters)) copy(out, m.meters) return out } +// Helper functions + func clamp(value, min, max float64) float64 { if value < min { return min diff --git a/internal/ui/hud/elements.go b/internal/ui/hud/elements.go index 11512e3..5c74b79 100644 --- a/internal/ui/hud/elements.go +++ b/internal/ui/hud/elements.go @@ -17,6 +17,8 @@ var ( rectPixel = newRectPixel() ) +// Drawing helpers + func newRectPixel() *ebiten.Image { img := ebiten.NewImage(1, 1) img.Fill(color.White) @@ -33,12 +35,12 @@ func drawHUDText(screen *ebiten.Image, txt string, clr color.Color, x, y int) { text.Draw(screen, txt, basicFace, op) } -// Drawable HUD chunk. +// HUD elements + type Element interface { Draw(screen *ebiten.Image, x, y int) (width, height int) } -// Plain text node. type Label struct { Text string Color color.Color @@ -53,7 +55,6 @@ func (l Label) Draw(screen *ebiten.Image, x, y int) (int, int) { return width, 13 } -// Percent readout node. type PercentageDisplay struct { Meter status.Meter Color color.Color @@ -68,7 +69,6 @@ func (p PercentageDisplay) Draw(screen *ebiten.Image, x, y int) (int, int) { return len(txt) * 7, 13 } -// Combined label and percent. type MeterLabel struct { Meter status.Meter Color color.Color @@ -80,17 +80,14 @@ func (m MeterLabel) Draw(screen *ebiten.Image, x, y int) (int, int) { } var txt string if m.Meter.Base < 0 { - // Text-only display without percentage. txt = m.Meter.Label } else { - // Standard meter with percentage. txt = fmt.Sprintf("%s: %3.0f%%", m.Meter.Label, m.Meter.Level) } drawHUDText(screen, txt, m.Meter.Color, x, y) return len(txt) * 7, 13 } -// Horizontal meter bar. type Bar struct { Meter status.Meter MaxWidth int @@ -117,23 +114,17 @@ func (b Bar) Draw(screen *ebiten.Image, x, y int) (int, int) { fillWidth = maxWidth } - // Draw border if requested if b.ShowBorder { borderColor := b.BorderColor if borderColor == nil { borderColor = color.RGBA{R: 80, G: 80, B: 80, A: 255} } - // Top border drawRect(screen, x, y, maxWidth, 1, borderColor) - // Bottom border drawRect(screen, x, y+height-1, maxWidth, 1, borderColor) - // Left border drawRect(screen, x, y, 1, height, borderColor) - // Right border drawRect(screen, x+maxWidth-1, y, 1, height, borderColor) } - // Draw filled portion if fillWidth > 0 { drawRect(screen, x, y, fillWidth, height, b.Meter.Color) } @@ -141,7 +132,8 @@ func (b Bar) Draw(screen *ebiten.Image, x, y int) (int, int) { return maxWidth, height } -// Helper for filled rectangles. +// Rectangle drawing + func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) { if width <= 0 || height <= 0 { return @@ -158,7 +150,8 @@ func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) { screen.DrawImage(rectPixel, op) } -// Empty padding block. +// Layout elements + type Spacer struct { Width int Height int @@ -168,7 +161,6 @@ func (s Spacer) Draw(screen *ebiten.Image, x, y int) (int, int) { return s.Width, s.Height } -// Horizontal layout row. type Row struct { Elements []Element Spacing int @@ -195,7 +187,6 @@ func (r Row) Draw(screen *ebiten.Image, x, y int) (int, int) { return totalWidth, maxHeight } -// Vertical stack layout. type Column struct { Elements []Element Spacing int diff --git a/internal/ui/hud/hud.go b/internal/ui/hud/hud.go index 5f2d8d4..a25a069 100644 --- a/internal/ui/hud/hud.go +++ b/internal/ui/hud/hud.go @@ -8,20 +8,17 @@ import ( "github.com/atridad/LilGuy/internal/status" ) -// HUD overlay anchor. type Overlay struct { X int Y int Color color.Color } -// Paints the HUD overlay. func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) { if o.Color == nil { o.Color = color.White } - // Instruction text instructions := Column{ Elements: []Element{ Label{Text: "Lil Guy", Color: o.Color}, @@ -31,16 +28,13 @@ func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) { } instructions.Draw(screen, 16, 16) - // Meter column meterElements := make([]Element, 0, len(meters)) for _, meter := range meters { if meter.Base < 0 { - // Text-only display (no bar). meterElements = append(meterElements, MeterLabel{Meter: meter, Color: o.Color}, ) } else { - // Full meter with bar. meterElements = append(meterElements, Column{ Elements: []Element{ MeterLabel{Meter: meter, Color: o.Color}, diff --git a/internal/ui/menu/menu.go b/internal/ui/menu/menu.go index caf1bde..31e591a 100644 --- a/internal/ui/menu/menu.go +++ b/internal/ui/menu/menu.go @@ -22,6 +22,8 @@ var ( } ) +// Menu options + func getOverlayImage(width, height int) *ebiten.Image { if width <= 0 || height <= 0 { return nil @@ -46,6 +48,8 @@ const ( optionCount ) +// Screen modes + type menuScreen int const ( @@ -80,7 +84,8 @@ func (m *PauseMenu) SetFPSCap(cap FPSCapSetting) { m.settingsScreen.SetFPSCap(cap) } -// Returns the selected option if one was chosen, nil otherwise +// Update logic + func (m *PauseMenu) Update() *MenuOption { if m.currentScreen == screenSettings { return m.updateSettings() @@ -123,6 +128,8 @@ func (m *PauseMenu) updateSettings() *MenuOption { return nil } +// Rendering + func (m *PauseMenu) Draw(screen *ebiten.Image, screenWidth, screenHeight int) { if overlay := getOverlayImage(screenWidth, screenHeight); overlay != nil { screen.DrawImage(overlay, nil) @@ -177,6 +184,7 @@ func (m *PauseMenu) drawMain(screen *ebiten.Image, menuX, menuY, menuWidth, menu } } + // Instructions hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select" hintX := menuX + (menuWidth / 2) - (len(hintText) * 7 / 2) hintY := menuY + menuHeight - 30 @@ -184,7 +192,6 @@ func (m *PauseMenu) drawMain(screen *ebiten.Image, menuX, menuY, menuWidth, menu } func (m *PauseMenu) drawSettings(screen *ebiten.Image, menuX, menuY, menuWidth, menuHeight int) { - // Draw menu background and border vector.DrawFilledRect(screen, float32(menuX), float32(menuY), float32(menuWidth), float32(menuHeight), @@ -200,16 +207,12 @@ func (m *PauseMenu) drawSettings(screen *ebiten.Image, menuX, menuY, menuWidth, false, ) - // Create a sub-image for the settings screen to draw within the menu bounds - // We'll draw to the full screen and the settings screen will handle positioning screenWidth := menuWidth screenHeight := menuHeight - // Temporarily adjust the drawing to center within the menu subScreen := ebiten.NewImage(screenWidth, screenHeight) m.settingsScreen.Draw(subScreen, screenWidth, screenHeight, "SETTINGS") - // Draw the settings content in the menu area op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(menuX), float64(menuY)) screen.DrawImage(subScreen, op) diff --git a/internal/world/surface.go b/internal/world/surface.go index 46a2124..15f71ec 100644 --- a/internal/world/surface.go +++ b/internal/world/surface.go @@ -7,6 +7,8 @@ import ( "github.com/hajimehoshi/ebiten/v2/vector" ) +// Surface types + type SurfaceTag int const ( @@ -67,6 +69,8 @@ func (s *Surface) Draw(screen *ebiten.Image) { ) } +// World container + type World struct { Surfaces []*Surface }