From c84ba3735344950dd023e94db53ae047a86e5c6e Mon Sep 17 00:00:00 2001 From: Atridad Lahiji Date: Tue, 25 Nov 2025 01:06:35 -0700 Subject: [PATCH] Optimizations and debug options --- internal/config/constants.go | 16 ++-- internal/config/factory.go | 78 ----------------- internal/game/game.go | 11 ++- internal/hero/hero.go | 53 +++++++++--- internal/maps/desert.go | 42 +++++++++ internal/maps/map.go | 40 ++------- internal/maps/plains.go | 42 +++++++++ internal/portal/portal.go | 36 ++++++-- internal/projectile/projectile.go | 17 +++- internal/save/save.go | 1 + internal/screens/gameplay.go | 102 ++++++++++++++-------- internal/screens/settings.go | 138 ++++++++++++++++++++++++------ internal/world/surface.go | 13 ++- 13 files changed, 377 insertions(+), 212 deletions(-) delete mode 100644 internal/config/factory.go create mode 100644 internal/maps/desert.go create mode 100644 internal/maps/plains.go diff --git a/internal/config/constants.go b/internal/config/constants.go index 2ee0916..57c29ed 100644 --- a/internal/config/constants.go +++ b/internal/config/constants.go @@ -12,18 +12,18 @@ const ( // Physics const ( - Gravity = 1200.0 - JumpStrength = -450.0 - MaxFallSpeed = 800.0 - GroundFriction = 0.85 - AirFriction = 0.95 + Gravity = 1400.0 + JumpStrength = -480.0 + MaxFallSpeed = 900.0 + GroundFriction = 0.82 + AirFriction = 0.96 ) // Gameplay const ( - SprintSpeedMultiplier = 1.8 - SprintRecoveryThreshold = 0.2 - ExhaustedThreshold = 0.2 + SprintSpeedMultiplier = 2.0 + SprintRecoveryThreshold = 0.25 + ExhaustedThreshold = 0.15 StaminaLowThreshold = 0.2 ) diff --git a/internal/config/factory.go b/internal/config/factory.go deleted file mode 100644 index d9eb03d..0000000 --- a/internal/config/factory.go +++ /dev/null @@ -1,78 +0,0 @@ -package config - -import ( - "image/color" -) - -type MapConfig struct { - ID string - Number int - GroundColor color.NRGBA - BackgroundColor color.NRGBA -} - -func DefaultMap1() MapConfig { - return MapConfig{ - ID: "map1", - Number: 1, - GroundColor: color.NRGBA{R: 34, G: 139, B: 34, A: 255}, - BackgroundColor: color.NRGBA{R: 135, G: 206, B: 235, A: 255}, - } -} - -func DefaultMap2() MapConfig { - return MapConfig{ - ID: "map2", - Number: 2, - GroundColor: color.NRGBA{R: 139, G: 69, B: 19, A: 255}, - BackgroundColor: color.NRGBA{R: 155, G: 196, B: 215, A: 255}, - } -} - -type PortalConfig struct { - ID string - DestinationMap string - DestinationPortal string - Color color.NRGBA - GlowColor color.NRGBA -} - -func LeftPortalMap1() PortalConfig { - return PortalConfig{ - ID: "map1_left", - DestinationMap: "map2", - DestinationPortal: "map2_right", - Color: color.NRGBA{R: 255, G: 100, B: 100, A: 180}, - GlowColor: color.NRGBA{R: 255, G: 150, B: 150, A: 100}, - } -} - -func RightPortalMap1() PortalConfig { - return PortalConfig{ - ID: "map1_right", - DestinationMap: "map2", - DestinationPortal: "map2_left", - Color: color.NRGBA{R: 100, G: 255, B: 100, A: 180}, - GlowColor: color.NRGBA{R: 150, G: 255, B: 150, A: 100}, - } -} - -func LeftPortalMap2() PortalConfig { - return PortalConfig{ - ID: "map2_left", - DestinationMap: "map1", - DestinationPortal: "map1_right", - Color: color.NRGBA{R: 100, G: 255, B: 100, A: 180}, - GlowColor: color.NRGBA{R: 150, G: 255, B: 150, A: 100}, - } -} - -func RightPortalMap2() PortalConfig { - return PortalConfig{ - ID: "map2_right", - DestinationMap: "map1", - DestinationPortal: "map1_left", - Color: color.NRGBA{R: 255, G: 100, B: 100, A: 180}, - GlowColor: color.NRGBA{R: 255, G: 150, B: 150, A: 100}, - } -} diff --git a/internal/game/game.go b/internal/game/game.go index 7304a8a..6c9c83d 100644 --- a/internal/game/game.go +++ b/internal/game/game.go @@ -273,6 +273,16 @@ func (g *Game) updatePlaying() error { 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, @@ -284,7 +294,6 @@ func (g *Game) updatePlaying() error { if now.Sub(g.state.lastAutoSave) >= g.state.autoSaveInterval { g.saveGame() - g.state.gameplayScreen.ShowSaveNotification() g.state.lastAutoSave = now } diff --git a/internal/hero/hero.go b/internal/hero/hero.go index baca4c5..5c24ab4 100644 --- a/internal/hero/hero.go +++ b/internal/hero/hero.go @@ -137,6 +137,9 @@ func New(cfg Config) *Hero { } func (h *Hero) Update(input Input, dt float64, bounds Bounds) { + if dt > 0.1 { + dt = 0.1 + } h.updateMovement(input, dt, bounds) h.updateStamina(input, dt) h.updateAnimation(dt) @@ -145,27 +148,30 @@ func (h *Hero) Update(input Input, dt float64, bounds Bounds) { // Movement and physics func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) { + // apply gravity h.VelocityY += config.Gravity * dt if h.VelocityY > config.MaxFallSpeed { h.VelocityY = config.MaxFallSpeed } - h.Y += h.VelocityY * dt + newY := h.Y + h.VelocityY*dt - footPosition := h.Y - if footPosition >= bounds.Ground { + wasGrounded := h.isGrounded + if newY >= bounds.Ground { h.Y = bounds.Ground h.VelocityY = 0 h.isGrounded = true } else { + h.Y = newY h.isGrounded = false } - if input.Jump && h.isGrounded { + if input.Jump && (h.isGrounded || (!wasGrounded && h.isGrounded)) { h.VelocityY = config.JumpStrength h.isGrounded = false } + // horizontal input targetVelocityX := 0.0 if input.Left { targetVelocityX -= h.Speed @@ -178,6 +184,7 @@ func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) { h.isMoving = targetVelocityX != 0 + // sprinting h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && h.isMoving if h.isSprinting { targetVelocityX *= config.SprintSpeedMultiplier @@ -188,17 +195,22 @@ func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) { friction = config.AirFriction } - h.VelocityX = h.VelocityX*friction + targetVelocityX*(1-friction) + frictionFactor := 1.0 - (1.0-friction)*dt*60.0 + if frictionFactor < 0 { + frictionFactor = 0 + } + h.VelocityX = h.VelocityX*frictionFactor + targetVelocityX*(1.0-frictionFactor) - h.X += h.VelocityX * dt + newX := h.X + h.VelocityX*dt - if h.X < h.Radius { + if newX < h.Radius { h.X = h.Radius h.VelocityX = 0 - } - if h.X > bounds.Width-h.Radius { + } else if newX > bounds.Width-h.Radius { h.X = bounds.Width - h.Radius h.VelocityX = 0 + } else { + h.X = newX } } @@ -234,12 +246,14 @@ func (h *Hero) updateAnimation(dt float64) { key.state = animMove } + // reset animation on state change if key != h.lastAnimKey { h.animFrame = 0 h.animTimer = 0 h.lastAnimKey = key } + // advance animation only when moving if isMoving { animSpeed := config.NormalAnimSpeed * 0.5 if h.isSprinting { @@ -248,12 +262,17 @@ func (h *Hero) updateAnimation(dt float64) { if !h.isGrounded { animSpeed = config.JumpingAnimSpeed * 0.5 } + h.animTimer += dt - frameAdvance := int(h.animTimer / animSpeed) - if frameAdvance > 0 { + + // precise frame advancement + if h.animTimer >= animSpeed { + frameAdvance := int(h.animTimer / animSpeed) h.animTimer -= animSpeed * float64(frameAdvance) h.animFrame = (h.animFrame + frameAdvance) % config.AnimFrameWrap } + } else { + h.animTimer = 0 } } @@ -280,18 +299,28 @@ func (h *Hero) Draw(screen *ebiten.Image) { actualWidth := float64(bounds.Dx()) op := &ebiten.DrawImageOptions{} + + // center sprite horizontally, align bottom to feet op.GeoM.Translate(-actualWidth/2, -actualHeight) op.GeoM.Scale(config.HeroSpriteScale, config.HeroSpriteScale) - op.GeoM.Translate(h.X, h.Y) + // round position to nearest pixel for crisp rendering + drawX := float64(int(h.X + 0.5)) + drawY := float64(int(h.Y + 0.5)) + op.GeoM.Translate(drawX, drawY) + + // apply visual state coloring state := h.getVisualState() switch state { case StateExhausted: op.ColorScale.ScaleWithColor(color.RGBA{R: 255, G: 100, B: 100, A: 255}) case StateSprinting: + // no color modification for sprinting case StateIdle: + // no color modification for idle } + op.Filter = ebiten.FilterNearest screen.DrawImage(sprite, op) } } diff --git a/internal/maps/desert.go b/internal/maps/desert.go new file mode 100644 index 0000000..1d7f575 --- /dev/null +++ b/internal/maps/desert.go @@ -0,0 +1,42 @@ +package maps + +import ( + "image/color" + + "github.com/atridad/LilGuy/internal/config" + "github.com/atridad/LilGuy/internal/portal" + "github.com/atridad/LilGuy/internal/world" +) + +func CreateDesert(screenWidth, screenHeight float64) *Map { + m := NewMap("desert", 2, "Desert", screenWidth, screenHeight) + m.BackgroundColor = color.NRGBA{R: 155, G: 196, B: 215, A: 255} + + // ground surface + m.World.AddSurface(&world.Surface{ + X: 0, + Y: screenHeight - config.GroundHeight, + Width: screenWidth, + Height: config.GroundHeight, + Tag: world.TagGround, + Color: color.NRGBA{R: 139, G: 69, B: 19, A: 255}, + }) + + // left portal to plains + leftPortal := portal.CreateSidePortal("desert_left", portal.SideLeft, screenWidth, screenHeight) + leftPortal.DestinationMap = "plains" + leftPortal.DestinationPortal = "plains_right" + leftPortal.Color = color.NRGBA{R: 100, G: 255, B: 100, A: 180} + leftPortal.GlowColor = color.NRGBA{R: 150, G: 255, B: 150, A: 100} + m.AddPortal(leftPortal) + + // right portal to plains + rightPortal := portal.CreateSidePortal("desert_right", portal.SideRight, screenWidth, screenHeight) + rightPortal.DestinationMap = "plains" + rightPortal.DestinationPortal = "plains_left" + rightPortal.Color = color.NRGBA{R: 255, G: 100, B: 100, A: 180} + rightPortal.GlowColor = color.NRGBA{R: 255, G: 150, B: 150, A: 100} + m.AddPortal(rightPortal) + + return m +} diff --git a/internal/maps/map.go b/internal/maps/map.go index 90148d0..8062b22 100644 --- a/internal/maps/map.go +++ b/internal/maps/map.go @@ -3,7 +3,6 @@ package maps import ( "image/color" - "github.com/atridad/LilGuy/internal/config" "github.com/atridad/LilGuy/internal/portal" "github.com/atridad/LilGuy/internal/world" ) @@ -11,6 +10,7 @@ import ( type Map struct { ID string Number int + DisplayName string Width float64 Height float64 World *world.World @@ -18,10 +18,11 @@ type Map struct { BackgroundColor color.NRGBA } -func NewMap(id string, number int, width, height float64) *Map { +func NewMap(id string, number int, displayName string, width, height float64) *Map { return &Map{ ID: id, Number: number, + DisplayName: displayName, Width: width, Height: height, World: world.NewWorld(), @@ -44,36 +45,7 @@ func (m *Map) GetPortalByID(id string) *portal.Portal { } func CreateDefaultMaps(screenWidth, screenHeight float64) (*Map, *Map) { - map1 := createMapFromConfig(config.DefaultMap1(), screenWidth, screenHeight) - map2 := createMapFromConfig(config.DefaultMap2(), screenWidth, screenHeight) - - addPortalFromConfig(map1, config.LeftPortalMap1(), portal.SideLeft, screenWidth, screenHeight) - addPortalFromConfig(map1, config.RightPortalMap1(), portal.SideRight, screenWidth, screenHeight) - addPortalFromConfig(map2, config.LeftPortalMap2(), portal.SideLeft, screenWidth, screenHeight) - addPortalFromConfig(map2, config.RightPortalMap2(), portal.SideRight, screenWidth, screenHeight) - - return map1, map2 -} - -func createMapFromConfig(cfg config.MapConfig, width, height float64) *Map { - m := NewMap(cfg.ID, cfg.Number, width, height) - m.BackgroundColor = cfg.BackgroundColor - m.World.AddSurface(&world.Surface{ - X: 0, - Y: height - config.GroundHeight, - Width: width, - Height: config.GroundHeight, - Tag: world.TagGround, - Color: cfg.GroundColor, - }) - return m -} - -func addPortalFromConfig(m *Map, cfg config.PortalConfig, side portal.PortalSide, screenWidth, screenHeight float64) { - p := portal.CreateSidePortal(cfg.ID, side, screenWidth, screenHeight) - p.DestinationMap = cfg.DestinationMap - p.DestinationPortal = cfg.DestinationPortal - p.Color = cfg.Color - p.GlowColor = cfg.GlowColor - m.AddPortal(p) + plains := CreatePlains(screenWidth, screenHeight) + desert := CreateDesert(screenWidth, screenHeight) + return plains, desert } diff --git a/internal/maps/plains.go b/internal/maps/plains.go new file mode 100644 index 0000000..a3f0a14 --- /dev/null +++ b/internal/maps/plains.go @@ -0,0 +1,42 @@ +package maps + +import ( + "image/color" + + "github.com/atridad/LilGuy/internal/config" + "github.com/atridad/LilGuy/internal/portal" + "github.com/atridad/LilGuy/internal/world" +) + +func CreatePlains(screenWidth, screenHeight float64) *Map { + m := NewMap("plains", 1, "Plains", screenWidth, screenHeight) + m.BackgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255} + + // ground surface + m.World.AddSurface(&world.Surface{ + X: 0, + Y: screenHeight - config.GroundHeight, + Width: screenWidth, + Height: config.GroundHeight, + Tag: world.TagGround, + Color: color.NRGBA{R: 34, G: 139, B: 34, A: 255}, + }) + + // left portal to desert + leftPortal := portal.CreateSidePortal("plains_left", portal.SideLeft, screenWidth, screenHeight) + leftPortal.DestinationMap = "desert" + leftPortal.DestinationPortal = "desert_right" + leftPortal.Color = color.NRGBA{R: 255, G: 100, B: 100, A: 180} + leftPortal.GlowColor = color.NRGBA{R: 255, G: 150, B: 150, A: 100} + m.AddPortal(leftPortal) + + // right portal to desert + rightPortal := portal.CreateSidePortal("plains_right", portal.SideRight, screenWidth, screenHeight) + rightPortal.DestinationMap = "desert" + rightPortal.DestinationPortal = "desert_left" + rightPortal.Color = color.NRGBA{R: 100, G: 255, B: 100, A: 180} + rightPortal.GlowColor = color.NRGBA{R: 150, G: 255, B: 150, A: 100} + m.AddPortal(rightPortal) + + return m +} diff --git a/internal/portal/portal.go b/internal/portal/portal.go index bdc2b5c..f29d67e 100644 --- a/internal/portal/portal.go +++ b/internal/portal/portal.go @@ -98,6 +98,10 @@ func (p *Portal) Update(dt float64) { return } + if dt > 0.1 { + dt = 0.1 + } + p.GlowIntensity += dt * 2.0 if p.GlowIntensity > 6.28 { // 2*PI p.GlowIntensity -= 6.28 @@ -109,18 +113,24 @@ func (p *Portal) Draw(screen *ebiten.Image) { return } + drawX := float32(int(p.X + 0.5)) + drawY := float32(int(p.Y + 0.5)) + drawWidth := float32(int(p.Width + 0.5)) + drawHeight := float32(int(p.Height + 0.5)) + vector.FillRect( screen, - float32(p.X), - float32(p.Y), - float32(p.Width), - float32(p.Height), + drawX, + drawY, + drawWidth, + drawHeight, p.Color, false, ) if p.Enabled { - glowAlpha := uint8(float64(p.GlowColor.A) * (0.5 + 0.5*float64(p.GlowIntensity))) + glowPulse := 0.5 + 0.5*float64(p.GlowIntensity)/6.28 + glowAlpha := uint8(float64(p.GlowColor.A) * glowPulse) glowColor := color.NRGBA{ R: p.GlowColor.R, G: p.GlowColor.G, @@ -131,10 +141,10 @@ func (p *Portal) Draw(screen *ebiten.Image) { borderWidth := float32(4) vector.StrokeRect( screen, - float32(p.X)-borderWidth/2, - float32(p.Y)-borderWidth/2, - float32(p.Width)+borderWidth, - float32(p.Height)+borderWidth, + drawX-borderWidth/2, + drawY-borderWidth/2, + drawWidth+borderWidth, + drawHeight+borderWidth, borderWidth, glowColor, false, @@ -182,12 +192,20 @@ func (m *Manager) Clear() { } func (m *Manager) Update(dt float64) { + // clamp delta time to prevent physics issues + if dt > 0.1 { + dt = 0.1 + } + for _, p := range m.Portals { p.Update(dt) } if m.transitionCooldown > 0 { m.transitionCooldown -= dt + if m.transitionCooldown < 0 { + m.transitionCooldown = 0 + } } } diff --git a/internal/projectile/projectile.go b/internal/projectile/projectile.go index e14d090..dd9b23d 100644 --- a/internal/projectile/projectile.go +++ b/internal/projectile/projectile.go @@ -38,10 +38,15 @@ func New(x, y, directionX, directionY float64, config ProjectileConfig) *Project } func (p *Projectile) Update(dt float64, screenWidth, screenHeight float64) { + if dt > 0.1 { + dt = 0.1 + } + p.X += p.VelocityX * dt p.Y += p.VelocityY * dt - if p.X < 0 || p.X > screenWidth || p.Y < 0 || p.Y > screenHeight { + margin := p.Radius * 2 + if p.X < -margin || p.X > screenWidth+margin || p.Y < -margin || p.Y > screenHeight+margin { p.Active = false } } @@ -50,13 +55,17 @@ func (p *Projectile) Draw(screen *ebiten.Image) { if !p.Active { return } + + drawX := float32(int(p.X + 0.5)) + drawY := float32(int(p.Y + 0.5)) + vector.DrawFilledCircle( screen, - float32(p.X), - float32(p.Y), + drawX, + drawY, float32(p.Radius), p.Color, - false, + true, ) } diff --git a/internal/save/save.go b/internal/save/save.go index f548d10..20f1b86 100644 --- a/internal/save/save.go +++ b/internal/save/save.go @@ -29,6 +29,7 @@ type Settings struct { type GameState struct { HasSave bool `toml:"has_save"` SavedAt time.Time `toml:"saved_at"` + CurrentMap string `toml:"current_map"` HeroX float64 `toml:"hero_x"` HeroY float64 `toml:"hero_y"` HeroStamina float64 `toml:"hero_stamina"` diff --git a/internal/screens/gameplay.go b/internal/screens/gameplay.go index b106e7e..7732e60 100644 --- a/internal/screens/gameplay.go +++ b/internal/screens/gameplay.go @@ -54,15 +54,15 @@ type GameplayScreen struct { func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool, portalVisibility *bool) *GameplayScreen { cfg := config.Default() - map1, map2 := maps.CreateDefaultMaps(float64(screenWidth), float64(screenHeight)) + plains, desert := maps.CreateDefaultMaps(float64(screenWidth), float64(screenHeight)) allMaps := make(map[string]*maps.Map) - allMaps["map1"] = map1 - allMaps["map2"] = map2 + allMaps["plains"] = plains + allMaps["desert"] = desert portalMgr := portal.NewManager() - for _, p := range map1.Portals { + for _, p := range plains.Portals { portalMgr.AddPortal(p) } @@ -87,9 +87,9 @@ func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool, portalVi X: cfg.HUD.X, Y: cfg.HUD.Y, Color: color.White, - ScreenName: "Map 1", + ScreenName: "Plains", }, - world: map1.World, + world: plains.World, projectiles: projectile.NewManager(), portals: portalMgr, bounds: hero.Bounds{ @@ -101,7 +101,7 @@ func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool, portalVi gameStartTime: time.Now(), fpsEnabled: fpsEnabled, portalVisibility: portalVisibility, - currentMap: map1, + currentMap: plains, allMaps: allMaps, } @@ -111,6 +111,11 @@ func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool, portalVi } func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) { + // clamp delta to prevent physics issues from lag spikes + if delta > 100*time.Millisecond { + delta = 100 * time.Millisecond + } + dt := delta.Seconds() g.hero.Update(hero.Input{ @@ -131,13 +136,14 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) { g.projectiles.Update(dt, g.bounds.Width, g.bounds.Height) g.portals.Update(dt) - // Check for portal collisions + // check for portal collisions g.checkPortalCollision() g.totalPlayTime += delta g.trackFPS(delta) + // update save notification timer if g.showSaveNotification { g.saveNotificationTimer -= delta if g.saveNotificationTimer <= 0 { @@ -146,7 +152,7 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) { } } -// Portal collision detection +// portal collision detection func (g *GameplayScreen) checkPortalCollision() { heroRadius := g.hero.Radius heroX := g.hero.X - heroRadius @@ -157,19 +163,6 @@ func (g *GameplayScreen) checkPortalCollision() { if p := g.portals.CheckCollision(heroX, heroY, heroWidth, heroHeight); p != nil { g.portals.TriggerTransition(p, g.hero.X, g.hero.Y) } - - g.updatePortalVisibility() -} - -func (g *GameplayScreen) updatePortalVisibility() { - if g.portalVisibility == nil { - return - } - - visible := *g.portalVisibility - for _, p := range g.portals.Portals { - p.Visible = visible - } } func (g *GameplayScreen) handlePortalTransition(event portal.TransitionEvent) { @@ -188,7 +181,13 @@ func (g *GameplayScreen) handlePortalTransition(event portal.TransitionEvent) { g.portals.AddPortal(p) } - g.updatePortalVisibility() + // set portal visibility + if g.portalVisibility != nil { + visible := *g.portalVisibility + for _, p := range g.portals.Portals { + p.Visible = visible + } + } // Find destination portal and position hero destPortal := destMap.GetPortalByID(event.DestinationPortal) @@ -242,7 +241,7 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) { g.hero.Draw(screen) if g.currentMap != nil { - g.hud.ScreenName = fmt.Sprintf("Map %d", g.currentMap.Number) + g.hud.ScreenName = g.currentMap.DisplayName } staminaColor := cfg.Visual.StaminaNormalColor @@ -327,11 +326,11 @@ func (g *GameplayScreen) Reset() { screenWidth := int(g.bounds.Width) screenHeight := int(g.bounds.Height) - map1, map2 := maps.CreateDefaultMaps(float64(screenWidth), float64(screenHeight)) + plains, desert := maps.CreateDefaultMaps(float64(screenWidth), float64(screenHeight)) g.allMaps = make(map[string]*maps.Map) - g.allMaps["map1"] = map1 - g.allMaps["map2"] = map2 - g.currentMap = map1 + g.allMaps["plains"] = plains + g.allMaps["desert"] = desert + g.currentMap = plains g.hero = hero.New(hero.Config{ StartX: cfg.Hero.StartX, @@ -343,14 +342,22 @@ func (g *GameplayScreen) Reset() { StaminaDrain: cfg.Hero.StaminaDrain, StaminaRegen: cfg.Hero.StaminaRegen, }) - g.world = map1.World + g.world = plains.World g.projectiles = projectile.NewManager() g.portals = portal.NewManager() - for _, p := range map1.Portals { + for _, p := range plains.Portals { g.portals.AddPortal(p) } g.portals.OnTransition = g.handlePortalTransition - g.updatePortalVisibility() + + // set portal visibility + if g.portalVisibility != nil { + visible := *g.portalVisibility + for _, p := range g.portals.Portals { + p.Visible = visible + } + } + g.bounds = hero.Bounds{ Width: float64(screenWidth), Height: float64(screenHeight), @@ -365,7 +372,12 @@ func (g *GameplayScreen) Reset() { } func (g *GameplayScreen) SaveState() *save.GameState { + currentMapID := "map1" + if g.currentMap != nil { + currentMapID = g.currentMap.ID + } return &save.GameState{ + CurrentMap: currentMapID, HeroX: g.hero.X, HeroY: g.hero.Y, HeroStamina: g.hero.Stamina, @@ -378,11 +390,17 @@ func (g *GameplayScreen) LoadState(state *save.GameState) { screenWidth := int(g.bounds.Width) screenHeight := int(g.bounds.Height) - map1, map2 := maps.CreateDefaultMaps(float64(screenWidth), float64(screenHeight)) + plains, desert := maps.CreateDefaultMaps(float64(screenWidth), float64(screenHeight)) g.allMaps = make(map[string]*maps.Map) - g.allMaps["map1"] = map1 - g.allMaps["map2"] = map2 - g.currentMap = map1 + g.allMaps["plains"] = plains + g.allMaps["desert"] = desert + + // load the saved map or default to plains + savedMap := g.allMaps[state.CurrentMap] + if savedMap == nil { + savedMap = plains + } + g.currentMap = savedMap g.hero = hero.New(hero.Config{ StartX: state.HeroX, @@ -395,14 +413,22 @@ func (g *GameplayScreen) LoadState(state *save.GameState) { StaminaRegen: cfg.Hero.StaminaRegen, }) g.hero.Stamina = state.HeroStamina - g.world = map1.World + g.world = g.currentMap.World g.projectiles = projectile.NewManager() g.portals = portal.NewManager() - for _, p := range map1.Portals { + for _, p := range g.currentMap.Portals { g.portals.AddPortal(p) } g.portals.OnTransition = g.handlePortalTransition - g.updatePortalVisibility() + + // set portal visibility + if g.portalVisibility != nil { + visible := *g.portalVisibility + for _, p := range g.portals.Portals { + p.Visible = visible + } + } + g.bounds = hero.Bounds{ Width: float64(screenWidth), Height: float64(screenHeight), diff --git a/internal/screens/settings.go b/internal/screens/settings.go index a938c66..0f07fcf 100644 --- a/internal/screens/settings.go +++ b/internal/screens/settings.go @@ -14,8 +14,16 @@ type FPSCapSetting interface { Cycle() } +type settingsScreen int + +const ( + settingsMain settingsScreen = iota + settingsDebugOptions +) + type SettingsScreen struct { selectedIndex int + currentScreen settingsScreen fpsMonitorValue *bool fpsCapValue FPSCapSetting portalVisibilityValue *bool @@ -24,6 +32,7 @@ type SettingsScreen struct { func NewSettingsScreen() *SettingsScreen { return &SettingsScreen{ selectedIndex: 0, + currentScreen: settingsMain, } } @@ -41,10 +50,22 @@ func (s *SettingsScreen) SetPortalVisibility(enabled *bool) { func (s *SettingsScreen) Update() bool { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { + if s.currentScreen == settingsDebugOptions { + s.currentScreen = settingsMain + s.selectedIndex = 0 + return false + } return true } - settingsCount := 3 + if s.currentScreen == settingsDebugOptions { + return s.updateDebugOptions() + } + return s.updateMain() +} + +func (s *SettingsScreen) updateMain() bool { + settingsCount := 2 if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { s.selectedIndex-- if s.selectedIndex < 0 { @@ -58,13 +79,41 @@ func (s *SettingsScreen) Update() bool { } } + if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace) { + if s.selectedIndex == 0 && s.fpsCapValue != nil { + s.fpsCapValue.Cycle() + } else if s.selectedIndex == 1 { + s.currentScreen = settingsDebugOptions + s.selectedIndex = 0 + } + } + + return false +} + +func (s *SettingsScreen) updateDebugOptions() bool { + debugOptionsCount := 3 + if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) || inpututil.IsKeyJustPressed(ebiten.KeyW) { + s.selectedIndex-- + if s.selectedIndex < 0 { + s.selectedIndex = 0 + } + } + if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) || inpututil.IsKeyJustPressed(ebiten.KeyS) { + s.selectedIndex++ + if s.selectedIndex >= debugOptionsCount { + s.selectedIndex = debugOptionsCount - 1 + } + } + if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeySpace) { if s.selectedIndex == 0 && s.fpsMonitorValue != nil { *s.fpsMonitorValue = !*s.fpsMonitorValue - } else if s.selectedIndex == 1 && s.fpsCapValue != nil { - s.fpsCapValue.Cycle() - } else if s.selectedIndex == 2 && s.portalVisibilityValue != nil { + } else if s.selectedIndex == 1 && s.portalVisibilityValue != nil { *s.portalVisibilityValue = !*s.portalVisibilityValue + } else if s.selectedIndex == 2 { + s.currentScreen = settingsMain + s.selectedIndex = 0 } } @@ -80,6 +129,52 @@ func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight in titleY := screenHeight/3 - 50 s.drawText(screen, title, color.White, titleX, titleY) + if s.currentScreen == settingsDebugOptions { + s.drawDebugOptions(screen, screenWidth, screenHeight) + } else { + s.drawMain(screen, screenWidth, screenHeight) + } +} + +func (s *SettingsScreen) drawMain(screen *ebiten.Image, screenWidth, screenHeight int) { + startY := screenHeight/2 - 20 + leftMargin := screenWidth/2 - 120 + + // FPS cap setting + fpsCapText := "FPS Cap: " + if s.fpsCapValue != nil { + fpsCapText += s.fpsCapValue.String() + } else { + fpsCapText += "60 FPS" + } + + if s.selectedIndex == 0 { + indicatorX := leftMargin - 20 + s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, startY) + s.drawText(screen, fpsCapText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, startY) + } else { + s.drawText(screen, fpsCapText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, startY) + } + + // debug options submenu + debugOptionsText := "Debug Options >" + debugY := startY + 40 + if s.selectedIndex == 1 { + indicatorX := leftMargin - 20 + s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, debugY) + s.drawText(screen, debugOptionsText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, debugY) + } else { + s.drawText(screen, debugOptionsText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, debugY) + } + + // Instructions + hintText := "Enter/Space to select, ESC to go back" + hintX := (screenWidth / 2) - (len(hintText) * 7 / 2) + hintY := screenHeight - 50 + s.drawText(screen, hintText, color.RGBA{R: 120, G: 120, B: 150, A: 255}, hintX, hintY) +} + +func (s *SettingsScreen) drawDebugOptions(screen *ebiten.Image, screenWidth, screenHeight int) { startY := screenHeight/2 - 20 leftMargin := screenWidth/2 - 120 @@ -99,23 +194,6 @@ 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() - } else { - fpsCapText += "60 FPS" - } - - capY := startY + 40 - if s.selectedIndex == 1 { - indicatorX := leftMargin - 20 - s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, capY) - s.drawText(screen, fpsCapText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, capY) - } else { - s.drawText(screen, fpsCapText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, capY) - } - // Portal visibility toggle portalVisText := "Portal Visibility: " if s.portalVisibilityValue != nil && *s.portalVisibilityValue { @@ -124,8 +202,8 @@ func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight in portalVisText += "OFF" } - portalY := startY + 80 - if s.selectedIndex == 2 { + portalY := startY + 40 + if s.selectedIndex == 1 { indicatorX := leftMargin - 20 s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, portalY) s.drawText(screen, portalVisText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, portalY) @@ -133,8 +211,19 @@ func (s *SettingsScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight in s.drawText(screen, portalVisText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, portalY) } + // back option + backText := "< Back" + backY := startY + 80 + if s.selectedIndex == 2 { + indicatorX := leftMargin - 20 + s.drawText(screen, ">", color.RGBA{R: 255, G: 200, B: 0, A: 255}, indicatorX, backY) + s.drawText(screen, backText, color.RGBA{R: 255, G: 255, B: 100, A: 255}, leftMargin, backY) + } else { + s.drawText(screen, backText, color.RGBA{R: 180, G: 180, B: 200, A: 255}, leftMargin, backY) + } + // Instructions - hintText := "Enter/Space to toggle, ESC to go back" + hintText := "Enter/Space to select, ESC to go back" hintX := (screenWidth / 2) - (len(hintText) * 7 / 2) hintY := screenHeight - 50 s.drawText(screen, hintText, color.RGBA{R: 120, G: 120, B: 150, A: 255}, hintX, hintY) @@ -149,4 +238,5 @@ func (s *SettingsScreen) drawText(screen *ebiten.Image, txt string, clr color.Co func (s *SettingsScreen) Reset() { s.selectedIndex = 0 + s.currentScreen = settingsMain } diff --git a/internal/world/surface.go b/internal/world/surface.go index 0910ead..69e6476 100644 --- a/internal/world/surface.go +++ b/internal/world/surface.go @@ -58,12 +58,17 @@ func (s *Surface) IsWalkable() bool { } func (s *Surface) Draw(screen *ebiten.Image) { + drawX := float32(int(s.X + 0.5)) + drawY := float32(int(s.Y + 0.5)) + drawWidth := float32(int(s.Width + 0.5)) + drawHeight := float32(int(s.Height + 0.5)) + vector.FillRect( screen, - float32(s.X), - float32(s.Y), - float32(s.Width), - float32(s.Height), + drawX, + drawY, + drawWidth, + drawHeight, s.Color, false, )