commit d9d09e5169b046bb37e65392bd5defcafd367cd9 Author: iegod Date: Sun Dec 8 12:24:33 2024 -0500 Initial commit. diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..070806a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ducky", + "type": "go", + "request": "launch", + "mode": "auto", + "program": "main.go" + } + ] +} \ No newline at end of file diff --git a/assets/duck_idle.png b/assets/duck_idle.png new file mode 100644 index 0000000..b648c5e Binary files /dev/null and b/assets/duck_idle.png differ diff --git a/assets/imagebank.go b/assets/imagebank.go new file mode 100644 index 0000000..4e81b48 --- /dev/null +++ b/assets/imagebank.go @@ -0,0 +1,45 @@ +package assets + +import ( + "bytes" + _ "embed" + "image" + _ "image/png" + "log" + + "github.com/hajimehoshi/ebiten/v2" +) + +type ImgAssetName string + +const ( + Ducky ImgAssetName = "Ducky" + ReDucky ImgAssetName = "ReDucky" + Orb ImgAssetName = "Orb" +) + +var ( + ImageBank map[ImgAssetName]*ebiten.Image + + //go:embed duck_idle.png + duckidle_img []byte + //go:embed reducky_idle.png + reduckidle_img []byte + //go:embed orb.png + orb_img []byte +) + +func LoadImages() { + ImageBank = make(map[ImgAssetName]*ebiten.Image) + ImageBank[Ducky] = LoadImagesFatal(duckidle_img) + ImageBank[ReDucky] = LoadImagesFatal(reduckidle_img) + ImageBank[Orb] = LoadImagesFatal(orb_img) +} + +func LoadImagesFatal(b []byte) *ebiten.Image { + img, _, err := image.Decode(bytes.NewReader(b)) + if err != nil { + log.Fatal(err) + } + return ebiten.NewImageFromImage(img) +} diff --git a/assets/mainloop.ogg b/assets/mainloop.ogg new file mode 100644 index 0000000..c22ac93 Binary files /dev/null and b/assets/mainloop.ogg differ diff --git a/assets/orb.png b/assets/orb.png new file mode 100644 index 0000000..4523413 Binary files /dev/null and b/assets/orb.png differ diff --git a/assets/reducky_idle.png b/assets/reducky_idle.png new file mode 100644 index 0000000..df5a75d Binary files /dev/null and b/assets/reducky_idle.png differ diff --git a/assets/soundbank.go b/assets/soundbank.go new file mode 100644 index 0000000..57db74f --- /dev/null +++ b/assets/soundbank.go @@ -0,0 +1,74 @@ +package assets + +import ( + "bytes" + "log" + + _ "embed" + + "github.com/hajimehoshi/ebiten/v2/audio/mp3" + "github.com/hajimehoshi/ebiten/v2/audio/vorbis" + "github.com/hajimehoshi/ebiten/v2/audio/wav" +) + +type SndAssetName string + +const ( + MainLoop SndAssetName = "MainLoop" + MainLoopMp3 SndAssetName = "MainLoopMp3" + MainLoopOgg SndAssetName = "MainLoopOgg" + SampleRate = 44100 +) + +var ( + //SoundBank map[SndAssetName]*wav.Stream + //SoundBankMp3 map[SndAssetName]*mp3.Stream + SoundBankOgg map[SndAssetName]*vorbis.Stream + //go:embed mainloop.ogg + mainloop_snd []byte +) + +func init() { + LoadSounds() +} + +func LoadSounds() { + //SoundBank = make(map[SndAssetName]*wav.Stream) + //SoundBankMp3 = make(map[SndAssetName]*mp3.Stream) + SoundBankOgg = make(map[SndAssetName]*vorbis.Stream) + + //SoundBank[MainLoop] = LoadSoundFatal(SampleRate, hyper_snd) + //SoundBankMp3[MainLoopMp3] = LoadSoundFatalMp3(SampleRate, mainloop_snd) + SoundBankOgg[MainLoopOgg] = LoadSoundFatalOgg(SampleRate, mainloop_snd) + +} + +func LoadSoundFatal(rate int, obj []byte) *wav.Stream { + + stream, err := wav.DecodeWithSampleRate(rate, bytes.NewReader(obj)) + if err != nil { + log.Fatal("dead, jim") + } + + return stream +} + +func LoadSoundFatalMp3(rate int, obj []byte) *mp3.Stream { + + stream, err := mp3.DecodeWithSampleRate(rate, bytes.NewReader(obj)) + if err != nil { + log.Fatal("dead, jim") + } + + return stream +} + +func LoadSoundFatalOgg(rate int, obj []byte) *vorbis.Stream { + + stream, err := vorbis.DecodeWithSampleRate(rate, bytes.NewReader(obj)) + if err != nil { + log.Fatal("dead, jim") + } + + return stream +} diff --git a/elements/duck.go b/elements/duck.go new file mode 100644 index 0000000..7a6a68d --- /dev/null +++ b/elements/duck.go @@ -0,0 +1,54 @@ +package elements + +import ( + "ducky/gamedata" + "image" + + "github.com/hajimehoshi/ebiten/v2" + "golang.org/x/exp/rand" +) + +const ( + DuckyWidth = 36 + DuckyHeight = DuckyWidth +) + +type Ducky struct { + Sprite *ebiten.Image + asset *ebiten.Image + cycle int + position gamedata.Coordinates +} + +func NewDucky(asset *ebiten.Image) *Ducky { + d := &Ducky{ + Sprite: ebiten.NewImage(DuckyWidth, DuckyHeight), + asset: asset, + cycle: rand.Intn(11), + } + return d +} + +func (d *Ducky) Update() { + d.cycle++ +} + +func (d *Ducky) Draw() { + d.Sprite.Clear() + + idx := d.cycle / 4 % 10 + x0 := idx * DuckyWidth + y0 := 0 + x1 := x0 + DuckyWidth + y1 := DuckyHeight + + d.Sprite.DrawImage(d.asset.SubImage(image.Rect(x0, y0, x1, y1)).(*ebiten.Image), nil) +} + +func (d *Ducky) SetPosition(pos gamedata.Coordinates) { + d.position = pos +} + +func (d *Ducky) GetPosition() gamedata.Coordinates { + return d.position +} diff --git a/elements/slider.go b/elements/slider.go new file mode 100644 index 0000000..0d39f6c --- /dev/null +++ b/elements/slider.go @@ -0,0 +1,87 @@ +package elements + +import ( + "ducky/gamedata" + "image/color" + "math" + + "github.com/hajimehoshi/ebiten/v2" + "golang.org/x/exp/rand" +) + +const ( + sliderWidth = 640 / 5 + sliderHeight = 480 + easeValue = 16 +) + +type Slider struct { + Sprite *ebiten.Image + position gamedata.Coordinates + targetposition gamedata.Coordinates + cycle int + callback func() + called bool + color color.Color +} + +func NewSlider() *Slider { + slider := &Slider{ + Sprite: ebiten.NewImage(sliderWidth, sliderHeight), + called: false, + color: color.RGBA{ + R: uint8(rand.Intn(256)) & 0xff, + G: uint8(rand.Intn(256)) & 0xff, + B: uint8(rand.Intn(256)) & 0xff, + A: 0xff, + }, + } + return slider +} + +func (s *Slider) Update() { + + dx := s.targetposition.X - s.position.X + dy := s.targetposition.Y - s.position.Y + + deltapos := math.Sqrt(dx*dx + dy*dy) + + if deltapos > 0.5 { + s.position.X = s.position.X + dx/easeValue + s.position.Y = s.position.Y + dy/easeValue + } else { + if s.callback != nil && !s.called { + s.callback() + s.called = true + } + } + + s.cycle++ +} + +func (s *Slider) Draw() { + s.Sprite.Clear() + s.Sprite.Fill(s.color) +} + +func (s *Slider) GetPosition() gamedata.Coordinates { + return s.position +} + +func (s *Slider) SetPosition(pos gamedata.Coordinates) { + s.position = pos +} + +func (s *Slider) SetTargetPosition(pos gamedata.Coordinates) { + s.targetposition = pos +} + +func (s *Slider) SetCallback(f func()) { + if f != nil { + s.callback = f + } +} + +func (s *Slider) Reset() { + s.called = false +} diff --git a/elements/spotlight.go b/elements/spotlight.go new file mode 100644 index 0000000..40ba7ce --- /dev/null +++ b/elements/spotlight.go @@ -0,0 +1,33 @@ +package elements + +import "github.com/hajimehoshi/ebiten/v2" + +type Spotlight struct { + Sprite *ebiten.Image + asset *ebiten.Image + cycle int +} + +func NewSpotlight(asset *ebiten.Image) *Spotlight { + sl := &Spotlight{ + Sprite: ebiten.NewImage(100, 100), + asset: asset, + } + + return sl +} + +func (s *Spotlight) Update() { + s.cycle++ +} + +func (s *Spotlight) Draw() { + s.Sprite.Clear() + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(-16, -16) + op.GeoM.Scale(3, 3) + op.GeoM.Translate(50, 50) + s.Sprite.DrawImage(s.asset, op) + +} diff --git a/fonts/bitbybit.ttf b/fonts/bitbybit.ttf new file mode 100644 index 0000000..f2222e1 Binary files /dev/null and b/fonts/bitbybit.ttf differ diff --git a/fonts/fonts.go b/fonts/fonts.go new file mode 100644 index 0000000..9ad8678 --- /dev/null +++ b/fonts/fonts.go @@ -0,0 +1,41 @@ +package fonts + +import ( + "bytes" + "log" + + _ "embed" + + "github.com/hajimehoshi/ebiten/v2/text/v2" +) + +type FontStruct struct { + Ducky *text.GoTextFaceSource + Karen *text.GoTextFaceSource +} + +var ( + //go:embed bitbybit.ttf + bitbybit_ttf []byte + //go:embed karenfat.ttf + karen_ttf []byte + + DuckyFont FontStruct +) + +func init() { + DuckyFont = FontStruct{} + + s, err := text.NewGoTextFaceSource(bytes.NewReader(bitbybit_ttf)) + if err != nil { + log.Fatal(err) + } + DuckyFont.Ducky = s + + s, err = text.NewGoTextFaceSource(bytes.NewReader(karen_ttf)) + if err != nil { + log.Fatal(err) + } + DuckyFont.Karen = s + +} diff --git a/fonts/karenfat.ttf b/fonts/karenfat.ttf new file mode 100644 index 0000000..59be609 Binary files /dev/null and b/fonts/karenfat.ttf differ diff --git a/game/game.go b/game/game.go new file mode 100644 index 0000000..c23708b --- /dev/null +++ b/game/game.go @@ -0,0 +1,267 @@ +package game + +import ( + "ducky/assets" + "ducky/elements" + "ducky/fonts" + "ducky/gamedata" + "fmt" + "image/color" + "math" + "math/rand" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/audio" + "github.com/hajimehoshi/ebiten/v2/inpututil" + "github.com/hajimehoshi/ebiten/v2/text/v2" +) + +const ( + screenWidth = 640 + screenHeight = 480 + + duckyWidth = 32 + duckyHeight = duckyWidth + + spotResetDuration = 120 + duckySpottedTextOffsetX = 120 + duckySpottedTextOffsetY = 32 + duckySpottedTextSize = 30 + toastCount = 120 +) + +var ( + audioContext = audio.NewContext(assets.SampleRate) +) + +type Game struct { + initialized bool + reducky *elements.Ducky + ducky *elements.Ducky + spotlight *elements.Spotlight + cycle int + mousepos gamedata.Coordinates + + darkness *ebiten.Image + offscreen *ebiten.Image + + reduckycol int + reduckyrow int + + spottedticks int + spotted bool + toast bool + toastcounter int + disabledarkness bool + + sliders []*elements.Slider + + reachcount int + + musicInitialized bool + audioplayer *audio.Player +} + +func (g *Game) Update() error { + + g.HandleInput() + + if !g.initialized { + g.Initialize() + } + + g.UpdateDuck() + g.UpdateDetection() + //g.UpdateSliders() + + g.cycle++ + + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + screen.Clear() + g.darkness.Clear() + + if g.initialized { + + //g.darkness.Fill(color.RGBA{R: 0xff, G: 0x00, B: 0x00, A: 0xff}) + g.darkness.Fill(color.Black) + g.ducky.Draw() + g.reducky.Draw() + + op := &ebiten.DrawImageOptions{} + for x := 0; x < screenWidth/duckyWidth; x++ { + for y := 0; y < screenHeight/duckyWidth; y++ { + op.GeoM.Reset() + op.GeoM.Translate(float64(x)*duckyWidth, float64(y)*duckyHeight) + if !(x == g.reduckycol && y == g.reduckyrow) { + screen.DrawImage(g.ducky.Sprite, op) + } + } + } + + op = &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(g.reduckycol)*duckyWidth, float64(g.reduckyrow)*duckyHeight) + screen.DrawImage(g.reducky.Sprite, op) + + op = &ebiten.DrawImageOptions{} + op.GeoM.Translate(-duckyWidth/2, -duckyHeight/2) + op.GeoM.Translate(g.mousepos.X, g.mousepos.Y) + //screen.DrawImage(assets.ImageBank[assets.Orb], op) + + g.spotlight.Draw() + + g.offscreen.Clear() + op = &ebiten.DrawImageOptions{} + + spotlight_w := float64(g.spotlight.Sprite.Bounds().Dx()) + spotlight_h := float64(g.spotlight.Sprite.Bounds().Dy()) + + op.GeoM.Translate(-spotlight_w/2, -spotlight_h/2) + op.GeoM.Translate(g.mousepos.X, g.mousepos.Y) + //g.offscreen.DrawImage(assets.ImageBank[assets.Orb], op) + g.offscreen.DrawImage(g.spotlight.Sprite, op) + + op.GeoM.Reset() + op.Blend = ebiten.BlendXor + g.offscreen.DrawImage(g.darkness, op) + + if !g.disabledarkness { + op = &ebiten.DrawImageOptions{} + if g.toast { + op.GeoM.Translate(-screenWidth/2, -screenHeight/2) + op.GeoM.Rotate(float64(g.toastcounter) / (math.Pi * 2)) + op.GeoM.Translate(screenWidth/2, screenHeight/2) + } + screen.DrawImage(g.offscreen, op) + } + + if g.spotted && !g.toast { + font := &text.GoTextFace{ + Source: fonts.DuckyFont.Karen, + Size: duckySpottedTextSize, + } + top := &text.DrawOptions{} + top.GeoM.Translate(screenWidth/2-duckySpottedTextOffsetX, screenHeight/2-duckySpottedTextOffsetY) + if g.cycle%30 < 15 { + text.Draw(screen, "DUCKY SPOTTED", font, top) + } + } + + for _, s := range g.sliders { + s.Draw() + op = &ebiten.DrawImageOptions{} + op.GeoM.Translate(s.GetPosition().X, s.GetPosition().Y) + screen.DrawImage(s.Sprite, op) + } + + } +} + +func (g *Game) Layout(outsideWidth, outsideHeight int) (screenwidth, screenheight int) { + return screenWidth, screenHeight +} + +func (g *Game) Initialize() { + assets.LoadImages() + + g.offscreen = ebiten.NewImage(screenWidth, screenHeight) + g.darkness = ebiten.NewImage(screenWidth, screenHeight) + + g.spotlight = elements.NewSpotlight(assets.ImageBank[assets.Orb]) + g.ducky = elements.NewDucky(assets.ImageBank[assets.Ducky]) + g.reducky = elements.NewDucky(assets.ImageBank[assets.ReDucky]) + + for i := 0; i < 5; i++ { + slider := elements.NewSlider() + g.sliders = append(g.sliders, slider) + } + + g.initialized = true + + if !g.musicInitialized { + //s := audio.NewInfiniteLoop(assets.SoundBank[assets.MainLoop], assets.SoundBank[assets.MainLoop].Length()) + s := audio.NewInfiniteLoop(assets.SoundBankOgg[assets.MainLoopOgg], assets.SoundBankOgg[assets.MainLoopOgg].Length()) + g.audioplayer, _ = audioContext.NewPlayer(s) + g.audioplayer.Play() + g.musicInitialized = true + } + + g.Reset() +} + +func (g *Game) UpdateDuck() { + g.ducky.Update() + g.reducky.Update() + + x, y := ebiten.CursorPosition() + g.mousepos.X = float64(x) + g.mousepos.Y = float64(y) +} + +func (g *Game) Reset() { + g.reduckycol = rand.Intn(screenWidth / duckyHeight) + g.reduckyrow = rand.Intn(screenHeight / duckyWidth) + + for i, slider := range g.sliders { + var yoffset float64 + + if i%2 == 0 { + yoffset = screenHeight + } else { + yoffset = -screenHeight + } + + slider.SetPosition(gamedata.Coordinates{X: float64(i) * screenWidth / 5, Y: yoffset}) + slider.SetTargetPosition(gamedata.Coordinates{X: float64(i) * screenWidth / 5, Y: 0}) + slider.Reset() + } + + //fmt.Printf("Ducky Position: %d, %d\n", g.reduckycol, g.reduckyrow) + g.spottedticks = 0 + g.toast = false + g.reachcount = 0 + g.toastcounter = 0 +} + +func (g *Game) HandleInput() { + if inpututil.IsKeyJustPressed(ebiten.KeyR) { + g.Reset() + } + + g.disabledarkness = ebiten.IsKeyPressed(ebiten.KeySpace) +} + +func (g *Game) UpdateDetection() { + x, y := ebiten.CursorPosition() + + if g.reduckycol*duckyWidth <= x && x <= (g.reduckycol+1)*duckyWidth && + g.reduckyrow*duckyHeight <= y && y <= (g.reduckyrow+1)*duckyHeight && !g.toast { + g.spottedticks++ + g.spotted = true + fmt.Println("ducky spotted!") + } else { + g.spotted = false + } + + if g.spottedticks > spotResetDuration { + g.toast = true + } + + if g.toast { + g.UpdateSliders() + g.toastcounter++ + } + + if g.toastcounter > toastCount { + g.Reset() + } + +} + +func (g *Game) UpdateSliders() { + for _, s := range g.sliders { + s.Update() + } +} diff --git a/gamedata/gamedata.go b/gamedata/gamedata.go new file mode 100644 index 0000000..208f25e --- /dev/null +++ b/gamedata/gamedata.go @@ -0,0 +1,6 @@ +package gamedata + +type Coordinates struct { + X float64 + Y float64 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ade4c26 --- /dev/null +++ b/go.mod @@ -0,0 +1,26 @@ +module ducky + +go 1.22.0 + +toolchain go1.22.10 + +require ( + github.com/hajimehoshi/ebiten/v2 v2.8.5 + golang.org/x/exp v0.0.0-20241204233417-43b7b7cde48d +) + +require ( + github.com/ebitengine/gomobile v0.0.0-20240911145611-4856209ac325 // indirect + github.com/ebitengine/hideconsole v1.0.0 // indirect + github.com/ebitengine/oto/v3 v3.3.1 // indirect + github.com/ebitengine/purego v0.8.0 // indirect + github.com/go-text/typesetting v0.2.0 // indirect + github.com/hajimehoshi/go-mp3 v0.3.4 // indirect + github.com/jezek/xgb v1.1.1 // indirect + github.com/jfreymuth/oggvorbis v1.0.5 // indirect + github.com/jfreymuth/vorbis v1.0.2 // indirect + golang.org/x/image v0.20.0 // indirect + golang.org/x/sync v0.10.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect +) diff --git a/main.go b/main.go new file mode 100644 index 0000000..53e06aa --- /dev/null +++ b/main.go @@ -0,0 +1,23 @@ +package main + +import ( + "ducky/game" + "fmt" + "log" + + "github.com/hajimehoshi/ebiten/v2" +) + +func main() { + ver := 0.04 + verstring := fmt.Sprintf("ducky v%.2f", ver) + + game := &game.Game{} + + ebiten.SetWindowSize(640*1.5, 480*1.5) + ebiten.SetWindowTitle(verstring) + + if err := ebiten.RunGame(game); err != nil { + log.Fatal(err) + } +}