Using a temp free online sprite for now to test sprites

This commit is contained in:
2025-11-19 09:08:25 -07:00
parent c5a3bcb3f4
commit 8eb5909919
11 changed files with 370 additions and 271 deletions

BIN
assets/hero/avt1_bk1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/hero/avt1_bk2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/hero/avt1_fr1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/hero/avt1_fr2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
assets/hero/avt1_lf1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/hero/avt1_lf2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/hero/avt1_rt1.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
assets/hero/avt1_rt2.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -13,18 +13,57 @@ import (
"github.com/atridad/BigFeelings/internal/ui/menu" "github.com/atridad/BigFeelings/internal/ui/menu"
) )
// ============================================================
// CONFIGURATION
// Tweak these values to change game settings
// ============================================================
const ( const (
ScreenWidth = 960 ScreenWidth = 960
ScreenHeight = 540 ScreenHeight = 540
TargetTPS = 60
TargetTPS = 60 WindowTitle = "Big Feelings"
WindowTitle = "Big Feelings"
) )
var ( var (
backgroundColor = color.NRGBA{R: 0, G: 0, B: 0, A: 255} backgroundColor = color.NRGBA{R: 0, G: 0, B: 0, A: 255}
) )
// Hero configuration
const (
heroStartX = ScreenWidth / 2
heroStartY = ScreenHeight / 2
heroRadius = 28.0
heroSpeed = 180.0
heroMaxStamina = 100.0
heroStaminaDrain = 50.0
heroStaminaRegen = 30.0
)
var (
heroColor = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
)
// HUD configuration
const (
hudX = ScreenWidth - 220
hudY = 20
)
// Stamina bar colors
var (
staminaNormalColor = color.NRGBA{R: 0, G: 255, B: 180, A: 255}
staminaLowColor = color.NRGBA{R: 255, G: 60, B: 60, A: 255}
)
const (
staminaLowThreshold = 0.2
)
// ============================================================
// TYPES
// ============================================================
type gameState int type gameState int
const ( const (
@@ -40,63 +79,10 @@ type controls struct {
Sprint bool Sprint 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),
Sprint: ebiten.IsKeyPressed(ebiten.KeyShift),
}
}
type Game struct { type Game struct {
state *state state *state
} }
func New() *Game {
return &Game{state: newState()}
}
func (g *Game) Update() error {
// Handle escape key to toggle pause
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
if g.state.gameState == statePlaying {
g.state.gameState = statePaused
g.state.pauseMenu.Reset()
} else if g.state.gameState == statePaused {
g.state.gameState = statePlaying
// Reset lastTick to prevent delta time accumulation while paused
g.state.lastTick = time.Now()
}
}
if g.state.gameState == statePlaying {
g.state.update(readControls())
} else if g.state.gameState == statePaused {
if selectedOption := g.state.pauseMenu.Update(); selectedOption != nil {
switch *selectedOption {
case menu.OptionResume:
g.state.gameState = statePlaying
// Reset lastTick to prevent delta time accumulation while paused
g.state.lastTick = time.Now()
case menu.OptionQuit:
return ebiten.Termination
}
}
}
return nil
}
func (g *Game) Draw(screen *ebiten.Image) {
g.state.draw(screen)
}
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return ScreenWidth, ScreenHeight
}
type state struct { type state struct {
hero *hero.Hero hero *hero.Hero
hud hud.Overlay hud hud.Overlay
@@ -106,22 +92,30 @@ type state struct {
gameState gameState gameState gameState
} }
// ============================================================
// INITIALIZATION
// ============================================================
func New() *Game {
return &Game{state: newState()}
}
func newState() *state { func newState() *state {
now := time.Now() now := time.Now()
return &state{ return &state{
hero: hero.New(hero.Config{ hero: hero.New(hero.Config{
StartX: ScreenWidth / 2, StartX: heroStartX,
StartY: ScreenHeight / 2, StartY: heroStartY,
Radius: 28, Radius: heroRadius,
Speed: 180, Speed: heroSpeed,
Color: color.NRGBA{R: 210, G: 220, B: 255, A: 255}, Color: heroColor,
MaxStamina: 100, MaxStamina: heroMaxStamina,
StaminaDrain: 50, StaminaDrain: heroStaminaDrain,
StaminaRegen: 30, StaminaRegen: heroStaminaRegen,
}), }),
hud: hud.Overlay{ hud: hud.Overlay{
X: ScreenWidth - 220, X: hudX,
Y: 20, Y: hudY,
Color: color.White, Color: color.White,
}, },
bounds: hero.Bounds{ bounds: hero.Bounds{
@@ -134,6 +128,52 @@ func newState() *state {
} }
} }
// ============================================================
// INPUT
// ============================================================
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),
Sprint: ebiten.IsKeyPressed(ebiten.KeyShift),
}
}
// ============================================================
// UPDATE
// ============================================================
func (g *Game) Update() error {
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
if g.state.gameState == statePlaying {
g.state.gameState = statePaused
g.state.pauseMenu.Reset()
} else if g.state.gameState == statePaused {
g.state.gameState = statePlaying
g.state.lastTick = time.Now()
}
}
if g.state.gameState == statePlaying {
g.state.update(readControls())
} else if g.state.gameState == statePaused {
if selectedOption := g.state.pauseMenu.Update(); selectedOption != nil {
switch *selectedOption {
case menu.OptionResume:
g.state.gameState = statePlaying
g.state.lastTick = time.Now()
case menu.OptionQuit:
return ebiten.Termination
}
}
}
return nil
}
func (s *state) update(input controls) { func (s *state) update(input controls) {
now := time.Now() now := time.Now()
dt := now.Sub(s.lastTick).Seconds() dt := now.Sub(s.lastTick).Seconds()
@@ -148,14 +188,21 @@ func (s *state) update(input controls) {
}, dt, s.bounds) }, dt, s.bounds)
} }
// ============================================================
// RENDERING
// ============================================================
func (g *Game) Draw(screen *ebiten.Image) {
g.state.draw(screen)
}
func (s *state) draw(screen *ebiten.Image) { func (s *state) draw(screen *ebiten.Image) {
screen.Fill(backgroundColor) screen.Fill(backgroundColor)
s.hero.Draw(screen) s.hero.Draw(screen)
// Create stamina meter from hero's stamina staminaColor := staminaNormalColor
staminaColor := color.NRGBA{R: 0, G: 255, B: 180, A: 255} if s.hero.Stamina < s.hero.MaxStamina*staminaLowThreshold {
if s.hero.Stamina < s.hero.MaxStamina*0.2 { staminaColor = staminaLowColor
staminaColor = color.NRGBA{R: 255, G: 60, B: 60, A: 255}
} }
staminaMeter := status.Meter{ staminaMeter := status.Meter{
@@ -166,8 +213,11 @@ func (s *state) draw(screen *ebiten.Image) {
} }
s.hud.Draw(screen, []status.Meter{staminaMeter}) s.hud.Draw(screen, []status.Meter{staminaMeter})
// Draw pause menu if paused
if s.gameState == statePaused { if s.gameState == statePaused {
s.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight) s.pauseMenu.Draw(screen, ScreenWidth, ScreenHeight)
} }
} }
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
return ScreenWidth, ScreenHeight
}

