Better commenting throughout

This commit is contained in:
2025-11-24 12:29:19 -07:00
parent 175479da69
commit 5e0413a259
14 changed files with 107 additions and 195 deletions

View File

@@ -11,8 +11,6 @@ import (
"github.com/atridad/LilGuy/internal/ui/menu" "github.com/atridad/LilGuy/internal/ui/menu"
) )
// Window and display configuration.
const ( const (
ScreenWidth = 960 ScreenWidth = 960
ScreenHeight = 540 ScreenHeight = 540
@@ -20,29 +18,17 @@ const (
WindowTitle = "Lil Guy" WindowTitle = "Lil Guy"
) )
// Game states define the different screens and modes the game can be in. // Game states
// 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
// ^____________|
type gameState int type gameState int
const ( const (
stateSplash gameState = iota // Initial splash screen with game logo stateSplash gameState = iota
stateTitle // Main menu (Play/Quit options) stateTitle
statePlaying // Active gameplay statePlaying
statePaused // Game paused (overlay menu) statePaused
) )
// FPS cap options for performance tuning. // FPS cap options
type FPSCap int type FPSCap int
const ( const (
@@ -82,8 +68,7 @@ func (f *FPSCap) Cycle() {
*f = (*f + 1) % fpsCapCount *f = (*f + 1) % fpsCapCount
} }
// Input state for player controls. // Player input
type controls struct { type controls struct {
Left bool Left bool
Right bool Right bool
@@ -106,34 +91,28 @@ type Game struct {
state *state state *state
} }
// state holds all game state including screens, settings, and current mode. // Main game state
type state struct { type state struct {
// Current state
gameState gameState gameState gameState
lastTick time.Time lastTick time.Time
// Screens - each screen manages its own UI and logic
splashScreen *screens.SplashScreen splashScreen *screens.SplashScreen
titleScreen *screens.TitleScreen titleScreen *screens.TitleScreen
gameplayScreen *screens.GameplayScreen gameplayScreen *screens.GameplayScreen
pauseMenu *menu.PauseMenu pauseMenu *menu.PauseMenu
// Settings
fpsEnabled bool fpsEnabled bool
fpsCap FPSCap fpsCap FPSCap
saveManager *save.Manager saveManager *save.Manager
// Auto-save
lastAutoSave time.Time lastAutoSave time.Time
autoSaveInterval time.Duration autoSaveInterval time.Duration
} }
// New creates a new game instance.
func New() *Game { func New() *Game {
return &Game{state: newState()} return &Game{state: newState()}
} }
// newState initializes a fresh game state.
func newState() *state { func newState() *state {
now := time.Now() now := time.Now()
s := &state{ s := &state{
@@ -142,19 +121,15 @@ func newState() *state {
fpsEnabled: false, fpsEnabled: false,
fpsCap: FPSCap60, fpsCap: FPSCap60,
lastAutoSave: now, lastAutoSave: now,
autoSaveInterval: 30 * time.Second, // Auto-save every 30 seconds autoSaveInterval: 30 * time.Second,
} }
// Initialize save manager
saveManager, err := save.NewManager() saveManager, err := save.NewManager()
if err != nil { if err != nil {
// If save manager fails, continue without it (settings won't persist)
// TODO: Show error to user
saveManager = nil saveManager = nil
} }
s.saveManager = saveManager s.saveManager = saveManager
// Load settings if available
if saveManager != nil { if saveManager != nil {
if settings, err := saveManager.LoadSettings(); err == nil { if settings, err := saveManager.LoadSettings(); err == nil {
s.fpsEnabled = settings.FPSMonitor s.fpsEnabled = settings.FPSMonitor
@@ -171,27 +146,25 @@ func newState() *state {
} }
} }
// Initialize all screens // Initialize screens
s.splashScreen = screens.NewSplashScreen() s.splashScreen = screens.NewSplashScreen()
s.titleScreen = screens.NewTitleScreen() s.titleScreen = screens.NewTitleScreen()
s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, &s.fpsEnabled) s.gameplayScreen = screens.NewGameplayScreen(ScreenWidth, ScreenHeight, &s.fpsEnabled)
s.pauseMenu = menu.NewPauseMenu() s.pauseMenu = menu.NewPauseMenu()
// Configure settings references for title screen and pause menu // Wire up settings references
s.titleScreen.SetFPSMonitor(&s.fpsEnabled) s.titleScreen.SetFPSMonitor(&s.fpsEnabled)
s.titleScreen.SetFPSCap(&s.fpsCap) s.titleScreen.SetFPSCap(&s.fpsCap)
s.pauseMenu.SetFPSMonitor(&s.fpsEnabled) s.pauseMenu.SetFPSMonitor(&s.fpsEnabled)
s.pauseMenu.SetFPSCap(&s.fpsCap) s.pauseMenu.SetFPSCap(&s.fpsCap)
// Check if saved game exists
if saveManager != nil { if saveManager != nil {
s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame()) s.titleScreen.SetHasSaveGame(saveManager.HasSavedGame())
} }
// Set initial TPS
ebiten.SetTPS(s.fpsCap.TPS()) ebiten.SetTPS(s.fpsCap.TPS())
// Save initial settings to create data.toml on first launch // Create initial save file
if saveManager != nil { if saveManager != nil {
settings := &save.Settings{ settings := &save.Settings{
FPSMonitor: s.fpsEnabled, FPSMonitor: s.fpsEnabled,
@@ -203,7 +176,6 @@ func newState() *state {
return s return s
} }
// Helper function for converting FPSCap to string (used in initialization)
func (s *state) fpCapToStringHelper(cap FPSCap) string { func (s *state) fpCapToStringHelper(cap FPSCap) string {
switch cap { switch cap {
case FPSCap60: 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 { func (g *Game) Update() error {
// Track previous FPS settings to detect changes
prevFPSEnabled := g.state.fpsEnabled prevFPSEnabled := g.state.fpsEnabled
prevFPSCap := g.state.fpsCap prevFPSCap := g.state.fpsCap
// Update TPS if FPS cap changed
currentTPS := g.state.fpsCap.TPS() currentTPS := g.state.fpsCap.TPS()
if currentTPS < 0 { if currentTPS < 0 {
ebiten.SetTPS(ebiten.SyncWithFPS) ebiten.SetTPS(ebiten.SyncWithFPS)
@@ -232,7 +200,7 @@ func (g *Game) Update() error {
ebiten.SetTPS(currentTPS) ebiten.SetTPS(currentTPS)
} }
// Handle state-specific updates // Update current screen
var err error var err error
switch g.state.gameState { switch g.state.gameState {
case stateSplash: case stateSplash:
@@ -245,7 +213,6 @@ func (g *Game) Update() error {
err = g.updatePaused() err = g.updatePaused()
} }
// Auto-save settings if they changed
if prevFPSEnabled != g.state.fpsEnabled || prevFPSCap != g.state.fpsCap { if prevFPSEnabled != g.state.fpsEnabled || prevFPSCap != g.state.fpsCap {
g.saveSettings() g.saveSettings()
} }
@@ -253,7 +220,8 @@ func (g *Game) Update() error {
return err return err
} }
// updateSplash handles the splash screen state. // Screen update handlers
func (g *Game) updateSplash() error { func (g *Game) updateSplash() error {
if g.state.splashScreen.Update() { if g.state.splashScreen.Update() {
g.state.gameState = stateTitle g.state.gameState = stateTitle
@@ -261,12 +229,10 @@ func (g *Game) updateSplash() error {
return nil return nil
} }
// updateTitle handles the title screen state.
func (g *Game) updateTitle() error { func (g *Game) updateTitle() error {
if selectedOption := g.state.titleScreen.Update(); selectedOption != nil { if selectedOption := g.state.titleScreen.Update(); selectedOption != nil {
switch *selectedOption { switch *selectedOption {
case screens.TitleOptionContinue: case screens.TitleOptionContinue:
// Load saved game
if g.state.saveManager != nil { if g.state.saveManager != nil {
if gameState, err := g.state.saveManager.LoadGameState(); err == nil && gameState != nil { if gameState, err := g.state.saveManager.LoadGameState(); err == nil && gameState != nil {
g.state.gameplayScreen.LoadState(gameState) g.state.gameplayScreen.LoadState(gameState)
@@ -278,13 +244,10 @@ func (g *Game) updateTitle() error {
g.state.gameState = statePlaying g.state.gameState = statePlaying
g.state.gameplayScreen.Reset() g.state.gameplayScreen.Reset()
g.state.lastTick = time.Now() g.state.lastTick = time.Now()
// Delete old save if it exists
if g.state.saveManager != nil { if g.state.saveManager != nil {
g.state.saveManager.DeleteGameState() g.state.saveManager.DeleteGameState()
} }
case screens.TitleOptionSettings: case screens.TitleOptionSettings:
// Settings are handled within the title screen itself
// No state change needed
case screens.TitleOptionQuit: case screens.TitleOptionQuit:
return ebiten.Termination return ebiten.Termination
} }
@@ -292,21 +255,17 @@ func (g *Game) updateTitle() error {
return nil return nil
} }
// updatePlaying handles the active gameplay state.
func (g *Game) updatePlaying() error { func (g *Game) updatePlaying() error {
// Check for pause
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
g.state.gameState = statePaused g.state.gameState = statePaused
g.state.pauseMenu.Reset() g.state.pauseMenu.Reset()
return nil return nil
} }
// Calculate delta time
now := time.Now() now := time.Now()
delta := now.Sub(g.state.lastTick) delta := now.Sub(g.state.lastTick)
g.state.lastTick = now g.state.lastTick = now
// Update gameplay
input := readControls() input := readControls()
g.state.gameplayScreen.Update(screens.GameplayInput{ g.state.gameplayScreen.Update(screens.GameplayInput{
Left: input.Left, Left: input.Left,
@@ -316,7 +275,6 @@ func (g *Game) updatePlaying() error {
Shoot: input.Shoot, Shoot: input.Shoot,
}, delta) }, delta)
// Periodic auto-save
if now.Sub(g.state.lastAutoSave) >= g.state.autoSaveInterval { if now.Sub(g.state.lastAutoSave) >= g.state.autoSaveInterval {
g.saveGame() g.saveGame()
g.state.gameplayScreen.ShowSaveNotification() g.state.gameplayScreen.ShowSaveNotification()
@@ -326,29 +284,24 @@ func (g *Game) updatePlaying() error {
return nil return nil
} }
// updatePaused handles the pause menu state.
func (g *Game) updatePaused() error { func (g *Game) updatePaused() error {
// Allow ESC to resume
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
g.state.gameState = statePlaying g.state.gameState = statePlaying
g.state.lastTick = time.Now() g.state.lastTick = time.Now()
return nil return nil
} }
// Handle pause menu selection
if selectedOption := g.state.pauseMenu.Update(); selectedOption != nil { if selectedOption := g.state.pauseMenu.Update(); selectedOption != nil {
switch *selectedOption { switch *selectedOption {
case menu.OptionResume: case menu.OptionResume:
g.state.gameState = statePlaying g.state.gameState = statePlaying
g.state.lastTick = time.Now() g.state.lastTick = time.Now()
case menu.OptionSave: case menu.OptionSave:
// Save game immediately
g.saveGame() g.saveGame()
g.state.gameplayScreen.ShowSaveNotification() g.state.gameplayScreen.ShowSaveNotification()
g.state.gameState = statePlaying g.state.gameState = statePlaying
g.state.lastTick = time.Now() g.state.lastTick = time.Now()
case menu.OptionMainMenu: case menu.OptionMainMenu:
// Save game before returning to main menu
g.saveGame() g.saveGame()
g.state.gameState = stateTitle g.state.gameState = stateTitle
if g.state.saveManager != nil { if g.state.saveManager != nil {
@@ -356,7 +309,6 @@ func (g *Game) updatePaused() error {
} }
g.state.titleScreen.Reset() g.state.titleScreen.Reset()
case menu.OptionQuit: case menu.OptionQuit:
// Save game before quitting
g.saveGame() g.saveGame()
return ebiten.Termination return ebiten.Termination
} }
@@ -365,23 +317,21 @@ func (g *Game) updatePaused() error {
return nil return nil
} }
// saveGame saves the current game state and settings. // Save/load operations
func (g *Game) saveGame() { func (g *Game) saveGame() {
if g.state.saveManager == nil { if g.state.saveManager == nil {
return return
} }
// Save game state if in playing mode
if g.state.gameState == statePlaying || g.state.gameState == statePaused { if g.state.gameState == statePlaying || g.state.gameState == statePaused {
gameState := g.state.gameplayScreen.SaveState() gameState := g.state.gameplayScreen.SaveState()
g.state.saveManager.SaveGameState(gameState) g.state.saveManager.SaveGameState(gameState)
} }
// Save settings
g.saveSettings() g.saveSettings()
} }
// saveSettings saves only the settings.
func (g *Game) saveSettings() { func (g *Game) saveSettings() {
if g.state.saveManager == nil { if g.state.saveManager == nil {
return return
@@ -394,7 +344,6 @@ func (g *Game) saveSettings() {
g.state.saveManager.SaveSettings(settings) g.state.saveManager.SaveSettings(settings)
} }
// fpCapToString converts FPSCap to string for saving.
func (g *Game) fpCapToString(cap FPSCap) string { func (g *Game) fpCapToString(cap FPSCap) string {
switch cap { switch cap {
case FPSCap60: 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) { func (g *Game) Draw(screen *ebiten.Image) {
switch g.state.gameState { switch g.state.gameState {
@@ -419,13 +368,11 @@ func (g *Game) Draw(screen *ebiten.Image) {
case statePlaying: case statePlaying:
g.state.gameplayScreen.Draw(screen) g.state.gameplayScreen.Draw(screen)
case statePaused: case statePaused:
// Draw gameplay in background, then overlay pause menu
g.state.gameplayScreen.Draw(screen) g.state.gameplayScreen.Draw(screen)
g.state.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight) g.state.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight)
} }
} }
// Layout returns the game's logical screen size.
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return ScreenWidth, ScreenHeight return ScreenWidth, ScreenHeight
} }

View File

@@ -8,6 +8,7 @@ import (
"github.com/atridad/LilGuy/internal/projectile" "github.com/atridad/LilGuy/internal/projectile"
) )
// Hero defaults
const ( const (
defaultRadius = 24.0 defaultRadius = 24.0
defaultSpeed = 200.0 defaultSpeed = 200.0
@@ -36,6 +37,8 @@ const (
airFriction = 0.95 airFriction = 0.95
) )
// Input and bounds
type Input struct { type Input struct {
Left bool Left bool
Right bool Right bool
@@ -64,6 +67,8 @@ const (
DirRight DirRight
) )
// Hero state
type Hero struct { type Hero struct {
X float64 X float64
Y float64 Y float64
@@ -155,6 +160,8 @@ func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
h.updateAnimation(dt) h.updateAnimation(dt)
} }
// Movement and physics
func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) { func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
h.VelocityY += gravity * dt h.VelocityY += gravity * dt
if h.VelocityY > maxFallSpeed { if h.VelocityY > maxFallSpeed {
@@ -236,6 +243,8 @@ func (h *Hero) updateStamina(input Input, dt float64) {
} }
} }
// Animation
func (h *Hero) updateAnimation(dt float64) { func (h *Hero) updateAnimation(dt float64) {
isMoving := h.isMoving isMoving := h.isMoving
key := animationKey{direction: h.direction, state: animIdle} key := animationKey{direction: h.direction, state: animIdle}
@@ -275,6 +284,8 @@ func (h *Hero) getVisualState() VisualState {
return StateIdle return StateIdle
} }
// Rendering
func (h *Hero) Draw(screen *ebiten.Image) { func (h *Hero) Draw(screen *ebiten.Image) {
sprite := h.getCurrentSprite() sprite := h.getCurrentSprite()

View File

@@ -11,10 +11,12 @@ import (
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
// Asset paths
const ( const (
heroDir = "assets/hero" heroDir = "assets/hero"
) )
// Animation state
type animState int type animState int
const ( const (
@@ -27,6 +29,7 @@ type animationKey struct {
state animState state animState
} }
// Sprite cache
var ( var (
knightAnimations map[animationKey][]*ebiten.Image knightAnimations map[animationKey][]*ebiten.Image
) )
@@ -47,6 +50,8 @@ func getKnightSprite(direction Direction, moving bool, frameIndex int) *ebiten.I
return frameFromSet(direction, state, frameIndex) return frameFromSet(direction, state, frameIndex)
} }
// Asset loading
func loadKnightAnimations() error { func loadKnightAnimations() error {
frames, err := loadAnimationFrames(heroDir) frames, err := loadAnimationFrames(heroDir)
if err != nil { if err != nil {
@@ -113,6 +118,8 @@ func loadImage(path string) (*ebiten.Image, error) {
return ebiten.NewImageFromImage(img), nil return ebiten.NewImageFromImage(img), nil
} }
// Image manipulation
func flipImageHorizontally(img *ebiten.Image) *ebiten.Image { func flipImageHorizontally(img *ebiten.Image) *ebiten.Image {
bounds := img.Bounds() bounds := img.Bounds()
w, h := bounds.Dx(), bounds.Dy() w, h := bounds.Dx(), bounds.Dy()

View File

@@ -7,6 +7,8 @@ import (
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
) )
// Projectile configuration
type ProjectileConfig struct { type ProjectileConfig struct {
Speed float64 Speed float64
Radius float64 Radius float64
@@ -58,6 +60,8 @@ func (p *Projectile) Draw(screen *ebiten.Image) {
) )
} }
// Projectile manager
type Manager struct { type Manager struct {
Projectiles []*Projectile Projectiles []*Projectile
} }

View File

@@ -9,47 +9,43 @@ import (
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
// File path for save data.
const ( const (
dataFile = "data.toml" dataFile = "data.toml"
) )
// Stores all persistent game data in a single file. // Save file structure
type Data struct { type Data struct {
Settings Settings `toml:"settings"` Settings Settings `toml:"settings"`
GameState GameState `toml:"game_state"` GameState GameState `toml:"game_state"`
} }
// Stores user preferences.
type Settings struct { type Settings struct {
FPSMonitor bool `toml:"fps_monitor"` 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 { type GameState struct {
HasSave bool `toml:"has_save"` HasSave bool `toml:"has_save"`
SavedAt time.Time `toml:"saved_at"` SavedAt time.Time `toml:"saved_at"`
HeroX float64 `toml:"hero_x"` HeroX float64 `toml:"hero_x"`
HeroY float64 `toml:"hero_y"` HeroY float64 `toml:"hero_y"`
HeroStamina float64 `toml:"hero_stamina"` 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 { type Manager struct {
dataPath string dataPath string
} }
// Creates a new save manager.
func NewManager() (*Manager, error) { func NewManager() (*Manager, error) {
// Get executable path
exePath, err := os.Executable() exePath, err := os.Executable()
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get executable path: %w", err) return nil, fmt.Errorf("failed to get executable path: %w", err)
} }
// Get directory containing the executable
exeDir := filepath.Dir(exePath) exeDir := filepath.Dir(exePath)
return &Manager{ return &Manager{
@@ -57,11 +53,10 @@ func NewManager() (*Manager, error) {
}, nil }, nil
} }
// Loads all data from disk. // Data operations
func (m *Manager) LoadData() (*Data, error) { func (m *Manager) LoadData() (*Data, error) {
// Check if file exists
if _, err := os.Stat(m.dataPath); os.IsNotExist(err) { if _, err := os.Stat(m.dataPath); os.IsNotExist(err) {
// Return default data
return &Data{ return &Data{
Settings: Settings{ Settings: Settings{
FPSMonitor: false, FPSMonitor: false,
@@ -81,7 +76,6 @@ func (m *Manager) LoadData() (*Data, error) {
return &data, nil return &data, nil
} }
// Writes all data to disk.
func (m *Manager) SaveData(data *Data) error { func (m *Manager) SaveData(data *Data) error {
file, err := os.Create(m.dataPath) file, err := os.Create(m.dataPath)
if err != nil { if err != nil {
@@ -97,7 +91,8 @@ func (m *Manager) SaveData(data *Data) error {
return nil return nil
} }
// Loads user settings from disk. // Settings operations
func (m *Manager) LoadSettings() (*Settings, error) { func (m *Manager) LoadSettings() (*Settings, error) {
data, err := m.LoadData() data, err := m.LoadData()
if err != nil { if err != nil {
@@ -106,11 +101,9 @@ func (m *Manager) LoadSettings() (*Settings, error) {
return &data.Settings, nil return &data.Settings, nil
} }
// Writes user settings to disk.
func (m *Manager) SaveSettings(settings *Settings) error { func (m *Manager) SaveSettings(settings *Settings) error {
data, err := m.LoadData() data, err := m.LoadData()
if err != nil { if err != nil {
// If load fails, create new data with these settings
data = &Data{ data = &Data{
Settings: *settings, Settings: *settings,
GameState: GameState{ GameState: GameState{
@@ -124,7 +117,8 @@ func (m *Manager) SaveSettings(settings *Settings) error {
return m.SaveData(data) return m.SaveData(data)
} }
// Loads game state from disk. // Game state operations
func (m *Manager) LoadGameState() (*GameState, error) { func (m *Manager) LoadGameState() (*GameState, error) {
data, err := m.LoadData() data, err := m.LoadData()
if err != nil { if err != nil {
@@ -132,17 +126,15 @@ func (m *Manager) LoadGameState() (*GameState, error) {
} }
if !data.GameState.HasSave { if !data.GameState.HasSave {
return nil, nil // No save exists return nil, nil
} }
return &data.GameState, nil return &data.GameState, nil
} }
// Writes game state to disk.
func (m *Manager) SaveGameState(state *GameState) error { func (m *Manager) SaveGameState(state *GameState) error {
data, err := m.LoadData() data, err := m.LoadData()
if err != nil { if err != nil {
// If load fails, create new data with this game state
data = &Data{ data = &Data{
Settings: Settings{ Settings: Settings{
FPSMonitor: false, FPSMonitor: false,
@@ -160,7 +152,6 @@ func (m *Manager) SaveGameState(state *GameState) error {
return m.SaveData(data) return m.SaveData(data)
} }
// Checks if a saved game exists.
func (m *Manager) HasSavedGame() bool { func (m *Manager) HasSavedGame() bool {
data, err := m.LoadData() data, err := m.LoadData()
if err != nil { if err != nil {
@@ -169,7 +160,6 @@ func (m *Manager) HasSavedGame() bool {
return data.GameState.HasSave return data.GameState.HasSave
} }
// Removes the saved game.
func (m *Manager) DeleteGameState() error { func (m *Manager) DeleteGameState() error {
data, err := m.LoadData() data, err := m.LoadData()
if err != nil { if err != nil {
@@ -183,7 +173,8 @@ func (m *Manager) DeleteGameState() error {
return m.SaveData(data) return m.SaveData(data)
} }
// Returns the path to the save file. // Utilities
func (m *Manager) GetSaveFilePath() string { func (m *Manager) GetSaveFilePath() string {
return m.dataPath return m.dataPath
} }

View File

@@ -17,6 +17,7 @@ import (
"github.com/atridad/LilGuy/internal/world" "github.com/atridad/LilGuy/internal/world"
) )
// Screen and hero defaults
var ( var (
backgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255} backgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255}
saveNotificationColor = color.NRGBA{R: 50, G: 200, B: 50, 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} heroColor = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
) )
// HUD settings. // HUD positioning and colors
const ( const (
hudX = 960 - 220 // ScreenWidth - 220 hudX = 960 - 220
hudY = 20 hudY = 20
) )
// HUD colors.
var ( var (
staminaNormalColor = color.NRGBA{R: 0, G: 255, B: 180, A: 255} staminaNormalColor = color.NRGBA{R: 0, G: 255, B: 180, A: 255}
staminaLowColor = color.NRGBA{R: 255, G: 60, B: 60, 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} fpsPoorColor = color.NRGBA{R: 255, G: 120, B: 120, A: 255}
) )
// FPS monitoring thresholds
const ( const (
staminaLowThreshold = 0.2 staminaLowThreshold = 0.2
fpsWarnThreshold = 0.85 fpsWarnThreshold = 0.85
@@ -61,7 +62,6 @@ const (
targetTPS = 60 targetTPS = 60
) )
// Player input for the gameplay screen.
type GameplayInput struct { type GameplayInput struct {
Left bool Left bool
Right bool Right bool
@@ -70,8 +70,6 @@ type GameplayInput struct {
Shoot bool 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 { type GameplayScreen struct {
hero *hero.Hero hero *hero.Hero
hud hud.Overlay hud hud.Overlay
@@ -90,7 +88,6 @@ type GameplayScreen struct {
showSaveNotification bool showSaveNotification bool
} }
// Creates a new gameplay screen instance.
func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *GameplayScreen { func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *GameplayScreen {
w := world.NewWorld() 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) { func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
dt := delta.Seconds() 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) { func (g *GameplayScreen) trackFPS(delta time.Duration) {
if g.fpsEnabled == nil || !*g.fpsEnabled { if g.fpsEnabled == nil || !*g.fpsEnabled {
return return
@@ -182,7 +178,7 @@ func (g *GameplayScreen) trackFPS(delta time.Duration) {
} }
} }
// Renders the gameplay screen. // Rendering
func (g *GameplayScreen) Draw(screen *ebiten.Image) { func (g *GameplayScreen) Draw(screen *ebiten.Image) {
screen.Fill(backgroundColor) screen.Fill(backgroundColor)
@@ -205,7 +201,6 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) {
meters := []status.Meter{staminaMeter} meters := []status.Meter{staminaMeter}
if g.fpsEnabled != nil && *g.fpsEnabled { if g.fpsEnabled != nil && *g.fpsEnabled {
// Color based on target FPS (60).
ratio := g.fpsValue / float64(targetTPS) ratio := g.fpsValue / float64(targetTPS)
fpsColor := fpsGoodColor fpsColor := fpsGoodColor
switch { switch {
@@ -217,7 +212,7 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) {
fpsMeter := status.Meter{ fpsMeter := status.Meter{
Label: fmt.Sprintf("Framerate: %3.0f FPS", g.fpsValue), Label: fmt.Sprintf("Framerate: %3.0f FPS", g.fpsValue),
Base: -1, // Negative base means text-only display. Base: -1,
Level: 0, Level: 0,
Color: fpsColor, Color: fpsColor,
} }
@@ -266,7 +261,7 @@ func (g *GameplayScreen) ShowSaveNotification() {
g.saveNotificationTimer = saveNotificationDuration g.saveNotificationTimer = saveNotificationDuration
} }
// Resets the gameplay screen to its initial state. // State management
func (g *GameplayScreen) Reset() { func (g *GameplayScreen) Reset() {
screenWidth := int(g.bounds.Width) screenWidth := int(g.bounds.Width)
screenHeight := int(g.bounds.Height) screenHeight := int(g.bounds.Height)
@@ -307,7 +302,6 @@ func (g *GameplayScreen) Reset() {
g.fpsValue = 0 g.fpsValue = 0
} }
// SaveState converts the current gameplay state to a saveable format.
func (g *GameplayScreen) SaveState() *save.GameState { func (g *GameplayScreen) SaveState() *save.GameState {
return &save.GameState{ return &save.GameState{
HeroX: g.hero.X, 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) { func (g *GameplayScreen) LoadState(state *save.GameState) {
screenWidth := int(g.bounds.Width) screenWidth := int(g.bounds.Width)
screenHeight := int(g.bounds.Height) screenHeight := int(g.bounds.Height)

View File

@@ -8,39 +8,32 @@ import (
"github.com/hajimehoshi/ebiten/v2/text/v2" "github.com/hajimehoshi/ebiten/v2/text/v2"
) )
// An interface for managing FPS cap settings. // Settings interface
type FPSCapSetting interface { type FPSCapSetting interface {
String() string String() string
Cycle() 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 { type SettingsScreen struct {
selectedIndex int selectedIndex int
fpsMonitorValue *bool fpsMonitorValue *bool
fpsCapValue FPSCapSetting fpsCapValue FPSCapSetting
} }
// Creates a new settings screen instance.
func NewSettingsScreen() *SettingsScreen { func NewSettingsScreen() *SettingsScreen {
return &SettingsScreen{ return &SettingsScreen{
selectedIndex: 0, selectedIndex: 0,
} }
} }
// Sets the FPS monitor toggle reference.
func (s *SettingsScreen) SetFPSMonitor(enabled *bool) { func (s *SettingsScreen) SetFPSMonitor(enabled *bool) {
s.fpsMonitorValue = enabled s.fpsMonitorValue = enabled
} }
// Sets the FPS cap setting reference.
func (s *SettingsScreen) SetFPSCap(cap FPSCapSetting) { func (s *SettingsScreen) SetFPSCap(cap FPSCapSetting) {
s.fpsCapValue = cap s.fpsCapValue = cap
} }
// Processes settings screen input.
// Returns true if the user wants to go back.
func (s *SettingsScreen) Update() bool { func (s *SettingsScreen) Update() bool {
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
return true return true
@@ -71,20 +64,19 @@ func (s *SettingsScreen) Update() bool {
return false return false
} }
// Renders the settings screen. // Rendering
func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int, title string) { 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}) screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255})
// Draw title
titleX := (screenWidth / 2) - (len(title) * 7 / 2) titleX := (screenWidth / 2) - (len(title) * 7 / 2)
titleY := screenHeight/3 - 50 titleY := screenHeight/3 - 50
s.drawText(screen, title, color.White, titleX, titleY) s.drawText(screen, title, color.White, titleX, titleY)
// Draw settings options
startY := screenHeight/2 - 20 startY := screenHeight/2 - 20
leftMargin := screenWidth/2 - 120 leftMargin := screenWidth/2 - 120
// FPS monitor toggle
fpsMonitorText := "FPS Monitor: " fpsMonitorText := "FPS Monitor: "
if s.fpsMonitorValue != nil && *s.fpsMonitorValue { if s.fpsMonitorValue != nil && *s.fpsMonitorValue {
fpsMonitorText += "ON" 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) s.drawText(screen, fpsMonitorText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, startY)
} }
// FPS cap setting
fpsCapText := "FPS Cap: " fpsCapText := "FPS Cap: "
if s.fpsCapValue != nil { if s.fpsCapValue != nil {
fpsCapText += s.fpsCapValue.String() 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) 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" hintText := "Enter/Space to toggle, ESC to go back"
hintX := (screenWidth / 2) - (len(hintText) * 7 / 2) hintX := (screenWidth / 2) - (len(hintText) * 7 / 2)
hintY := screenHeight - 50 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) text.Draw(screen, txt, basicFace, op)
} }
// Resets the settings screen to its initial state.
func (s *SettingsScreen) Reset() { func (s *SettingsScreen) Reset() {
s.selectedIndex = 0 s.selectedIndex = 0
} }

View File

@@ -15,14 +15,14 @@ var (
basicFaceAscent = basicFace.Metrics().HAscent basicFaceAscent = basicFace.Metrics().HAscent
) )
// Timing
const ( const (
splashDuration = 2 * time.Second splashDuration = 2 * time.Second
fadeInDuration = 500 * time.Millisecond fadeInDuration = 500 * time.Millisecond
fadeOutDuration = 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 { type SplashScreen struct {
startTime time.Time startTime time.Time
fadeInEnd time.Time fadeInEnd time.Time
@@ -30,7 +30,6 @@ type SplashScreen struct {
endTime time.Time endTime time.Time
} }
// Creates a new splash screen instance.
func NewSplashScreen() *SplashScreen { func NewSplashScreen() *SplashScreen {
now := time.Now() now := time.Now()
return &SplashScreen{ 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 { func (s *SplashScreen) Update() bool {
// Return true if splash is complete
if time.Now().After(s.endTime) { if time.Now().After(s.endTime) {
return true return true
} }
// Allow skipping with any key
if inpututil.IsKeyJustPressed(ebiten.KeySpace) || if inpututil.IsKeyJustPressed(ebiten.KeySpace) ||
inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeyEnter) ||
inpututil.IsKeyJustPressed(ebiten.KeyEscape) { inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
@@ -59,14 +54,14 @@ func (s *SplashScreen) Update() bool {
return false return false
} }
// Renders the splash screen. // Rendering
func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) { func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
screen.Fill(color.RGBA{R: 0, G: 0, B: 0, A: 255}) screen.Fill(color.RGBA{R: 0, G: 0, B: 0, A: 255})
now := time.Now() now := time.Now()
alpha := 1.0 alpha := 1.0
// Calculate fade in/out
if now.Before(s.fadeInEnd) { if now.Before(s.fadeInEnd) {
elapsed := now.Sub(s.startTime) elapsed := now.Sub(s.startTime)
alpha = float64(elapsed) / float64(fadeInDuration) alpha = float64(elapsed) / float64(fadeInDuration)
@@ -81,10 +76,9 @@ func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
alpha = 1 alpha = 1
} }
// Draw large game title // Draw title
titleText := "LIL GUY" titleText := "LIL GUY"
// Calculate size for large text (scale up the basic font)
scale := 4.0 scale := 4.0
charWidth := 7.0 * scale charWidth := 7.0 * scale
textWidth := float64(len(titleText)) * charWidth textWidth := float64(len(titleText)) * charWidth

View File

@@ -9,7 +9,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
) )
// Represents the options available on the title screen. // Menu options
type TitleMenuOption int type TitleMenuOption int
const ( const (
@@ -20,6 +20,7 @@ const (
titleOptionCount titleOptionCount
) )
// Screen modes
type titleScreenMode int type titleScreenMode int
const ( const (
@@ -27,8 +28,6 @@ const (
titleModeSettings 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 { type TitleScreen struct {
selectedIndex int selectedIndex int
currentMode titleScreenMode currentMode titleScreenMode
@@ -36,7 +35,6 @@ type TitleScreen struct {
hasSaveGame bool hasSaveGame bool
} }
// Creates a new title screen instance.
func NewTitleScreen() *TitleScreen { func NewTitleScreen() *TitleScreen {
return &TitleScreen{ return &TitleScreen{
selectedIndex: 0, selectedIndex: 0,
@@ -46,28 +44,22 @@ func NewTitleScreen() *TitleScreen {
} }
} }
// Sets the FPS monitor toggle reference for settings.
func (t *TitleScreen) SetFPSMonitor(enabled *bool) { func (t *TitleScreen) SetFPSMonitor(enabled *bool) {
t.settingsScreen.SetFPSMonitor(enabled) t.settingsScreen.SetFPSMonitor(enabled)
} }
// Sets the FPS cap setting reference for settings.
func (t *TitleScreen) SetFPSCap(cap FPSCapSetting) { func (t *TitleScreen) SetFPSCap(cap FPSCapSetting) {
t.settingsScreen.SetFPSCap(cap) t.settingsScreen.SetFPSCap(cap)
} }
// Sets whether a saved game exists.
func (t *TitleScreen) SetHasSaveGame(hasSave bool) { func (t *TitleScreen) SetHasSaveGame(hasSave bool) {
t.hasSaveGame = hasSave t.hasSaveGame = hasSave
// If no save game, skip Continue option
if !hasSave && t.selectedIndex == 0 { 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 { func (t *TitleScreen) Update() *TitleMenuOption {
// Handle settings screen
if t.currentMode == titleModeSettings { if t.currentMode == titleModeSettings {
if t.settingsScreen.Update() { if t.settingsScreen.Update() {
t.currentMode = titleModeMain t.currentMode = titleModeMain
@@ -76,13 +68,11 @@ func (t *TitleScreen) Update() *TitleMenuOption {
return nil return nil
} }
// Handle main menu
if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) {
t.selectedIndex-- t.selectedIndex--
if t.selectedIndex < 0 { if t.selectedIndex < 0 {
t.selectedIndex = int(titleOptionCount) - 1 t.selectedIndex = int(titleOptionCount) - 1
} }
// Skip Continue if no save exists
if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame { if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame {
t.selectedIndex-- t.selectedIndex--
if t.selectedIndex < 0 { if t.selectedIndex < 0 {
@@ -96,7 +86,6 @@ func (t *TitleScreen) Update() *TitleMenuOption {
if t.selectedIndex >= int(titleOptionCount) { if t.selectedIndex >= int(titleOptionCount) {
t.selectedIndex = 0 t.selectedIndex = 0
} }
// Skip Continue if no save exists
if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame { if t.selectedIndex == int(TitleOptionContinue) && !t.hasSaveGame {
t.selectedIndex++ t.selectedIndex++
if t.selectedIndex >= int(titleOptionCount) { if t.selectedIndex >= int(titleOptionCount) {
@@ -118,18 +107,15 @@ func (t *TitleScreen) Update() *TitleMenuOption {
return nil return nil
} }
// Renders the title screen. // Rendering
func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) { func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
// If in settings mode, draw settings screen
if t.currentMode == titleModeSettings { if t.currentMode == titleModeSettings {
t.settingsScreen.Draw(screen, screenWidth, screenHeight, "SETTINGS") t.settingsScreen.Draw(screen, screenWidth, screenHeight, "SETTINGS")
return return
} }
// Draw main menu
screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255}) screen.Fill(color.RGBA{R: 20, G: 20, B: 30, A: 255})
// Draw game title
titleText := "LIL GUY" titleText := "LIL GUY"
scale := 3.0 scale := 3.0
charWidth := 7.0 * scale 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}) op.ColorScale.ScaleWithColor(color.RGBA{R: 210, G: 220, B: 255, A: 255})
text.Draw(screen, titleText, basicFace, op) text.Draw(screen, titleText, basicFace, op)
// Draw menu options // Draw menu
options := []string{"Continue", "New Game", "Settings", "Quit"} options := []string{"Continue", "New Game", "Settings", "Quit"}
startY := screenHeight/2 + 10 startY := screenHeight/2 + 10
for i, option := range options { for i, option := range options {
// Skip Continue option if no save exists
if i == int(TitleOptionContinue) && !t.hasSaveGame { if i == int(TitleOptionContinue) && !t.hasSaveGame {
continue continue
} }
@@ -163,12 +148,10 @@ func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
} }
if i == t.selectedIndex { if i == t.selectedIndex {
// Draw selection indicator
indicatorX := optionX - 20 indicatorX := optionX - 20
t.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, optionY) 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) t.drawText(screen, option, color.RGBA{R: 255, G: 255, B: 100, A: 255}, optionX, optionY)
// Draw selection box
boxPadding := float32(10.0) boxPadding := float32(10.0)
boxWidth := float32(len(option)*7) + boxPadding*2 boxWidth := float32(len(option)*7) + boxPadding*2
boxHeight := float32(20) 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" hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select"
hintX := (screenWidth / 2) - (len(hintText) * 7 / 2) hintX := (screenWidth / 2) - (len(hintText) * 7 / 2)
hintY := screenHeight - 50 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) text.Draw(screen, txt, basicFace, op)
} }
// Resets the title screen to its initial state.
func (t *TitleScreen) Reset() { func (t *TitleScreen) Reset() {
// Start at Continue if available, otherwise New Game
if t.hasSaveGame { if t.hasSaveGame {
t.selectedIndex = 0 t.selectedIndex = 0
} else { } else {

View File

@@ -5,7 +5,8 @@ import (
"image/color" "image/color"
) )
// HUD resource entry. // Meter types
type Meter struct { type Meter struct {
Label string Label string
Base float64 Base float64
@@ -13,19 +14,18 @@ type Meter struct {
Color color.NRGBA Color color.NRGBA
} }
// Meter template values.
type Config struct { type Config struct {
Label string Label string
Base float64 Base float64
Color color.NRGBA Color color.NRGBA
} }
// Collection of meters. // Meter manager
type Manager struct { type Manager struct {
meters []Meter meters []Meter
} }
// Builds meters from configs.
func NewManager(cfgs []Config) *Manager { func NewManager(cfgs []Config) *Manager {
meters := make([]Meter, len(cfgs)) meters := make([]Meter, len(cfgs))
for i, cfg := range cfgs { for i, cfg := range cfgs {
@@ -40,20 +40,20 @@ func NewManager(cfgs []Config) *Manager {
return &Manager{meters: meters} return &Manager{meters: meters}
} }
// Resets levels to base.
func (m *Manager) Update() { func (m *Manager) Update() {
for i := range m.meters { for i := range m.meters {
m.meters[i].Level = m.meters[i].Base m.meters[i].Level = m.meters[i].Base
} }
} }
// Meters exposes a copy of the internal slice to prevent mutation.
func (m *Manager) Meters() []Meter { func (m *Manager) Meters() []Meter {
out := make([]Meter, len(m.meters)) out := make([]Meter, len(m.meters))
copy(out, m.meters) copy(out, m.meters)
return out return out
} }
// Helper functions
func clamp(value, min, max float64) float64 { func clamp(value, min, max float64) float64 {
if value < min { if value < min {
return min return min

View File

@@ -17,6 +17,8 @@ var (
rectPixel = newRectPixel() rectPixel = newRectPixel()
) )
// Drawing helpers
func newRectPixel() *ebiten.Image { func newRectPixel() *ebiten.Image {
img := ebiten.NewImage(1, 1) img := ebiten.NewImage(1, 1)
img.Fill(color.White) 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) text.Draw(screen, txt, basicFace, op)
} }
// Drawable HUD chunk. // HUD elements
type Element interface { type Element interface {
Draw(screen *ebiten.Image, x, y int) (width, height int) Draw(screen *ebiten.Image, x, y int) (width, height int)
} }
// Plain text node.
type Label struct { type Label struct {
Text string Text string
Color color.Color Color color.Color
@@ -53,7 +55,6 @@ func (l Label) Draw(screen *ebiten.Image, x, y int) (int, int) {
return width, 13 return width, 13
} }
// Percent readout node.
type PercentageDisplay struct { type PercentageDisplay struct {
Meter status.Meter Meter status.Meter
Color color.Color Color color.Color
@@ -68,7 +69,6 @@ func (p PercentageDisplay) Draw(screen *ebiten.Image, x, y int) (int, int) {
return len(txt) * 7, 13 return len(txt) * 7, 13
} }
// Combined label and percent.
type MeterLabel struct { type MeterLabel struct {
Meter status.Meter Meter status.Meter
Color color.Color Color color.Color
@@ -80,17 +80,14 @@ func (m MeterLabel) Draw(screen *ebiten.Image, x, y int) (int, int) {
} }
var txt string var txt string
if m.Meter.Base < 0 { if m.Meter.Base < 0 {
// Text-only display without percentage.
txt = m.Meter.Label txt = m.Meter.Label
} else { } else {
// Standard meter with percentage.
txt = fmt.Sprintf("%s: %3.0f%%", m.Meter.Label, m.Meter.Level) txt = fmt.Sprintf("%s: %3.0f%%", m.Meter.Label, m.Meter.Level)
} }
drawHUDText(screen, txt, m.Meter.Color, x, y) drawHUDText(screen, txt, m.Meter.Color, x, y)
return len(txt) * 7, 13 return len(txt) * 7, 13
} }
// Horizontal meter bar.
type Bar struct { type Bar struct {
Meter status.Meter Meter status.Meter
MaxWidth int MaxWidth int
@@ -117,23 +114,17 @@ func (b Bar) Draw(screen *ebiten.Image, x, y int) (int, int) {
fillWidth = maxWidth fillWidth = maxWidth
} }
// Draw border if requested
if b.ShowBorder { if b.ShowBorder {
borderColor := b.BorderColor borderColor := b.BorderColor
if borderColor == nil { if borderColor == nil {
borderColor = color.RGBA{R: 80, G: 80, B: 80, A: 255} borderColor = color.RGBA{R: 80, G: 80, B: 80, A: 255}
} }
// Top border
drawRect(screen, x, y, maxWidth, 1, borderColor) drawRect(screen, x, y, maxWidth, 1, borderColor)
// Bottom border
drawRect(screen, x, y+height-1, maxWidth, 1, borderColor) drawRect(screen, x, y+height-1, maxWidth, 1, borderColor)
// Left border
drawRect(screen, x, y, 1, height, borderColor) drawRect(screen, x, y, 1, height, borderColor)
// Right border
drawRect(screen, x+maxWidth-1, y, 1, height, borderColor) drawRect(screen, x+maxWidth-1, y, 1, height, borderColor)
} }
// Draw filled portion
if fillWidth > 0 { if fillWidth > 0 {
drawRect(screen, x, y, fillWidth, height, b.Meter.Color) 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 return maxWidth, height
} }
// Helper for filled rectangles. // Rectangle drawing
func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) { func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
return return
@@ -158,7 +150,8 @@ func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) {
screen.DrawImage(rectPixel, op) screen.DrawImage(rectPixel, op)
} }
// Empty padding block. // Layout elements
type Spacer struct { type Spacer struct {
Width int Width int
Height int Height int
@@ -168,7 +161,6 @@ func (s Spacer) Draw(screen *ebiten.Image, x, y int) (int, int) {
return s.Width, s.Height return s.Width, s.Height
} }
// Horizontal layout row.
type Row struct { type Row struct {
Elements []Element Elements []Element
Spacing int Spacing int
@@ -195,7 +187,6 @@ func (r Row) Draw(screen *ebiten.Image, x, y int) (int, int) {
return totalWidth, maxHeight return totalWidth, maxHeight
} }
// Vertical stack layout.
type Column struct { type Column struct {
Elements []Element Elements []Element
Spacing int Spacing int

View File

@@ -8,20 +8,17 @@ import (
"github.com/atridad/LilGuy/internal/status" "github.com/atridad/LilGuy/internal/status"
) )
// HUD overlay anchor.
type Overlay struct { type Overlay struct {
X int X int
Y int Y int
Color color.Color Color color.Color
} }
// Paints the HUD overlay.
func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) { func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
if o.Color == nil { if o.Color == nil {
o.Color = color.White o.Color = color.White
} }
// Instruction text
instructions := Column{ instructions := Column{
Elements: []Element{ Elements: []Element{
Label{Text: "Lil Guy", Color: o.Color}, 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) instructions.Draw(screen, 16, 16)
// Meter column
meterElements := make([]Element, 0, len(meters)) meterElements := make([]Element, 0, len(meters))
for _, meter := range meters { for _, meter := range meters {
if meter.Base < 0 { if meter.Base < 0 {
// Text-only display (no bar).
meterElements = append(meterElements, meterElements = append(meterElements,
MeterLabel{Meter: meter, Color: o.Color}, MeterLabel{Meter: meter, Color: o.Color},
) )
} else { } else {
// Full meter with bar.
meterElements = append(meterElements, Column{ meterElements = append(meterElements, Column{
Elements: []Element{ Elements: []Element{
MeterLabel{Meter: meter, Color: o.Color}, MeterLabel{Meter: meter, Color: o.Color},

View File

@@ -22,6 +22,8 @@ var (
} }
) )
// Menu options
func getOverlayImage(width, height int) *ebiten.Image { func getOverlayImage(width, height int) *ebiten.Image {
if width <= 0 || height <= 0 { if width <= 0 || height <= 0 {
return nil return nil
@@ -46,6 +48,8 @@ const (
optionCount optionCount
) )
// Screen modes
type menuScreen int type menuScreen int
const ( const (
@@ -80,7 +84,8 @@ func (m *PauseMenu) SetFPSCap(cap FPSCapSetting) {
m.settingsScreen.SetFPSCap(cap) m.settingsScreen.SetFPSCap(cap)
} }
// Returns the selected option if one was chosen, nil otherwise // Update logic
func (m *PauseMenu) Update() *MenuOption { func (m *PauseMenu) Update() *MenuOption {
if m.currentScreen == screenSettings { if m.currentScreen == screenSettings {
return m.updateSettings() return m.updateSettings()
@@ -123,6 +128,8 @@ func (m *PauseMenu) updateSettings() *MenuOption {
return nil return nil
} }
// Rendering
func (m *PauseMenu) Draw(screen *ebiten.Image, screenWidth, screenHeight int) { func (m *PauseMenu) Draw(screen *ebiten.Image, screenWidth, screenHeight int) {
if overlay := getOverlayImage(screenWidth, screenHeight); overlay != nil { if overlay := getOverlayImage(screenWidth, screenHeight); overlay != nil {
screen.DrawImage(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" hintText := "Use Arrow Keys/WASD to navigate, Enter/Space to select"
hintX := menuX + (menuWidth / 2) - (len(hintText) * 7 / 2) hintX := menuX + (menuWidth / 2) - (len(hintText) * 7 / 2)
hintY := menuY + menuHeight - 30 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) { func (m *PauseMenu) drawSettings(screen *ebiten.Image, menuX, menuY, menuWidth, menuHeight int) {
// Draw menu background and border
vector.DrawFilledRect(screen, vector.DrawFilledRect(screen,
float32(menuX), float32(menuY), float32(menuX), float32(menuY),
float32(menuWidth), float32(menuHeight), float32(menuWidth), float32(menuHeight),
@@ -200,16 +207,12 @@ func (m *PauseMenu) drawSettings(screen *ebiten.Image, menuX, menuY, menuWidth,
false, 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 screenWidth := menuWidth
screenHeight := menuHeight screenHeight := menuHeight
// Temporarily adjust the drawing to center within the menu
subScreen := ebiten.NewImage(screenWidth, screenHeight) subScreen := ebiten.NewImage(screenWidth, screenHeight)
m.settingsScreen.Draw(subScreen, screenWidth, screenHeight, "SETTINGS") m.settingsScreen.Draw(subScreen, screenWidth, screenHeight, "SETTINGS")
// Draw the settings content in the menu area
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.GeoM.Translate(float64(menuX), float64(menuY)) op.GeoM.Translate(float64(menuX), float64(menuY))
screen.DrawImage(subScreen, op) screen.DrawImage(subScreen, op)

View File

@@ -7,6 +7,8 @@ import (
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
) )
// Surface types
type SurfaceTag int type SurfaceTag int
const ( const (
@@ -67,6 +69,8 @@ func (s *Surface) Draw(screen *ebiten.Image) {
) )
} }
// World container
type World struct { type World struct {
Surfaces []*Surface Surfaces []*Surface
} }