Name change
18
Makefile
@@ -3,10 +3,10 @@
|
||||
export CGO_CFLAGS=-Wno-deprecated-declarations
|
||||
|
||||
run:
|
||||
@go run ./cmd/bigfeelings
|
||||
@go run ./cmd/lilguy
|
||||
|
||||
build:
|
||||
@go build -o bin/bigfeelings ./cmd/bigfeelings
|
||||
@go build -o bin/lilguy ./cmd/lilguy
|
||||
|
||||
clean:
|
||||
@rm -rf bin/
|
||||
@@ -22,21 +22,21 @@ deps:
|
||||
|
||||
# Run with verbose output
|
||||
run-verbose:
|
||||
@go run -v ./cmd/bigfeelings
|
||||
@go run -v ./cmd/lilguy
|
||||
|
||||
# Build for current platform
|
||||
build-local: clean
|
||||
@mkdir -p bin
|
||||
@go build -o bin/bigfeelings ./cmd/bigfeelings
|
||||
@echo "Built: bin/bigfeelings"
|
||||
@go build -o bin/lilguy ./cmd/lilguy
|
||||
@echo "Built: bin/lilguy"
|
||||
|
||||
# Build for multiple platforms
|
||||
build-all: clean
|
||||
@mkdir -p bin
|
||||
GOOS=darwin GOARCH=arm64 go build -o bin/bigfeelings-darwin-arm64 ./cmd/bigfeelings
|
||||
GOOS=darwin GOARCH=amd64 go build -o bin/bigfeelings-darwin-amd64 ./cmd/bigfeelings
|
||||
GOOS=linux GOARCH=amd64 go build -o bin/bigfeelings-linux-amd64 ./cmd/bigfeelings
|
||||
GOOS=windows GOARCH=amd64 go build -o bin/bigfeelings-windows-amd64.exe ./cmd/bigfeelings
|
||||
GOOS=darwin GOARCH=arm64 go build -o bin/lilguy-darwin-arm64 ./cmd/lilguy
|
||||
GOOS=darwin GOARCH=amd64 go build -o bin/lilguy-darwin-amd64 ./cmd/lilguy
|
||||
GOOS=linux GOARCH=amd64 go build -o bin/lilguy-linux-amd64 ./cmd/lilguy
|
||||
GOOS=windows GOARCH=amd64 go build -o bin/lilguy-windows-amd64.exe ./cmd/lilguy
|
||||
@echo "Built all platforms"
|
||||
|
||||
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"
|
||||
|
||||
game "github.com/atridad/BigFeelings/internal/game"
|
||||
game "github.com/atridad/LilGuy/internal/game"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
2
go.mod
@@ -1,4 +1,4 @@
|
||||
module github.com/atridad/BigFeelings
|
||||
module github.com/atridad/LilGuy
|
||||
|
||||
go 1.25.4
|
||||
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
|
||||
"github.com/atridad/BigFeelings/internal/save"
|
||||
"github.com/atridad/BigFeelings/internal/screens"
|
||||
"github.com/atridad/BigFeelings/internal/ui/menu"
|
||||
"github.com/atridad/LilGuy/internal/save"
|
||||
"github.com/atridad/LilGuy/internal/screens"
|
||||
"github.com/atridad/LilGuy/internal/ui/menu"
|
||||
)
|
||||
|
||||
// Window and display configuration.
|
||||
@@ -17,7 +17,7 @@ const (
|
||||
ScreenWidth = 960
|
||||
ScreenHeight = 540
|
||||
TargetTPS = 60
|
||||
WindowTitle = "Big Feelings"
|
||||
WindowTitle = "Lil Guy"
|
||||
)
|
||||
|
||||
// Game states define the different screens and modes the game can be in.
|
||||
@@ -87,18 +87,18 @@ func (f *FPSCap) Cycle() {
|
||||
type controls struct {
|
||||
Left bool
|
||||
Right bool
|
||||
Up bool
|
||||
Down bool
|
||||
Jump bool
|
||||
Sprint bool
|
||||
Shoot bool
|
||||
}
|
||||
|
||||
func readControls() controls {
|
||||
return controls{
|
||||
Left: ebiten.IsKeyPressed(ebiten.KeyArrowLeft) || ebiten.IsKeyPressed(ebiten.KeyA),
|
||||
Right: ebiten.IsKeyPressed(ebiten.KeyArrowRight) || ebiten.IsKeyPressed(ebiten.KeyD),
|
||||
Up: ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW),
|
||||
Down: ebiten.IsKeyPressed(ebiten.KeyArrowDown) || ebiten.IsKeyPressed(ebiten.KeyS),
|
||||
Jump: ebiten.IsKeyPressed(ebiten.KeyArrowUp) || ebiten.IsKeyPressed(ebiten.KeyW) || ebiten.IsKeyPressed(ebiten.KeySpace),
|
||||
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{
|
||||
Left: input.Left,
|
||||
Right: input.Right,
|
||||
Up: input.Up,
|
||||
Down: input.Down,
|
||||
Jump: input.Jump,
|
||||
Sprint: input.Sprint,
|
||||
Shoot: input.Shoot,
|
||||
}, delta)
|
||||
|
||||
// Periodic auto-save
|
||||
if now.Sub(g.state.lastAutoSave) >= g.state.autoSaveInterval {
|
||||
g.saveGame()
|
||||
g.state.gameplayScreen.ShowSaveNotification()
|
||||
g.state.lastAutoSave = now
|
||||
}
|
||||
|
||||
@@ -343,6 +344,9 @@ func (g *Game) updatePaused() error {
|
||||
case menu.OptionSave:
|
||||
// Save game immediately
|
||||
g.saveGame()
|
||||
g.state.gameplayScreen.ShowSaveNotification()
|
||||
g.state.gameState = statePlaying
|
||||
g.state.lastTick = time.Now()
|
||||
case menu.OptionMainMenu:
|
||||
// Save game before returning to main menu
|
||||
g.saveGame()
|
||||
|
||||
@@ -2,44 +2,51 @@ package hero
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"github.com/atridad/LilGuy/internal/projectile"
|
||||
)
|
||||
|
||||
// Default values and gameplay constants.
|
||||
|
||||
const (
|
||||
// Default values if not specified in config
|
||||
defaultRadius = 24.0
|
||||
defaultSpeed = 180.0
|
||||
defaultSpeed = 200.0
|
||||
defaultMaxStamina = 100.0
|
||||
defaultStaminaDrain = 50.0 // Per second when sprinting
|
||||
defaultStaminaRegen = 30.0 // Per second when not sprinting
|
||||
defaultStaminaDrain = 50.0
|
||||
defaultStaminaRegen = 30.0
|
||||
|
||||
// Sprint mechanics
|
||||
sprintSpeedMultiplier = 2.0
|
||||
sprintRecoveryThreshold = 0.2 // 20% stamina needed to sprint again
|
||||
sprintSpeedMultiplier = 1.8
|
||||
sprintRecoveryThreshold = 0.2
|
||||
|
||||
// Animation
|
||||
normalAnimSpeed = 0.15 // Seconds per frame when walking
|
||||
sprintAnimSpeed = 0.08 // Seconds per frame when sprinting
|
||||
normalAnimSpeed = 0.15
|
||||
idleAnimSpeed = 0.3
|
||||
sprintAnimSpeed = 0.08
|
||||
|
||||
// Visual state thresholds
|
||||
exhaustedThreshold = 0.2 // Show exhausted state below 20% stamina
|
||||
exhaustedThreshold = 0.2
|
||||
|
||||
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 {
|
||||
Left bool
|
||||
Right bool
|
||||
Up bool
|
||||
Down bool
|
||||
Jump bool
|
||||
Sprint bool
|
||||
}
|
||||
|
||||
type Bounds struct {
|
||||
Width float64
|
||||
Height float64
|
||||
Ground float64
|
||||
}
|
||||
|
||||
type VisualState int
|
||||
@@ -53,37 +60,38 @@ const (
|
||||
type Direction int
|
||||
|
||||
const (
|
||||
DirDown Direction = iota
|
||||
DirUp
|
||||
DirLeft
|
||||
DirLeft Direction = iota
|
||||
DirRight
|
||||
)
|
||||
|
||||
type Hero struct {
|
||||
// Position and size
|
||||
X float64
|
||||
Y float64
|
||||
Radius float64
|
||||
|
||||
// Movement
|
||||
VelocityX float64
|
||||
VelocityY float64
|
||||
|
||||
Speed float64
|
||||
|
||||
// Appearance
|
||||
Color color.NRGBA
|
||||
|
||||
// Stamina system
|
||||
Stamina float64
|
||||
MaxStamina float64
|
||||
StaminaDrain float64
|
||||
StaminaRegen float64
|
||||
|
||||
// Internal state
|
||||
canSprint bool
|
||||
wasSprintHeld bool
|
||||
isSprinting bool
|
||||
isMoving bool
|
||||
isGrounded bool
|
||||
direction Direction
|
||||
animFrame int
|
||||
animTimer float64
|
||||
lastAnimKey animationKey
|
||||
|
||||
ProjectileConfig projectile.ProjectileConfig
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
@@ -129,60 +137,80 @@ func New(cfg Config) *Hero {
|
||||
StaminaRegen: cfg.StaminaRegen,
|
||||
canSprint: true,
|
||||
wasSprintHeld: false,
|
||||
direction: DirDown,
|
||||
direction: DirRight,
|
||||
animFrame: 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) {
|
||||
h.updateMovement(input, dt, bounds)
|
||||
h.updateStamina(input, dt)
|
||||
h.updateAnimation(input, dt)
|
||||
h.updateAnimation(dt)
|
||||
}
|
||||
|
||||
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 {
|
||||
dx -= 1
|
||||
targetVelocityX -= h.Speed
|
||||
h.direction = DirLeft
|
||||
}
|
||||
if input.Right {
|
||||
dx += 1
|
||||
targetVelocityX += h.Speed
|
||||
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 {
|
||||
length := math.Hypot(dx, dy)
|
||||
dx /= length
|
||||
dy /= length
|
||||
}
|
||||
|
||||
speed := h.Speed
|
||||
h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && isMoving
|
||||
h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && h.isMoving
|
||||
if h.isSprinting {
|
||||
speed *= sprintSpeedMultiplier
|
||||
targetVelocityX *= sprintSpeedMultiplier
|
||||
}
|
||||
|
||||
h.X += dx * speed * dt
|
||||
h.Y += dy * speed * dt
|
||||
friction := groundFriction
|
||||
if !h.isGrounded {
|
||||
friction = airFriction
|
||||
}
|
||||
|
||||
maxX := math.Max(h.Radius, bounds.Width-h.Radius)
|
||||
maxY := math.Max(h.Radius, bounds.Height-h.Radius)
|
||||
h.VelocityX = h.VelocityX*friction + targetVelocityX*(1-friction)
|
||||
|
||||
h.X = clamp(h.X, h.Radius, maxX)
|
||||
h.Y = clamp(h.Y, h.Radius, maxY)
|
||||
h.X += h.VelocityX * dt
|
||||
|
||||
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) {
|
||||
@@ -208,22 +236,30 @@ func (h *Hero) updateStamina(input Input, dt float64) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hero) updateAnimation(input Input, dt float64) {
|
||||
isMoving := input.Left || input.Right || input.Up || input.Down
|
||||
|
||||
func (h *Hero) updateAnimation(dt float64) {
|
||||
isMoving := h.isMoving
|
||||
key := animationKey{direction: h.direction, state: animIdle}
|
||||
if isMoving {
|
||||
animSpeed := normalAnimSpeed
|
||||
if h.isSprinting {
|
||||
animSpeed = sprintAnimSpeed
|
||||
key.state = animMove
|
||||
}
|
||||
h.animTimer += dt
|
||||
if h.animTimer >= animSpeed {
|
||||
h.animTimer = 0
|
||||
h.animFrame = 1 - h.animFrame
|
||||
}
|
||||
} else {
|
||||
|
||||
if key != h.lastAnimKey {
|
||||
h.animFrame = 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()
|
||||
|
||||
if sprite != nil {
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
bounds := sprite.Bounds()
|
||||
w, height := float64(bounds.Dx()), float64(bounds.Dy())
|
||||
op.GeoM.Translate(-w/2, -height/2)
|
||||
actualHeight := float64(bounds.Dy())
|
||||
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)
|
||||
|
||||
state := h.getVisualState()
|
||||
@@ -254,9 +293,7 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
||||
case StateExhausted:
|
||||
op.ColorScale.ScaleWithColor(color.RGBA{R: 255, G: 100, B: 100, A: 255})
|
||||
case StateSprinting:
|
||||
// No color change
|
||||
case StateIdle:
|
||||
// No color change
|
||||
}
|
||||
|
||||
screen.DrawImage(sprite, op)
|
||||
@@ -264,44 +301,9 @@ func (h *Hero) Draw(screen *ebiten.Image) {
|
||||
}
|
||||
|
||||
func (h *Hero) getCurrentSprite() *ebiten.Image {
|
||||
var sprite *ebiten.Image
|
||||
|
||||
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
|
||||
return getKnightSprite(h.direction, h.isMoving, h.animFrame)
|
||||
}
|
||||
|
||||
func clamp(value, min, max float64) float64 {
|
||||
if value < min {
|
||||
return min
|
||||
}
|
||||
if value > max {
|
||||
return max
|
||||
}
|
||||
return value
|
||||
func (h *Hero) GetDirection() Direction {
|
||||
return h.direction
|
||||
}
|
||||
|
||||
@@ -1,65 +1,151 @@
|
||||
package hero
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
_ "image/gif"
|
||||
_ "image/png"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
|
||||
"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 (
|
||||
spriteBack1 *ebiten.Image
|
||||
spriteBack2 *ebiten.Image
|
||||
spriteFront1 *ebiten.Image
|
||||
spriteFront2 *ebiten.Image
|
||||
spriteLeft1 *ebiten.Image
|
||||
spriteLeft2 *ebiten.Image
|
||||
spriteRight1 *ebiten.Image
|
||||
spriteRight2 *ebiten.Image
|
||||
knightAnimations map[animationKey][]*ebiten.Image
|
||||
)
|
||||
|
||||
func init() {
|
||||
spriteBack1 = loadSprite("assets/hero/avt1_bk1.gif")
|
||||
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")
|
||||
knightAnimations = make(map[animationKey][]*ebiten.Image)
|
||||
|
||||
if err := loadKnightAnimations(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func loadSprite(path string) *ebiten.Image {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
func getKnightSprite(direction Direction, moving bool, frameIndex int) *ebiten.Image {
|
||||
state := animIdle
|
||||
if moving {
|
||||
state = animMove
|
||||
}
|
||||
defer file.Close()
|
||||
return frameFromSet(direction, state, frameIndex)
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(file)
|
||||
func loadKnightAnimations() error {
|
||||
frames, err := loadAnimationFrames(heroDir)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
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()
|
||||
rgba := image.NewRGBA(bounds)
|
||||
for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
|
||||
for x := bounds.Min.X; x < bounds.Max.X; x++ {
|
||||
r, g, b, a := img.At(x, y).RGBA()
|
||||
if r > 0xf000 && g > 0xf000 && b > 0xf000 {
|
||||
rgba.Set(x, y, color.RGBA{0, 0, 0, 0})
|
||||
} else {
|
||||
rgba.Set(x, y, color.RGBA{
|
||||
R: uint8(r >> 8),
|
||||
G: uint8(g >> 8),
|
||||
B: uint8(b >> 8),
|
||||
A: uint8(a >> 8),
|
||||
})
|
||||
w, h := bounds.Dx(), bounds.Dy()
|
||||
|
||||
flipped := ebiten.NewImage(w, h)
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(-1, 1)
|
||||
op.GeoM.Translate(float64(w), 0)
|
||||
|
||||
flipped.DrawImage(img, op)
|
||||
|
||||
return flipped
|
||||
}
|
||||
|
||||
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"
|
||||
|
||||
"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/BigFeelings/internal/save"
|
||||
"github.com/atridad/BigFeelings/internal/status"
|
||||
"github.com/atridad/BigFeelings/internal/ui/hud"
|
||||
"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: 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 (
|
||||
heroStartX = 960 / 2 // ScreenWidth / 2
|
||||
heroStartY = 540 / 2 // ScreenHeight / 2
|
||||
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 (
|
||||
@@ -59,9 +65,9 @@ const (
|
||||
type GameplayInput struct {
|
||||
Left bool
|
||||
Right bool
|
||||
Up bool
|
||||
Down bool
|
||||
Jump bool
|
||||
Sprint bool
|
||||
Shoot bool
|
||||
}
|
||||
|
||||
// Manages the main gameplay state including the hero, HUD, and game world.
|
||||
@@ -69,6 +75,8 @@ type GameplayInput struct {
|
||||
type GameplayScreen struct {
|
||||
hero *hero.Hero
|
||||
hud hud.Overlay
|
||||
world *world.World
|
||||
projectiles *projectile.Manager
|
||||
bounds hero.Bounds
|
||||
lastTick time.Time
|
||||
gameStartTime time.Time
|
||||
@@ -77,10 +85,25 @@ type GameplayScreen struct {
|
||||
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,
|
||||
@@ -97,9 +120,12 @@ func NewGameplayScreen(screenWidth, screenHeight int, fpsEnabled *bool) *Gamepla
|
||||
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(),
|
||||
@@ -114,15 +140,30 @@ func (g *GameplayScreen) Update(input GameplayInput, delta time.Duration) {
|
||||
g.hero.Update(hero.Input{
|
||||
Left: input.Left,
|
||||
Right: input.Right,
|
||||
Up: input.Up,
|
||||
Down: input.Down,
|
||||
Jump: input.Jump,
|
||||
Sprint: input.Sprint,
|
||||
}, 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.trackFPS(delta)
|
||||
|
||||
if g.showSaveNotification {
|
||||
g.saveNotificationTimer -= delta
|
||||
if g.saveNotificationTimer <= 0 {
|
||||
g.showSaveNotification = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Calculates the current FPS if FPS monitoring is enabled.
|
||||
@@ -144,6 +185,9 @@ func (g *GameplayScreen) trackFPS(delta time.Duration) {
|
||||
// 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
|
||||
@@ -181,10 +225,63 @@ func (g *GameplayScreen) Draw(screen *ebiten.Image) {
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -195,6 +292,13 @@ func (g *GameplayScreen) Reset() {
|
||||
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
|
||||
@@ -215,6 +319,20 @@ func (g *GameplayScreen) SaveState() *save.GameState {
|
||||
|
||||
// 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,
|
||||
@@ -226,6 +344,13 @@ func (g *GameplayScreen) LoadState(state *save.GameState) {
|
||||
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()
|
||||
|
||||
@@ -82,7 +82,7 @@ func (s *SplashScreen) Draw(screen *ebiten.Image, screenWidth, screenHeight int)
|
||||
}
|
||||
|
||||
// Draw large game title
|
||||
titleText := "BIG FEELINGS"
|
||||
titleText := "LIL GUY"
|
||||
|
||||
// Calculate size for large text (scale up the basic font)
|
||||
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})
|
||||
|
||||
// Draw game title
|
||||
titleText := "BIG FEELINGS"
|
||||
titleText := "LIL GUY"
|
||||
scale := 3.0
|
||||
charWidth := 7.0 * scale
|
||||
textWidth := float64(len(titleText)) * charWidth
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2/text/v2"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
|
||||
"github.com/atridad/BigFeelings/internal/status"
|
||||
"github.com/atridad/LilGuy/internal/status"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
|
||||
"github.com/atridad/BigFeelings/internal/status"
|
||||
"github.com/atridad/LilGuy/internal/status"
|
||||
)
|
||||
|
||||
// HUD overlay anchor.
|
||||
@@ -24,7 +24,7 @@ func (o Overlay) Draw(screen *ebiten.Image, meters []status.Meter) {
|
||||
// Instruction text
|
||||
instructions := Column{
|
||||
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: "Hold Shift to Sprint", Color: o.Color}},
|
||||
Spacing: 7,
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
|
||||
"github.com/atridad/BigFeelings/internal/screens"
|
||||
"github.com/atridad/LilGuy/internal/screens"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||