Name change
18
Makefile
@@ -3,10 +3,10 @@
|
|||||||
export CGO_CFLAGS=-Wno-deprecated-declarations
|
export CGO_CFLAGS=-Wno-deprecated-declarations
|
||||||
|
|
||||||
run:
|
run:
|
||||||
@go run ./cmd/bigfeelings
|
@go run ./cmd/lilguy
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@go build -o bin/bigfeelings ./cmd/bigfeelings
|
@go build -o bin/lilguy ./cmd/lilguy
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
@rm -rf bin/
|
@rm -rf bin/
|
||||||
@@ -22,21 +22,21 @@ deps:
|
|||||||
|
|
||||||
# Run with verbose output
|
# Run with verbose output
|
||||||
run-verbose:
|
run-verbose:
|
||||||
@go run -v ./cmd/bigfeelings
|
@go run -v ./cmd/lilguy
|
||||||
|
|
||||||
# Build for current platform
|
# Build for current platform
|
||||||
build-local: clean
|
build-local: clean
|
||||||
@mkdir -p bin
|
@mkdir -p bin
|
||||||
@go build -o bin/bigfeelings ./cmd/bigfeelings
|
@go build -o bin/lilguy ./cmd/lilguy
|
||||||
@echo "Built: bin/bigfeelings"
|
@echo "Built: bin/lilguy"
|
||||||
|
|
||||||
# Build for multiple platforms
|
# Build for multiple platforms
|
||||||
build-all: clean
|
build-all: clean
|
||||||
@mkdir -p bin
|
@mkdir -p bin
|
||||||
GOOS=darwin GOARCH=arm64 go build -o bin/bigfeelings-darwin-arm64 ./cmd/bigfeelings
|
GOOS=darwin GOARCH=arm64 go build -o bin/lilguy-darwin-arm64 ./cmd/lilguy
|
||||||
GOOS=darwin GOARCH=amd64 go build -o bin/bigfeelings-darwin-amd64 ./cmd/bigfeelings
|
GOOS=darwin GOARCH=amd64 go build -o bin/lilguy-darwin-amd64 ./cmd/lilguy
|
||||||
GOOS=linux GOARCH=amd64 go build -o bin/bigfeelings-linux-amd64 ./cmd/bigfeelings
|
GOOS=linux GOARCH=amd64 go build -o bin/lilguy-linux-amd64 ./cmd/lilguy
|
||||||
GOOS=windows GOARCH=amd64 go build -o bin/bigfeelings-windows-amd64.exe ./cmd/bigfeelings
|
GOOS=windows GOARCH=amd64 go build -o bin/lilguy-windows-amd64.exe ./cmd/lilguy
|
||||||
@echo "Built all platforms"
|
@echo "Built all platforms"
|
||||||
|
|
||||||
help:
|
help:
|
||||||
|
|||||||
BIN
assets/hero/a_000.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_001.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_002.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_003.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_004.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_005.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_006.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_007.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_008.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_009.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_010.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_011.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_012.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/hero/a_013.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_014.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_015.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/hero/a_016.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_017.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_018.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_019.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_020.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/hero/a_021.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_022.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_023.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/hero/a_024.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_025.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_026.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
BIN
assets/hero/a_027.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_028.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_029.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_030.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_031.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_032.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_033.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/hero/a_034.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_035.png
Normal file
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/hero/a_036.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_037.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_038.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_039.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_040.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_041.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
assets/hero/a_042.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.2 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
game "github.com/atridad/BigFeelings/internal/game"
|
game "github.com/atridad/LilGuy/internal/game"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
2
go.mod
@@ -1,4 +1,4 @@
|
|||||||
module github.com/atridad/BigFeelings
|
module github.com/atridad/LilGuy
|
||||||
|
|
||||||
go 1.25.4
|
go 1.25.4
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
|
|
||||||
"github.com/atridad/BigFeelings/internal/save"
|
"github.com/atridad/LilGuy/internal/save"
|
||||||
"github.com/atridad/BigFeelings/internal/screens"
|
"github.com/atridad/LilGuy/internal/screens"
|
||||||
"github.com/atridad/BigFeelings/internal/ui/menu"
|
"github.com/atridad/LilGuy/internal/ui/menu"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Window and display configuration.
|
// Window and display configuration.
|
||||||
@@ -17,7 +17,7 @@ const (
|
|||||||
ScreenWidth = 960
|
ScreenWidth = 960
|
||||||
ScreenHeight = 540
|
ScreenHeight = 540
|
||||||
TargetTPS = 60
|
TargetTPS = 60
|
||||||
WindowTitle = "Big Feelings"
|
WindowTitle = "Lil Guy"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Game states define the different screens and modes the game can be in.
|
// Game states define the different screens and modes the game can be in.
|
||||||
@@ -87,18 +87,18 @@ func (f *FPSCap) Cycle() {
|
|||||||
type controls struct {
|
type controls struct {
|
||||||
Left bool
|
Left bool
|
||||||
Right bool
|
Right bool
|
||||||
Up bool
|
Jump bool
|
||||||
Down bool
|
|
||||||
Sprint bool
|
Sprint bool
|
||||||
|
Shoot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func readControls() controls {
|
func readControls() controls {
|
||||||
return controls{
|
return controls{
|
||||||
Left: ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA),
|
Left: ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA),
|
||||||
Right: ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD),
|
Right: ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD),
|
||||||
Up: ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW),
|
Jump: ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW) || ebiten.IsKeyPressed(ebiten.KeySpace),
|
||||||
Down: ebiten.IsKeyPressed(ebiten.KeyArrowDown) || ebiten.IsKeyPressed(ebiten.KeyS),
|
|
||||||
Sprint: ebiten.IsKeyPressed(ebiten.KeyShift),
|
Sprint: ebiten.IsKeyPressed(ebiten.KeyShift),
|
||||||
|
Shoot: inpututil.IsKeyJustPressed(ebiten.KeyK),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -311,14 +311,15 @@ func (g *Game) updatePlaying() error {
|
|||||||
g.state.gameplayScreen.Update(screens.GameplayInput{
|
g.state.gameplayScreen.Update(screens.GameplayInput{
|
||||||
Left: input.Left,
|
Left: input.Left,
|
||||||
Right: input.Right,
|
Right: input.Right,
|
||||||
Up: input.Up,
|
Jump: input.Jump,
|
||||||
Down: input.Down,
|
|
||||||
Sprint: input.Sprint,
|
Sprint: input.Sprint,
|
||||||
|
Shoot: input.Shoot,
|
||||||
}, delta)
|
}, delta)
|
||||||
|
|
||||||
// Periodic auto-save
|
// 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.lastAutoSave = now
|
g.state.lastAutoSave = now
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,6 +344,9 @@ func (g *Game) updatePaused() error {
|
|||||||
case menu.OptionSave:
|
case menu.OptionSave:
|
||||||
// Save game immediately
|
// Save game immediately
|
||||||
g.saveGame()
|
g.saveGame()
|
||||||
|
g.state.gameplayScreen.ShowSaveNotification()
|
||||||
|
g.state.gameState = statePlaying
|
||||||
|
g.state.lastTick = time.Now()
|
||||||
case menu.OptionMainMenu:
|
case menu.OptionMainMenu:
|
||||||
// Save game before returning to main menu
|
// Save game before returning to main menu
|
||||||
g.saveGame()
|
g.saveGame()
|
||||||
|
|||||||
@@ -2,44 +2,51 @@ package hero
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
|
"github.com/atridad/LilGuy/internal/projectile"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Default values and gameplay constants.
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// Default values if not specified in config
|
|
||||||
defaultRadius = 24.0
|
defaultRadius = 24.0
|
||||||
defaultSpeed = 180.0
|
defaultSpeed = 200.0
|
||||||
defaultMaxStamina = 100.0
|
defaultMaxStamina = 100.0
|
||||||
defaultStaminaDrain = 50.0 // Per second when sprinting
|
defaultStaminaDrain = 50.0
|
||||||
defaultStaminaRegen = 30.0 // Per second when not sprinting
|
defaultStaminaRegen = 30.0
|
||||||
|
|
||||||
// Sprint mechanics
|
sprintSpeedMultiplier = 1.8
|
||||||
sprintSpeedMultiplier = 2.0
|
sprintRecoveryThreshold = 0.2
|
||||||
sprintRecoveryThreshold = 0.2 // 20% stamina needed to sprint again
|
|
||||||
|
|
||||||
// Animation
|
normalAnimSpeed = 0.15
|
||||||
normalAnimSpeed = 0.15 // Seconds per frame when walking
|
idleAnimSpeed = 0.3
|
||||||
sprintAnimSpeed = 0.08 // Seconds per frame when sprinting
|
sprintAnimSpeed = 0.08
|
||||||
|
|
||||||
// Visual state thresholds
|
exhaustedThreshold = 0.2
|
||||||
exhaustedThreshold = 0.2 // Show exhausted state below 20% stamina
|
|
||||||
|
animFrameWrap = 4096
|
||||||
|
heroSpriteScale = 0.175
|
||||||
|
fixedSpriteHeight = 329.0
|
||||||
|
fixedSpriteWidth = 315.0
|
||||||
|
|
||||||
|
gravity = 1200.0
|
||||||
|
jumpStrength = -450.0
|
||||||
|
maxFallSpeed = 800.0
|
||||||
|
groundFriction = 0.85
|
||||||
|
airFriction = 0.95
|
||||||
)
|
)
|
||||||
|
|
||||||
type Input struct {
|
type Input struct {
|
||||||
Left bool
|
Left bool
|
||||||
Right bool
|
Right bool
|
||||||
Up bool
|
Jump bool
|
||||||
Down bool
|
|
||||||
Sprint bool
|
Sprint bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type Bounds struct {
|
type Bounds struct {
|
||||||
Width float64
|
Width float64
|
||||||
Height float64
|
Height float64
|
||||||
|
Ground float64
|
||||||
}
|
}
|
||||||
|
|
||||||
type VisualState int
|
type VisualState int
|
||||||
@@ -53,37 +60,38 @@ const (
|
|||||||
type Direction int
|
type Direction int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
DirDown Direction = iota
|
DirLeft Direction = iota
|
||||||
DirUp
|
|
||||||
DirLeft
|
|
||||||
DirRight
|
DirRight
|
||||||
)
|
)
|
||||||
|
|
||||||
type Hero struct {
|
type Hero struct {
|
||||||
// Position and size
|
|
||||||
X float64
|
X float64
|
||||||
Y float64
|
Y float64
|
||||||
Radius float64
|
Radius float64
|
||||||
|
|
||||||
// Movement
|
VelocityX float64
|
||||||
|
VelocityY float64
|
||||||
|
|
||||||
Speed float64
|
Speed float64
|
||||||
|
|
||||||
// Appearance
|
|
||||||
Color color.NRGBA
|
Color color.NRGBA
|
||||||
|
|
||||||
// Stamina system
|
|
||||||
Stamina float64
|
Stamina float64
|
||||||
MaxStamina float64
|
MaxStamina float64
|
||||||
StaminaDrain float64
|
StaminaDrain float64
|
||||||
StaminaRegen float64
|
StaminaRegen float64
|
||||||
|
|
||||||
// Internal state
|
|
||||||
canSprint bool
|
canSprint bool
|
||||||
wasSprintHeld bool
|
wasSprintHeld bool
|
||||||
isSprinting bool
|
isSprinting bool
|
||||||
|
isMoving bool
|
||||||
|
isGrounded bool
|
||||||
direction Direction
|
direction Direction
|
||||||
animFrame int
|
animFrame int
|
||||||
animTimer float64
|
animTimer float64
|
||||||
|
lastAnimKey animationKey
|
||||||
|
|
||||||
|
ProjectileConfig projectile.ProjectileConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type Config struct {
|
type Config struct {
|
||||||
@@ -129,60 +137,80 @@ func New(cfg Config) *Hero {
|
|||||||
StaminaRegen: cfg.StaminaRegen,
|
StaminaRegen: cfg.StaminaRegen,
|
||||||
canSprint: true,
|
canSprint: true,
|
||||||
wasSprintHeld: false,
|
wasSprintHeld: false,
|
||||||
direction: DirDown,
|
direction: DirRight,
|
||||||
animFrame: 0,
|
animFrame: 0,
|
||||||
animTimer: 0,
|
animTimer: 0,
|
||||||
|
lastAnimKey: animationKey{direction: DirRight, state: animIdle},
|
||||||
|
ProjectileConfig: projectile.ProjectileConfig{
|
||||||
|
Speed: 500.0,
|
||||||
|
Radius: 4.0,
|
||||||
|
Color: color.NRGBA{R: 255, G: 0, B: 0, A: 255},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
|
func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
|
||||||
h.updateMovement(input, dt, bounds)
|
h.updateMovement(input, dt, bounds)
|
||||||
h.updateStamina(input, dt)
|
h.updateStamina(input, dt)
|
||||||
h.updateAnimation(input, dt)
|
h.updateAnimation(dt)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
|
func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
|
||||||
dx, dy := 0.0, 0.0
|
h.VelocityY += gravity * dt
|
||||||
|
if h.VelocityY > maxFallSpeed {
|
||||||
|
h.VelocityY = maxFallSpeed
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Y += h.VelocityY * dt
|
||||||
|
|
||||||
|
footPosition := h.Y
|
||||||
|
if footPosition >= bounds.Ground {
|
||||||
|
h.Y = bounds.Ground
|
||||||
|
h.VelocityY = 0
|
||||||
|
h.isGrounded = true
|
||||||
|
} else {
|
||||||
|
h.isGrounded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
if input.Jump && h.isGrounded {
|
||||||
|
h.VelocityY = jumpStrength
|
||||||
|
h.isGrounded = false
|
||||||
|
}
|
||||||
|
|
||||||
|
targetVelocityX := 0.0
|
||||||
if input.Left {
|
if input.Left {
|
||||||
dx -= 1
|
targetVelocityX -= h.Speed
|
||||||
h.direction = DirLeft
|
h.direction = DirLeft
|
||||||
}
|
}
|
||||||
if input.Right {
|
if input.Right {
|
||||||
dx += 1
|
targetVelocityX += h.Speed
|
||||||
h.direction = DirRight
|
h.direction = DirRight
|
||||||
}
|
}
|
||||||
if input.Up {
|
|
||||||
dy -= 1
|
|
||||||
h.direction = DirUp
|
|
||||||
}
|
|
||||||
if input.Down {
|
|
||||||
dy += 1
|
|
||||||
h.direction = DirDown
|
|
||||||
}
|
|
||||||
|
|
||||||
isMoving := dx != 0 || dy != 0
|
h.isMoving = targetVelocityX != 0
|
||||||
|
|
||||||
if isMoving {
|
h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && h.isMoving
|
||||||
length := math.Hypot(dx, dy)
|
|
||||||
dx /= length
|
|
||||||
dy /= length
|
|
||||||
}
|
|
||||||
|
|
||||||
speed := h.Speed
|
|
||||||
h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && isMoving
|
|
||||||
if h.isSprinting {
|
if h.isSprinting {
|
||||||
speed *= sprintSpeedMultiplier
|
targetVelocityX *= sprintSpeedMultiplier
|
||||||
}
|
}
|
||||||
|
|
||||||
h.X += dx * speed * dt
|
friction := groundFriction
|
||||||
h.Y += dy * speed * dt
|
if !h.isGrounded {
|
||||||
|
friction = airFriction
|
||||||
|
}
|
||||||
|
|
||||||
maxX := math.Max(h.Radius, bounds.Width-h.Radius)
|
h.VelocityX = h.VelocityX*friction + targetVelocityX*(1-friction)
|
||||||
maxY := math.Max(h.Radius, bounds.Height-h.Radius)
|
|
||||||
|
|
||||||
h.X = clamp(h.X, h.Radius, maxX)
|
h.X += h.VelocityX * dt
|
||||||
h.Y = clamp(h.Y, h.Radius, maxY)
|
|
||||||
|
if h.X < h.Radius {
|
||||||
|
h.X = h.Radius
|
||||||
|
h.VelocityX = 0
|
||||||
|
}
|
||||||
|
if h.X > bounds.Width-h.Radius {
|
||||||
|
h.X = bounds.Width - h.Radius
|
||||||
|
h.VelocityX = 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hero) updateStamina(input Input, dt float64) {
|
func (h *Hero) updateStamina(input Input, dt float64) {
|
||||||
@@ -208,22 +236,30 @@ func (h *Hero) updateStamina(input Input, dt float64) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hero) updateAnimation(input Input, dt float64) {
|
func (h *Hero) updateAnimation(dt float64) {
|
||||||
isMoving := input.Left || input.Right || input.Up || input.Down
|
isMoving := h.isMoving
|
||||||
|
key := animationKey{direction: h.direction, state: animIdle}
|
||||||
if isMoving {
|
if isMoving {
|
||||||
animSpeed := normalAnimSpeed
|
key.state = animMove
|
||||||
if h.isSprinting {
|
|
||||||
animSpeed = sprintAnimSpeed
|
|
||||||
}
|
}
|
||||||
h.animTimer += dt
|
|
||||||
if h.animTimer >= animSpeed {
|
if key != h.lastAnimKey {
|
||||||
h.animTimer = 0
|
|
||||||
h.animFrame = 1 - h.animFrame
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
h.animFrame = 0
|
h.animFrame = 0
|
||||||
h.animTimer = 0
|
h.animTimer = 0
|
||||||
|
h.lastAnimKey = key
|
||||||
|
}
|
||||||
|
|
||||||
|
if isMoving {
|
||||||
|
animSpeed := normalAnimSpeed * 0.5
|
||||||
|
if h.isSprinting {
|
||||||
|
animSpeed = sprintAnimSpeed * 0.5
|
||||||
|
}
|
||||||
|
h.animTimer += dt
|
||||||
|
frameAdvance := int(h.animTimer / animSpeed)
|
||||||
|
if frameAdvance > 0 {
|
||||||
|
h.animTimer -= animSpeed * float64(frameAdvance)
|
||||||
|
h.animFrame = (h.animFrame + frameAdvance) % animFrameWrap
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,10 +279,13 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
|||||||
sprite := h.getCurrentSprite()
|
sprite := h.getCurrentSprite()
|
||||||
|
|
||||||
if sprite != nil {
|
if sprite != nil {
|
||||||
op := &ebiten.DrawImageOptions{}
|
|
||||||
bounds := sprite.Bounds()
|
bounds := sprite.Bounds()
|
||||||
w, height := float64(bounds.Dx()), float64(bounds.Dy())
|
actualHeight := float64(bounds.Dy())
|
||||||
op.GeoM.Translate(-w/2, -height/2)
|
actualWidth := float64(bounds.Dx())
|
||||||
|
|
||||||
|
op := &ebiten.DrawImageOptions{}
|
||||||
|
op.GeoM.Translate(-actualWidth/2, -actualHeight)
|
||||||
|
op.GeoM.Scale(heroSpriteScale, heroSpriteScale)
|
||||||
op.GeoM.Translate(h.X, h.Y)
|
op.GeoM.Translate(h.X, h.Y)
|
||||||
|
|
||||||
state := h.getVisualState()
|
state := h.getVisualState()
|
||||||
@@ -254,9 +293,7 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
|||||||
case StateExhausted:
|
case StateExhausted:
|
||||||
op.ColorScale.ScaleWithColor(color.RGBA{R: 255, G: 100, B: 100, A: 255})
|
op.ColorScale.ScaleWithColor(color.RGBA{R: 255, G: 100, B: 100, A: 255})
|
||||||
case StateSprinting:
|
case StateSprinting:
|
||||||
// No color change
|
|
||||||
case StateIdle:
|
case StateIdle:
|
||||||
// No color change
|
|
||||||
}
|
}
|
||||||
|
|
||||||
screen.DrawImage(sprite, op)
|
screen.DrawImage(sprite, op)
|
||||||
@@ -264,44 +301,9 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Hero) getCurrentSprite() *ebiten.Image {
|
func (h *Hero) getCurrentSprite() *ebiten.Image {
|
||||||
var sprite *ebiten.Image
|
return getKnightSprite(h.direction, h.isMoving, h.animFrame)
|
||||||
|
|
||||||
switch h.direction {
|
|
||||||
case DirUp:
|
|
||||||
if h.animFrame == 0 {
|
|
||||||
sprite = spriteBack1
|
|
||||||
} else {
|
|
||||||
sprite = spriteBack2
|
|
||||||
}
|
|
||||||
case DirDown:
|
|
||||||
if h.animFrame == 0 {
|
|
||||||
sprite = spriteFront1
|
|
||||||
} else {
|
|
||||||
sprite = spriteFront2
|
|
||||||
}
|
|
||||||
case DirLeft:
|
|
||||||
if h.animFrame == 0 {
|
|
||||||
sprite = spriteLeft1
|
|
||||||
} else {
|
|
||||||
sprite = spriteLeft2
|
|
||||||
}
|
|
||||||
case DirRight:
|
|
||||||
if h.animFrame == 0 {
|
|
||||||
sprite = spriteRight1
|
|
||||||
} else {
|
|
||||||
sprite = spriteRight2
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return sprite
|
func (h *Hero) GetDirection() Direction {
|
||||||
}
|
return h.direction
|
||||||
|
|
||||||
func clamp(value, min, max float64) float64 {
|
|
||||||
if value < min {
|
|
||||||
return min
|
|
||||||
}
|
|
||||||
if value > max {
|
|
||||||
return max
|
|
||||||
}
|
|
||||||
return value
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +1,151 @@
|
|||||||
package hero
|
package hero
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
_ "image/png"
|
||||||
_ "image/gif"
|
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
heroDir = "assets/hero"
|
||||||
|
)
|
||||||
|
|
||||||
|
type animState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
animIdle animState = iota
|
||||||
|
animMove
|
||||||
|
)
|
||||||
|
|
||||||
|
type animationKey struct {
|
||||||
|
direction Direction
|
||||||
|
state animState
|
||||||
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
spriteBack1 *ebiten.Image
|
knightAnimations map[animationKey][]*ebiten.Image
|
||||||
spriteBack2 *ebiten.Image
|
|
||||||
spriteFront1 *ebiten.Image
|
|
||||||
spriteFront2 *ebiten.Image
|
|
||||||
spriteLeft1 *ebiten.Image
|
|
||||||
spriteLeft2 *ebiten.Image
|
|
||||||
spriteRight1 *ebiten.Image
|
|
||||||
spriteRight2 *ebiten.Image
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
spriteBack1 = loadSprite("assets/hero/avt1_bk1.gif")
|
knightAnimations = make(map[animationKey][]*ebiten.Image)
|
||||||
spriteBack2 = loadSprite("assets/hero/avt1_bk2.gif")
|
|
||||||
spriteFront1 = loadSprite("assets/hero/avt1_fr1.gif")
|
|
||||||
spriteFront2 = loadSprite("assets/hero/avt1_fr2.gif")
|
|
||||||
spriteLeft1 = loadSprite("assets/hero/avt1_lf1.gif")
|
|
||||||
spriteLeft2 = loadSprite("assets/hero/avt1_lf2.gif")
|
|
||||||
spriteRight1 = loadSprite("assets/hero/avt1_rt1.gif")
|
|
||||||
spriteRight2 = loadSprite("assets/hero/avt1_rt2.gif")
|
|
||||||
}
|
|
||||||
|
|
||||||
func loadSprite(path string) *ebiten.Image {
|
if err := loadKnightAnimations(); err != nil {
|
||||||
file, err := os.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
img, _, err := image.Decode(file)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getKnightSprite(direction Direction, moving bool, frameIndex int) *ebiten.Image {
|
||||||
|
state := animIdle
|
||||||
|
if moving {
|
||||||
|
state = animMove
|
||||||
|
}
|
||||||
|
return frameFromSet(direction, state, frameIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadKnightAnimations() error {
|
||||||
|
frames, err := loadAnimationFrames(heroDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
knightAnimations[animationKey{DirLeft, animMove}] = frames
|
||||||
|
knightAnimations[animationKey{DirLeft, animIdle}] = frames
|
||||||
|
|
||||||
|
flippedFrames := make([]*ebiten.Image, len(frames))
|
||||||
|
for i, frame := range frames {
|
||||||
|
flippedFrames[i] = flipImageHorizontally(frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
knightAnimations[animationKey{DirRight, animMove}] = flippedFrames
|
||||||
|
knightAnimations[animationKey{DirRight, animIdle}] = flippedFrames
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadAnimationFrames(dir string) ([]*ebiten.Image, error) {
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var files []string
|
||||||
|
for _, entry := range entries {
|
||||||
|
if entry.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
name := entry.Name()
|
||||||
|
if filepath.Ext(name) == ".png" {
|
||||||
|
files = append(files, filepath.Join(dir, name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(files)
|
||||||
|
|
||||||
|
var frames []*ebiten.Image
|
||||||
|
for _, file := range files {
|
||||||
|
img, err := loadImage(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("load frame %s: %w", file, err)
|
||||||
|
}
|
||||||
|
frames = append(frames, img)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frames, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadImage(path string) (*ebiten.Image, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
img, _, err := image.Decode(f)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ebiten.NewImageFromImage(img), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func flipImageHorizontally(img *ebiten.Image) *ebiten.Image {
|
||||||
bounds := img.Bounds()
|
bounds := img.Bounds()
|
||||||
rgba := image.NewRGBA(bounds)
|
w, h := bounds.Dx(), bounds.Dy()
|
||||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
|
||||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
flipped := ebiten.NewImage(w, h)
|
||||||
r, g, b, a := img.At(x, y).RGBA()
|
|
||||||
if r > 0xf000 && g > 0xf000 && b > 0xf000 {
|
op := &ebiten.DrawImageOptions{}
|
||||||
rgba.Set(x, y, color.RGBA{0, 0, 0, 0})
|
op.GeoM.Scale(-1, 1)
|
||||||
} else {
|
op.GeoM.Translate(float64(w), 0)
|
||||||
rgba.Set(x, y, color.RGBA{
|
|
||||||
R: uint8(r >> 8),
|
flipped.DrawImage(img, op)
|
||||||
G: uint8(g >> 8),
|
|
||||||
B: uint8(b >> 8),
|
return flipped
|
||||||
A: uint8(a >> 8),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func frameFromSet(direction Direction, state animState, frameIndex int) *ebiten.Image {
|
||||||
|
if frameIndex < 0 {
|
||||||
|
frameIndex = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if frames := knightAnimations[animationKey{direction, state}]; len(frames) > 0 {
|
||||||
|
return frames[frameIndex%len(frames)]
|
||||||
|
}
|
||||||
|
|
||||||
|
if state == animMove {
|
||||||
|
if frames := knightAnimations[animationKey{direction, animIdle}]; len(frames) > 0 {
|
||||||
|
return frames[frameIndex%len(frames)]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if frames := knightAnimations[animationKey{direction, animMove}]; len(frames) > 0 {
|
||||||
|
return frames[frameIndex%len(frames)]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ebiten.NewImageFromImage(rgba)
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
95
internal/projectile/projectile.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package projectile
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ProjectileConfig struct {
|
||||||
|
Speed float64
|
||||||
|
Radius float64
|
||||||
|
Color color.NRGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
type Projectile struct {
|
||||||
|
X float64
|
||||||
|
Y float64
|
||||||
|
VelocityX float64
|
||||||
|
VelocityY float64
|
||||||
|
Radius float64
|
||||||
|
Color color.NRGBA
|
||||||
|
Active bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(x, y, directionX, directionY float64, config ProjectileConfig) *Projectile {
|
||||||
|
return &Projectile{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
VelocityX: directionX * config.Speed,
|
||||||
|
VelocityY: directionY * config.Speed,
|
||||||
|
Radius: config.Radius,
|
||||||
|
Color: config.Color,
|
||||||
|
Active: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Projectile) Update(dt float64, screenWidth, screenHeight float64) {
|
||||||
|
p.X += p.VelocityX * dt
|
||||||
|
p.Y += p.VelocityY * dt
|
||||||
|
|
||||||
|
if p.X < 0 || p.X > screenWidth || p.Y < 0 || p.Y > screenHeight {
|
||||||
|
p.Active = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Projectile) Draw(screen *ebiten.Image) {
|
||||||
|
if !p.Active {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
vector.DrawFilledCircle(
|
||||||
|
screen,
|
||||||
|
float32(p.X),
|
||||||
|
float32(p.Y),
|
||||||
|
float32(p.Radius),
|
||||||
|
p.Color,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manager struct {
|
||||||
|
Projectiles []*Projectile
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewManager() *Manager {
|
||||||
|
return &Manager{
|
||||||
|
Projectiles: make([]*Projectile, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Shoot(x, y, directionX float64, config ProjectileConfig) {
|
||||||
|
p := New(x, y, directionX, 0, config)
|
||||||
|
m.Projectiles = append(m.Projectiles, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Update(dt float64, screenWidth, screenHeight float64) {
|
||||||
|
for i := len(m.Projectiles) - 1; i >= 0; i-- {
|
||||||
|
p := m.Projectiles[i]
|
||||||
|
p.Update(dt, screenWidth, screenHeight)
|
||||||
|
|
||||||
|
if !p.Active {
|
||||||
|
m.Projectiles = append(m.Projectiles[:i], m.Projectiles[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Draw(screen *ebiten.Image) {
|
||||||
|
for _, p := range m.Projectiles {
|
||||||
|
p.Draw(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Manager) Clear() {
|
||||||
|
m.Projectiles = make([]*Projectile, 0)
|
||||||
|
}
|
||||||
@@ -6,26 +6,32 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
|
||||||
"github.com/atridad/BigFeelings/internal/hero"
|
"github.com/atridad/LilGuy/internal/hero"
|
||||||
"github.com/atridad/BigFeelings/internal/save"
|
"github.com/atridad/LilGuy/internal/projectile"
|
||||||
"github.com/atridad/BigFeelings/internal/status"
|
"github.com/atridad/LilGuy/internal/save"
|
||||||
"github.com/atridad/BigFeelings/internal/ui/hud"
|
"github.com/atridad/LilGuy/internal/status"
|
||||||
|
"github.com/atridad/LilGuy/internal/ui/hud"
|
||||||
|
"github.com/atridad/LilGuy/internal/world"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
backgroundColor = color.NRGBA{R: 0, G: 0, B: 0, A: 255}
|
backgroundColor = color.NRGBA{R: 135, G: 206, B: 235, A: 255}
|
||||||
|
saveNotificationColor = color.NRGBA{R: 50, G: 200, B: 50, A: 255}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Hero settings.
|
|
||||||
const (
|
const (
|
||||||
heroStartX = 960 / 2 // ScreenWidth / 2
|
heroStartX = 960 / 2
|
||||||
heroStartY = 540 / 2 // ScreenHeight / 2
|
heroStartY = 540 / 2
|
||||||
heroRadius = 28.0
|
heroRadius = 28.0
|
||||||
heroSpeed = 180.0
|
heroSpeed = 180.0
|
||||||
heroMaxStamina = 100.0
|
heroMaxStamina = 100.0
|
||||||
heroStaminaDrain = 50.0
|
heroStaminaDrain = 50.0
|
||||||
heroStaminaRegen = 30.0
|
heroStaminaRegen = 30.0
|
||||||
|
|
||||||
|
saveNotificationDuration = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -59,9 +65,9 @@ const (
|
|||||||
type GameplayInput struct {
|
type GameplayInput struct {
|
||||||
Left bool
|
Left bool
|
||||||
Right bool
|
Right bool
|
||||||
Up bool
|
Jump bool
|
||||||
Down bool
|
|
||||||
Sprint bool
|
Sprint bool
|
||||||
|
Shoot bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Manages the main gameplay state including the hero, HUD, and game world.
|
// Manages the main gameplay state including the hero, HUD, and game world.
|
||||||
@@ -69,6 +75,8 @@ type GameplayInput struct {
|
|||||||
type GameplayScreen struct {
|
type GameplayScreen struct {
|
||||||
hero *hero.Hero
|
hero *hero.Hero
|
||||||
hud hud.Overlay
|
hud hud.Overlay
|
||||||
|
world *world.World
|
||||||
|
projectiles *projectile.Manager
|
||||||
bounds hero.Bounds
|
bounds hero.Bounds
|
||||||
lastTick time.Time
|
lastTick time.Time
|
||||||
gameStartTime time.Time
|
gameStartTime time.Time
|
||||||
@@ -77,10 +85,25 @@ type GameplayScreen struct {
|
|||||||
fpsFrames int
|
fpsFrames int
|
||||||
fpsAccumulator time.Duration
|
fpsAccumulator time.Duration
|
||||||
fpsValue float64
|
fpsValue float64
|
||||||
|
|
||||||
|
saveNotificationTimer time.Duration
|
||||||
|
showSaveNotification bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new gameplay screen instance.
|
// 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()
|
||||||
|
|
||||||
|
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{
|
return &GameplayScreen{
|
||||||
hero: hero.New(hero.Config{
|
hero: hero.New(hero.Config{
|
||||||
StartX: heroStartX,
|
StartX: heroStartX,
|
||||||
@@ -97,9 +120,12 @@ func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *Gamepla
|
|||||||
Y: hudY,
|
Y: hudY,
|
||||||
Color: color.White,
|
Color: color.White,
|
||||||
},
|
},
|
||||||
|
world: w,
|
||||||
|
projectiles: projectile.NewManager(),
|
||||||
bounds: hero.Bounds{
|
bounds: hero.Bounds{
|
||||||
Width: float64(screenWidth),
|
Width: float64(screenWidth),
|
||||||
Height: float64(screenHeight),
|
Height: float64(screenHeight),
|
||||||
|
Ground: float64(screenHeight) - groundHeight,
|
||||||
},
|
},
|
||||||
lastTick: time.Now(),
|
lastTick: time.Now(),
|
||||||
gameStartTime: time.Now(),
|
gameStartTime: time.Now(),
|
||||||
@@ -114,15 +140,30 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
|||||||
g.hero.Update(hero.Input{
|
g.hero.Update(hero.Input{
|
||||||
Left: input.Left,
|
Left: input.Left,
|
||||||
Right: input.Right,
|
Right: input.Right,
|
||||||
Up: input.Up,
|
Jump: input.Jump,
|
||||||
Down: input.Down,
|
|
||||||
Sprint: input.Sprint,
|
Sprint: input.Sprint,
|
||||||
}, dt, g.bounds)
|
}, dt, g.bounds)
|
||||||
|
|
||||||
// Track total play time
|
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.totalPlayTime += delta
|
||||||
|
|
||||||
g.trackFPS(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.
|
// Calculates the current FPS if FPS monitoring is enabled.
|
||||||
@@ -144,6 +185,9 @@ func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
|||||||
// Renders the gameplay screen.
|
// Renders the gameplay screen.
|
||||||
func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
||||||
screen.Fill(backgroundColor)
|
screen.Fill(backgroundColor)
|
||||||
|
|
||||||
|
g.world.Draw(screen)
|
||||||
|
g.projectiles.Draw(screen)
|
||||||
g.hero.Draw(screen)
|
g.hero.Draw(screen)
|
||||||
|
|
||||||
staminaColor := staminaNormalColor
|
staminaColor := staminaNormalColor
|
||||||
@@ -181,10 +225,63 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g.hud.Draw(screen, meters)
|
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.
|
// Resets the gameplay screen to its initial state.
|
||||||
func (g *GameplayScreen) Reset() {
|
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{
|
g.hero = hero.New(hero.Config{
|
||||||
StartX: heroStartX,
|
StartX: heroStartX,
|
||||||
StartY: heroStartY,
|
StartY: heroStartY,
|
||||||
@@ -195,6 +292,13 @@ func (g *GameplayScreen) Reset() {
|
|||||||
StaminaDrain: heroStaminaDrain,
|
StaminaDrain: heroStaminaDrain,
|
||||||
StaminaRegen: heroStaminaRegen,
|
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.lastTick = time.Now()
|
||||||
g.gameStartTime = time.Now()
|
g.gameStartTime = time.Now()
|
||||||
g.totalPlayTime = 0
|
g.totalPlayTime = 0
|
||||||
@@ -215,6 +319,20 @@ func (g *GameplayScreen) SaveState() *save.GameState {
|
|||||||
|
|
||||||
// LoadState restores gameplay state from saved data.
|
// 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)
|
||||||
|
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{
|
g.hero = hero.New(hero.Config{
|
||||||
StartX: state.HeroX,
|
StartX: state.HeroX,
|
||||||
StartY: state.HeroY,
|
StartY: state.HeroY,
|
||||||
@@ -226,6 +344,13 @@ func (g *GameplayScreen) LoadState(state *save.GameState) {
|
|||||||
StaminaRegen: heroStaminaRegen,
|
StaminaRegen: heroStaminaRegen,
|
||||||
})
|
})
|
||||||
g.hero.Stamina = state.HeroStamina
|
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.totalPlayTime = time.Duration(state.PlayTimeMS) * time.Millisecond
|
||||||
g.lastTick = time.Now()
|
g.lastTick = time.Now()
|
||||||
g.gameStartTime = time.Now()
|
g.gameStartTime = time.Now()
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw large game title
|
// Draw large game title
|
||||||
titleText := "BIG FEELINGS"
|
titleText := "LIL GUY"
|
||||||
|
|
||||||
// Calculate size for large text (scale up the basic font)
|
// Calculate size for large text (scale up the basic font)
|
||||||
scale := 4.0
|
scale := 4.0
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func (t *TitleScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
|
|||||||
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
|
// Draw game title
|
||||||
titleText := "BIG FEELINGS"
|
titleText := "LIL GUY"
|
||||||
scale := 3.0
|
scale := 3.0
|
||||||
charWidth := 7.0 * scale
|
charWidth := 7.0 * scale
|
||||||
textWidth := float64(len(titleText)) * charWidth
|
textWidth := float64(len(titleText)) * charWidth
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||||
"golang.org/x/image/font/basicfont"
|
"golang.org/x/image/font/basicfont"
|
||||||
|
|
||||||
"github.com/atridad/BigFeelings/internal/status"
|
"github.com/atridad/LilGuy/internal/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
|
||||||
"github.com/atridad/BigFeelings/internal/status"
|
"github.com/atridad/LilGuy/internal/status"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HUD overlay anchor.
|
// HUD overlay anchor.
|
||||||
@@ -24,7 +24,7 @@ func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
|||||||
// Instruction text
|
// Instruction text
|
||||||
instructions := Column{
|
instructions := Column{
|
||||||
Elements: []Element{
|
Elements: []Element{
|
||||||
Label{Text: "Big Feelings", Color: o.Color},
|
Label{Text: "Lil Guy", Color: o.Color},
|
||||||
Label{Text: "Move with Arrow Keys / WASD", Color: o.Color},
|
Label{Text: "Move with Arrow Keys / WASD", Color: o.Color},
|
||||||
Label{Text: "Hold Shift to Sprint", Color: o.Color}},
|
Label{Text: "Hold Shift to Sprint", Color: o.Color}},
|
||||||
Spacing: 7,
|
Spacing: 7,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
"golang.org/x/image/font/basicfont"
|
"golang.org/x/image/font/basicfont"
|
||||||
|
|
||||||
"github.com/atridad/BigFeelings/internal/screens"
|
"github.com/atridad/LilGuy/internal/screens"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
121
internal/world/surface.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package world
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SurfaceTag int
|
||||||
|
|
||||||
|
const (
|
||||||
|
TagGround SurfaceTag = iota
|
||||||
|
TagWall
|
||||||
|
TagPlatform
|
||||||
|
TagHazard
|
||||||
|
)
|
||||||
|
|
||||||
|
type Surface struct {
|
||||||
|
X float64
|
||||||
|
Y float64
|
||||||
|
Width float64
|
||||||
|
Height float64
|
||||||
|
Tag SurfaceTag
|
||||||
|
Color color.NRGBA
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Surface) Bounds() (x, y, width, height float64) {
|
||||||
|
return s.X, s.Y, s.Width, s.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Surface) Contains(x, y float64) bool {
|
||||||
|
return x >= s.X && x <= s.X+s.Width && y >= s.Y && y <= s.Y+s.Height
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Surface) IsSolid() bool {
|
||||||
|
switch s.Tag {
|
||||||
|
case TagGround, TagWall, TagPlatform:
|
||||||
|
return true
|
||||||
|
case TagHazard:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Surface) IsWalkable() bool {
|
||||||
|
switch s.Tag {
|
||||||
|
case TagGround, TagPlatform:
|
||||||
|
return true
|
||||||
|
case TagWall, TagHazard:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Surface) Draw(screen *ebiten.Image) {
|
||||||
|
vector.DrawFilledRect(
|
||||||
|
screen,
|
||||||
|
float32(s.X),
|
||||||
|
float32(s.Y),
|
||||||
|
float32(s.Width),
|
||||||
|
float32(s.Height),
|
||||||
|
s.Color,
|
||||||
|
false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type World struct {
|
||||||
|
Surfaces []*Surface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWorld() *World {
|
||||||
|
return &World{
|
||||||
|
Surfaces: make([]*Surface, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *World) AddSurface(surface *Surface) {
|
||||||
|
w.Surfaces = append(w.Surfaces, surface)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *World) GetSurfacesWithTag(tag SurfaceTag) []*Surface {
|
||||||
|
var result []*Surface
|
||||||
|
for _, s := range w.Surfaces {
|
||||||
|
if s.Tag == tag {
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *World) GetSurfacesAt(x, y float64) []*Surface {
|
||||||
|
var result []*Surface
|
||||||
|
for _, s := range w.Surfaces {
|
||||||
|
if s.Contains(x, y) {
|
||||||
|
result = append(result, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *World) CheckCollision(x, y, width, height float64) *Surface {
|
||||||
|
for _, s := range w.Surfaces {
|
||||||
|
if !s.IsSolid() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if x+width > s.X && x < s.X+s.Width &&
|
||||||
|
y+height > s.Y && y < s.Y+s.Height {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *World) Draw(screen *ebiten.Image) {
|
||||||
|
for _, s := range w.Surfaces {
|
||||||
|
s.Draw(screen)
|
||||||
|
}
|
||||||
|
}
|
||||||