package hero import ( "fmt" "image" _ "image/png" "os" "path/filepath" "sort" "github.com/hajimehoshi/ebiten/v2" ) // Asset paths const ( heroDir = "assets/hero" ) // Animation state type animState int const ( animIdle animState = iota animMove ) type animationKey struct { direction Direction state animState } // Sprite cache var ( knightAnimations map[animationKey][]*ebiten.Image ) func init() { knightAnimations = make(map[animationKey][]*ebiten.Image) if err := loadKnightAnimations(); 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) } // Asset loading 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 } // Image manipulation func flipImageHorizontally(img *ebiten.Image) *ebiten.Image { bounds := img.Bounds() 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 nil }