View File

@@ -5,10 +5,37 @@ import (
"math" "math"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
) )
// Direction flags from the controls. // ============================================================
// CONFIGURATION
// Tweak these values to change gameplay behavior
// ============================================================
const (
// Default values if not specified in config
defaultRadius = 24.0
defaultSpeed = 180.0
defaultMaxStamina = 100.0
defaultStaminaDrain = 50.0 // Per second when sprinting
defaultStaminaRegen = 30.0 // Per second when not sprinting
// Sprint mechanics
sprintSpeedMultiplier = 2.0
sprintRecoveryThreshold = 0.2 // 20% stamina needed to sprint again
// Animation
normalAnimSpeed = 0.15 // Seconds per frame when walking
sprintAnimSpeed = 0.08 // Seconds per frame when sprinting
// Visual state thresholds
exhaustedThreshold = 0.2 // Show exhausted state below 20% stamina
)
// ============================================================
// TYPES
// ============================================================
type Input struct { type Input struct {
Left bool Left bool
Right bool Right bool
@@ -17,13 +44,11 @@ type Input struct {
Sprint bool Sprint bool
} }
// Playfield limits for movement.
type Bounds struct { type Bounds struct {
Width float64 Width float64
Height float64 Height float64
} }
// Visual states for the hero.
type VisualState int type VisualState int
const ( const (
@@ -32,23 +57,42 @@ const (
StateExhausted StateExhausted
) )
// Player avatar data. type Direction int
const (
DirDown Direction = iota
DirUp
DirLeft
DirRight
)
type Hero struct { type Hero struct {
X float64 // Position and size
Y float64 X float64
Radius float64 Y float64
Speed float64 Radius float64
Color color.NRGBA
Stamina float64 // Movement
MaxStamina float64 Speed float64
StaminaDrain float64
StaminaRegen float64 // Appearance
Color color.NRGBA
// Stamina system
Stamina float64
MaxStamina float64
StaminaDrain float64
StaminaRegen float64
// Internal state
canSprint bool canSprint bool
wasSprintHeld bool wasSprintHeld bool
isSprinting bool isSprinting bool
direction Direction
animFrame int
animTimer float64
} }
// Spawn settings for the avatar.
type Config struct { type Config struct {
StartX float64 StartX float64
StartY float64 StartY float64
@@ -60,25 +104,28 @@ type Config struct {
StaminaRegen float64 StaminaRegen float64
} }
// Builds an avatar from the config with fallbacks. // ============================================================
// INITIALIZATION
// ============================================================
func New(cfg Config) *Hero { func New(cfg Config) *Hero {
if cfg.Radius <= 0 { if cfg.Radius <= 0 {
cfg.Radius = 24 cfg.Radius = defaultRadius
} }
if cfg.Speed <= 0 { if cfg.Speed <= 0 {
cfg.Speed = 180 cfg.Speed = defaultSpeed
} }
if cfg.Color.A == 0 { if cfg.Color.A == 0 {
cfg.Color = color.NRGBA{R: 210, G: 220, B: 255, A: 255} cfg.Color = color.NRGBA{R: 210, G: 220, B: 255, A: 255}
} }
if cfg.MaxStamina <= 0 { if cfg.MaxStamina <= 0 {
cfg.MaxStamina = 100 cfg.MaxStamina = defaultMaxStamina
} }
if cfg.StaminaDrain <= 0 { if cfg.StaminaDrain <= 0 {
cfg.StaminaDrain = 50 cfg.StaminaDrain = defaultStaminaDrain
} }
if cfg.StaminaRegen <= 0 { if cfg.StaminaRegen <= 0 {
cfg.StaminaRegen = 30 cfg.StaminaRegen = defaultStaminaRegen
} }
return &Hero{ return &Hero{
@@ -93,24 +140,40 @@ func New(cfg Config) *Hero {
StaminaRegen: cfg.StaminaRegen, StaminaRegen: cfg.StaminaRegen,
canSprint: true, canSprint: true,
wasSprintHeld: false, wasSprintHeld: false,
direction: DirDown,
animFrame: 0,
animTimer: 0,
} }
} }
// Applies movement input and clamps to the playfield. // ============================================================
// UPDATE
// ============================================================
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.updateStamina(input, dt)
h.updateAnimation(input, dt)
}
func (h *Hero) updateMovement(input Input, dt float64, bounds Bounds) {
dx, dy := 0.0, 0.0 dx, dy := 0.0, 0.0
if input.Left { if input.Left {
dx -= 1 dx -= 1
h.direction = DirLeft
} }
if input.Right { if input.Right {
dx += 1 dx += 1
h.direction = DirRight
} }
if input.Up { if input.Up {
dy -= 1 dy -= 1
h.direction = DirUp
} }
if input.Down { if input.Down {
dy += 1 dy += 1
h.direction = DirDown
} }
isMoving := dx != 0 || dy != 0 isMoving := dx != 0 || dy != 0
@@ -122,28 +185,9 @@ func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
} }
speed := h.Speed speed := h.Speed
if !input.Sprint {
h.wasSprintHeld = false
if h.Stamina >= h.MaxStamina*0.2 {
h.canSprint = true
}
}
h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && isMoving h.isSprinting = input.Sprint && h.canSprint && h.Stamina > 0 && isMoving
if h.isSprinting { if h.isSprinting {
speed *= 2.0 speed *= sprintSpeedMultiplier
h.wasSprintHeld = true
h.Stamina -= h.StaminaDrain * dt
if h.Stamina <= 0 {
h.Stamina = 0
h.canSprint = false
}
} else {
h.Stamina += h.StaminaRegen * dt
if h.Stamina > h.MaxStamina {
h.Stamina = h.MaxStamina
}
} }
h.X += dx * speed * dt h.X += dx * speed * dt
@@ -156,9 +200,54 @@ func (h *Hero) Update(input Input, dt float64, bounds Bounds) {
h.Y = clamp(h.Y, h.Radius, maxY) h.Y = clamp(h.Y, h.Radius, maxY)
} }
// Returns the current visual state based on hero state. func (h *Hero) updateStamina(input Input, dt float64) {
if !input.Sprint {
h.wasSprintHeld = false
if h.Stamina >= h.MaxStamina*sprintRecoveryThreshold {
h.canSprint = true
}
}
if h.isSprinting {
h.wasSprintHeld = true
h.Stamina -= h.StaminaDrain * dt
if h.Stamina <= 0 {
h.Stamina = 0
h.canSprint = false
}
} else {
h.Stamina += h.StaminaRegen * dt
if h.Stamina > h.MaxStamina {
h.Stamina = h.MaxStamina
}
}
}
func (h *Hero) updateAnimation(input Input, dt float64) {
isMoving := input.Left || input.Right || input.Up || input.Down
if isMoving {
animSpeed := normalAnimSpeed
if h.isSprinting {
animSpeed = sprintAnimSpeed
}
h.animTimer += dt
if h.animTimer >= animSpeed {
h.animTimer = 0
h.animFrame = 1 - h.animFrame
}
} else {
h.animFrame = 0
h.animTimer = 0
}
}
// ============================================================
// STATE
// ============================================================
func (h *Hero) getVisualState() VisualState { func (h *Hero) getVisualState() VisualState {
if h.Stamina < h.MaxStamina*0.2 { if h.Stamina < h.MaxStamina*exhaustedThreshold {
return StateExhausted return StateExhausted
} }
@@ -169,175 +258,70 @@ func (h *Hero) getVisualState() VisualState {
return StateIdle return StateIdle
} }
// Renders the avatar. // ============================================================
// RENDERING
// ============================================================
func (h *Hero) Draw(screen *ebiten.Image) { func (h *Hero) Draw(screen *ebiten.Image) {
state := h.getVisualState() sprite := h.getCurrentSprite()
vector.FillCircle( if sprite != nil {
screen, op := &ebiten.DrawImageOptions{}
float32(h.X), bounds := sprite.Bounds()
float32(h.Y), w, height := float64(bounds.Dx()), float64(bounds.Dy())
float32(h.Radius), op.GeoM.Translate(-w/2, -height/2)
h.Color, op.GeoM.Translate(h.X, h.Y)
false,
)
eyeOffsetX := h.Radius * 0.3 state := h.getVisualState()
eyeOffsetY := h.Radius * 0.25 switch state {
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
}
switch state { screen.DrawImage(sprite, op)
case StateExhausted:
drawExhaustedFace(screen, h.X, h.Y, h.Radius, eyeOffsetX, eyeOffsetY)
case StateSprinting:
drawSprintingFace(screen, h.X, h.Y, h.Radius, eyeOffsetX, eyeOffsetY)
case StateIdle:
drawIdleFace(screen, h.X, h.Y, h.Radius, eyeOffsetX, eyeOffsetY)
} }
} }
func drawIdleFace(screen *ebiten.Image, x, y, radius, eyeOffsetX, eyeOffsetY float64) { func (h *Hero) getCurrentSprite() *ebiten.Image {
eyeRadius := radius * 0.15 var sprite *ebiten.Image
vector.FillCircle( switch h.direction {
screen, case DirUp:
float32(x-eyeOffsetX), if h.animFrame == 0 {
float32(y-eyeOffsetY), sprite = spriteBack1
float32(eyeRadius), } else {
color.NRGBA{R: 0, G: 0, B: 0, A: 255}, sprite = spriteBack2
false, }
) case DirDown:
if h.animFrame == 0 {
vector.FillCircle( sprite = spriteFront1
screen, } else {
float32(x+eyeOffsetX), sprite = spriteFront2
float32(y-eyeOffsetY), }
float32(eyeRadius), case DirLeft:
color.NRGBA{R: 0, G: 0, B: 0, A: 255}, if h.animFrame == 0 {
false, sprite = spriteLeft1
) } else {
sprite = spriteLeft2
smileRadius := radius * 0.5 }
smileY := y + radius*0.15 case DirRight:
for angle := 0.3; angle <= 2.84; angle += 0.15 { if h.animFrame == 0 {
smileX := x + smileRadius*math.Cos(angle) sprite = spriteRight1
smileYPos := smileY + smileRadius*0.3*math.Sin(angle) } else {
vector.FillCircle( sprite = spriteRight2
screen,
float32(smileX),
float32(smileYPos),
float32(radius*0.08),
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
false,
)
}
}
func drawSprintingFace(screen *ebiten.Image, x, y, radius, eyeOffsetX, eyeOffsetY float64) {
eyeWidth := radius * 0.2
eyeHeight := radius * 0.12
for ex := -eyeWidth; ex <= eyeWidth; ex += radius * 0.08 {
for ey := -eyeHeight; ey <= eyeHeight; ey += radius * 0.08 {
if ex*ex/(eyeWidth*eyeWidth)+ey*ey/(eyeHeight*eyeHeight) <= 1 {
vector.FillCircle(
screen,
float32(x-eyeOffsetX+ex),
float32(y-eyeOffsetY+ey),
float32(radius*0.05),
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
false,
)
}
} }
} }
for ex := -eyeWidth; ex <= eyeWidth; ex += radius * 0.08 { return sprite
for ey := -eyeHeight; ey <= eyeHeight; ey += radius * 0.08 {
if ex*ex/(eyeWidth*eyeWidth)+ey*ey/(eyeHeight*eyeHeight) <= 1 {
vector.FillCircle(
screen,
float32(x+eyeOffsetX+ex),
float32(y-eyeOffsetY+ey),
float32(radius*0.05),
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
false,
)
}
}
}
mouthY := y + radius*0.3
mouthWidth := radius * 0.5
for mx := -mouthWidth; mx <= mouthWidth; mx += radius * 0.08 {
vector.FillCircle(
screen,
float32(x+mx),
float32(mouthY),
float32(radius*0.06),
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
false,
)
}
} }
func drawExhaustedFace(screen *ebiten.Image, x, y, radius, eyeOffsetX, eyeOffsetY float64) { // ============================================================
eyeSize := radius * 0.15 // UTILITIES
// ============================================================
for i := -eyeSize; i <= eyeSize; i += radius * 0.08 {
vector.FillCircle(
screen,
float32(x-eyeOffsetX+i),
float32(y-eyeOffsetY+i),
float32(radius*0.05),
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
false,
)
vector.FillCircle(
screen,
float32(x-eyeOffsetX+i),
float32(y-eyeOffsetY-i),
float32(radius*0.05),
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
false,
)
}
for i := -eyeSize; i <= eyeSize; i += radius * 0.08 {
vector.FillCircle(
screen,
float32(x+eyeOffsetX+i),
float32(y-eyeOffsetY+i),
float32(radius*0.05),
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
false,
)
vector.FillCircle(
screen,
float32(x+eyeOffsetX+i),
float32(y-eyeOffsetY-i),
float32(radius*0.05),
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
false,
)
}
mouthY := y + radius*0.35
mouthWidth := radius * 0.2
mouthHeight := radius * 0.25
for angle := 0.0; angle < 2*math.Pi; angle += 0.3 {
mx := x + mouthWidth*math.Cos(angle)
my := mouthY + mouthHeight*math.Sin(angle)
vector.FillCircle(
screen,
float32(mx),
float32(my),
float32(radius*0.06),
color.NRGBA{R: 0, G: 0, B: 0, A: 255},
false,
)
}
}
func clamp(value, min, max float64) float64 { func clamp(value, min, max float64) float64 {
if value < min { if value < min {

65
internal/hero/sprites.go Normal file
View File

@@ -0,0 +1,65 @@
package hero
import (
"image"
"image/color"
_ "image/gif"
"os"
"github.com/hajimehoshi/ebiten/v2"
)
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
)
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")
}
func loadSprite(path string) *ebiten.Image {
file, err := os.Open(path)
if err != nil {
panic(err)
}
defer file.Close()
img, _, err := image.Decode(file)
if err != nil {
panic(err)
}
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),
})
}
}
}
return ebiten.NewImageFromImage(rgba)
}