Optimizations and debug options
This commit is contained in:
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
// precise frame advancement
|
||||
if h.animTimer >= animSpeed {
|
||||
frameAdvance := int(h.animTimer / animSpeed)
|
||||
if frameAdvance > 0 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
42
internal/maps/desert.go
Normal file
42
internal/maps/desert.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
42
internal/maps/plains.go
Normal file
42
internal/maps/plains.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user