361 lines
8.5 KiB
Go
361 lines
8.5 KiB
Go
package screens
|
|
|
|
import (
|
|
"fmt"
|
|
"image/color"
|
|
"time"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2"
|
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
|
|
|
"github.com/atridad/LilGuy/internal/hero"
|
|
"github.com/atridad/LilGuy/internal/projectile"
|
|
"github.com/atridad/LilGuy/internal/save"
|
|
"github.com/atridad/LilGuy/internal/status"
|
|
"github.com/atridad/LilGuy/internal/ui/hud"
|
|
"github.com/atridad/LilGuy/internal/world"
|
|
)
|
|
|
|
var (
|
|
backgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255}
|
|
saveNotificationColor = color.NRGBA{R: 50, G: 200, B: 50, A: 255}
|
|
)
|
|
|
|
const (
|
|
heroStartX = 960 / 2
|
|
heroStartY = 540 / 2
|
|
heroRadius = 28.0
|
|
heroSpeed = 180.0
|
|
heroMaxStamina = 100.0
|
|
heroStaminaDrain = 50.0
|
|
heroStaminaRegen = 30.0
|
|
|
|
saveNotificationDuration = 2 * time.Second
|
|
)
|
|
|
|
var (
|
|
heroColor = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
|
|
)
|
|
|
|
// HUD settings.
|
|
const (
|
|
hudX = 960 - 220 // ScreenWidth - 220
|
|
hudY = 20
|
|
)
|
|
|
|
// HUD colors.
|
|
var (
|
|
staminaNormalColor = color.NRGBA{R: 0, G: 255, B: 180, A: 255}
|
|
staminaLowColor = color.NRGBA{R: 255, G: 60, B: 60, A: 255}
|
|
fpsGoodColor = color.NRGBA{R: 120, G: 255, B: 120, A: 255}
|
|
fpsWarnColor = color.NRGBA{R: 255, G: 210, B: 100, A: 255}
|
|
fpsPoorColor = color.NRGBA{R: 255, G: 120, B: 120, A: 255}
|
|
)
|
|
|
|
const (
|
|
staminaLowThreshold = 0.2
|
|
fpsWarnThreshold = 0.85
|
|
fpsPoorThreshold = 0.6
|
|
fpsSampleWindow = time.Second
|
|
targetTPS = 60
|
|
)
|
|
|
|
// Player input for the gameplay screen.
|
|
type GameplayInput struct {
|
|
Left bool
|
|
Right bool
|
|
Jump bool
|
|
Sprint 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 {
|
|
hero *hero.Hero
|
|
hud hud.Overlay
|
|
world *world.World
|
|
projectiles *projectile.Manager
|
|
bounds hero.Bounds
|
|
lastTick time.Time
|
|
gameStartTime time.Time
|
|
totalPlayTime time.Duration
|
|
fpsEnabled *bool
|
|
fpsFrames int
|
|
fpsAccumulator time.Duration
|
|
fpsValue float64
|
|
|
|
saveNotificationTimer time.Duration
|
|
showSaveNotification bool
|
|
}
|
|
|
|
// Creates a new gameplay screen instance.
|
|
func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *GameplayScreen {
|
|
w := world.NewWorld()
|
|
|
|
groundHeight := 16.0
|
|
w.AddSurface(&world.Surface{
|
|
X: 0,
|
|
Y: float64(screenHeight) - groundHeight,
|
|
Width: float64(screenWidth),
|
|
Height: groundHeight,
|
|
Tag: world.TagGround,
|
|
Color: color.NRGBA{R: 34, G: 139, B: 34, A: 255},
|
|
})
|
|
|
|
return &GameplayScreen{
|
|
hero: hero.New(hero.Config{
|
|
StartX: heroStartX,
|
|
StartY: heroStartY,
|
|
Radius: heroRadius,
|
|
Speed: heroSpeed,
|
|
Color: heroColor,
|
|
MaxStamina: heroMaxStamina,
|
|
StaminaDrain: heroStaminaDrain,
|
|
StaminaRegen: heroStaminaRegen,
|
|
}),
|
|
hud: hud.Overlay{
|
|
X: hudX,
|
|
Y: hudY,
|
|
Color: color.White,
|
|
},
|
|
world: w,
|
|
projectiles: projectile.NewManager(),
|
|
bounds: hero.Bounds{
|
|
Width: float64(screenWidth),
|
|
Height: float64(screenHeight),
|
|
Ground: float64(screenHeight) - groundHeight,
|
|
},
|
|
lastTick: time.Now(),
|
|
gameStartTime: time.Now(),
|
|
fpsEnabled: fpsEnabled,
|
|
}
|
|
}
|
|
|
|
// Processes gameplay logic with the given input and delta time.
|
|
func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
|
dt := delta.Seconds()
|
|
|
|
g.hero.Update(hero.Input{
|
|
Left: input.Left,
|
|
Right: input.Right,
|
|
Jump: input.Jump,
|
|
Sprint: input.Sprint,
|
|
}, dt, g.bounds)
|
|
|
|
if input.Shoot {
|
|
direction := 1.0
|
|
if g.hero.GetDirection() == hero.DirLeft {
|
|
direction = -1.0
|
|
}
|
|
g.projectiles.Shoot(g.hero.X, g.hero.Y-20, direction, g.hero.ProjectileConfig)
|
|
}
|
|
|
|
g.projectiles.Update(dt, g.bounds.Width, g.bounds.Height)
|
|
|
|
g.totalPlayTime += delta
|
|
|
|
g.trackFPS(delta)
|
|
|
|
if g.showSaveNotification {
|
|
g.saveNotificationTimer -= delta
|
|
if g.saveNotificationTimer <= 0 {
|
|
g.showSaveNotification = false
|
|
}
|
|
}
|
|
}
|
|
|
|
// Calculates the current FPS if FPS monitoring is enabled.
|
|
func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
|
if g.fpsEnabled == nil || !*g.fpsEnabled {
|
|
return
|
|
}
|
|
|
|
g.fpsAccumulator += delta
|
|
g.fpsFrames++
|
|
|
|
if g.fpsAccumulator >= fpsSampleWindow {
|
|
g.fpsValue = float64(g.fpsFrames) / g.fpsAccumulator.Seconds()
|
|
g.fpsAccumulator = 0
|
|
g.fpsFrames = 0
|
|
}
|
|
}
|
|
|
|
// Renders the gameplay screen.
|
|
func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
|
screen.Fill(backgroundColor)
|
|
|
|
g.world.Draw(screen)
|
|
g.projectiles.Draw(screen)
|
|
g.hero.Draw(screen)
|
|
|
|
staminaColor := staminaNormalColor
|
|
if g.hero.Stamina < g.hero.MaxStamina*staminaLowThreshold {
|
|
staminaColor = staminaLowColor
|
|
}
|
|
|
|
staminaMeter := status.Meter{
|
|
Label: "Stamina",
|
|
Base: g.hero.MaxStamina,
|
|
Level: g.hero.Stamina,
|
|
Color: staminaColor,
|
|
}
|
|
|
|
meters := []status.Meter{staminaMeter}
|
|
|
|
if g.fpsEnabled != nil && *g.fpsEnabled {
|
|
// Color based on target FPS (60).
|
|
ratio := g.fpsValue / float64(targetTPS)
|
|
fpsColor := fpsGoodColor
|
|
switch {
|
|
case ratio < fpsPoorThreshold:
|
|
fpsColor = fpsPoorColor
|
|
case ratio < fpsWarnThreshold:
|
|
fpsColor = fpsWarnColor
|
|
}
|
|
|
|
fpsMeter := status.Meter{
|
|
Label: fmt.Sprintf("Framerate: %3.0f FPS", g.fpsValue),
|
|
Base: -1, // Negative base means text-only display.
|
|
Level: 0,
|
|
Color: fpsColor,
|
|
}
|
|
meters = append(meters, fpsMeter)
|
|
}
|
|
|
|
g.hud.Draw(screen, meters)
|
|
|
|
if g.showSaveNotification {
|
|
g.drawSaveNotification(screen)
|
|
}
|
|
}
|
|
|
|
func (g *GameplayScreen) drawSaveNotification(screen *ebiten.Image) {
|
|
centerX := float32(g.bounds.Width / 2)
|
|
centerY := float32(30)
|
|
|
|
boxWidth := float32(140)
|
|
boxHeight := float32(40)
|
|
|
|
vector.DrawFilledRect(screen,
|
|
centerX-boxWidth/2,
|
|
centerY-boxHeight/2,
|
|
boxWidth,
|
|
boxHeight,
|
|
color.NRGBA{R: 0, G: 0, B: 0, A: 180},
|
|
false)
|
|
|
|
vector.StrokeRect(screen,
|
|
centerX-boxWidth/2,
|
|
centerY-boxHeight/2,
|
|
boxWidth,
|
|
boxHeight,
|
|
2,
|
|
saveNotificationColor,
|
|
false)
|
|
|
|
msg := "Game Saved!"
|
|
textX := centerX - 45
|
|
textY := centerY - 5
|
|
ebitenutil.DebugPrintAt(screen, msg, int(textX), int(textY))
|
|
}
|
|
|
|
func (g *GameplayScreen) ShowSaveNotification() {
|
|
g.showSaveNotification = true
|
|
g.saveNotificationTimer = saveNotificationDuration
|
|
}
|
|
|
|
// Resets the gameplay screen to its initial state.
|
|
func (g *GameplayScreen) Reset() {
|
|
screenWidth := int(g.bounds.Width)
|
|
screenHeight := int(g.bounds.Height)
|
|
groundHeight := 16.0
|
|
|
|
w := world.NewWorld()
|
|
w.AddSurface(&world.Surface{
|
|
X: 0,
|
|
Y: float64(screenHeight) - groundHeight,
|
|
Width: float64(screenWidth),
|
|
Height: groundHeight,
|
|
Tag: world.TagGround,
|
|
Color: color.NRGBA{R: 34, G: 139, B: 34, A: 255},
|
|
})
|
|
|
|
g.hero = hero.New(hero.Config{
|
|
StartX: heroStartX,
|
|
StartY: heroStartY,
|
|
Radius: heroRadius,
|
|
Speed: heroSpeed,
|
|
Color: heroColor,
|
|
MaxStamina: heroMaxStamina,
|
|
StaminaDrain: heroStaminaDrain,
|
|
StaminaRegen: heroStaminaRegen,
|
|
})
|
|
g.world = w
|
|
g.projectiles = projectile.NewManager()
|
|
g.bounds = hero.Bounds{
|
|
Width: float64(screenWidth),
|
|
Height: float64(screenHeight),
|
|
Ground: float64(screenHeight) - groundHeight,
|
|
}
|
|
g.lastTick = time.Now()
|
|
g.gameStartTime = time.Now()
|
|
g.totalPlayTime = 0
|
|
g.fpsFrames = 0
|
|
g.fpsAccumulator = 0
|
|
g.fpsValue = 0
|
|
}
|
|
|
|
// SaveState converts the current gameplay state to a saveable format.
|
|
func (g *GameplayScreen) SaveState() *save.GameState {
|
|
return &save.GameState{
|
|
HeroX: g.hero.X,
|
|
HeroY: g.hero.Y,
|
|
HeroStamina: g.hero.Stamina,
|
|
PlayTimeMS: g.totalPlayTime.Milliseconds(),
|
|
}
|
|
}
|
|
|
|
// LoadState restores gameplay state from saved data.
|
|
func (g *GameplayScreen) LoadState(state *save.GameState) {
|
|
screenWidth := int(g.bounds.Width)
|
|
screenHeight := int(g.bounds.Height)
|
|
groundHeight := 16.0
|
|
|
|
w := world.NewWorld()
|
|
w.AddSurface(&world.Surface{
|
|
X: 0,
|
|
Y: float64(screenHeight) - groundHeight,
|
|
Width: float64(screenWidth),
|
|
Height: groundHeight,
|
|
Tag: world.TagGround,
|
|
Color: color.NRGBA{R: 34, G: 139, B: 34, A: 255},
|
|
})
|
|
|
|
g.hero = hero.New(hero.Config{
|
|
StartX: state.HeroX,
|
|
StartY: state.HeroY,
|
|
Radius: heroRadius,
|
|
Speed: heroSpeed,
|
|
Color: heroColor,
|
|
MaxStamina: heroMaxStamina,
|
|
StaminaDrain: heroStaminaDrain,
|
|
StaminaRegen: heroStaminaRegen,
|
|
})
|
|
g.hero.Stamina = state.HeroStamina
|
|
g.world = w
|
|
g.projectiles = projectile.NewManager()
|
|
g.bounds = hero.Bounds{
|
|
Width: float64(screenWidth),
|
|
Height: float64(screenHeight),
|
|
Ground: float64(screenHeight) - groundHeight,
|
|
}
|
|
g.totalPlayTime = time.Duration(state.PlayTimeMS) * time.Millisecond
|
|
g.lastTick = time.Now()
|
|
g.gameStartTime = time.Now()
|
|
g.fpsFrames = 0
|
|
g.fpsAccumulator = 0
|
|
g.fpsValue = 0
|
|
}
|