Initial commit.

This commit is contained in:
2024-12-08 12:24:33 -05:00
commit d9d09e5169
17 changed files with 671 additions and 0 deletions

15
.vscode/launch.json vendored Normal file
View File

@@ -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"
}
]
}

BIN
assets/duck_idle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

45
assets/imagebank.go Normal file
View File

@@ -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)
}

BIN
assets/mainloop.ogg Normal file

Binary file not shown.

BIN
assets/orb.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

BIN
assets/reducky_idle.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

74
assets/soundbank.go Normal file
View File

@@ -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
}

54
elements/duck.go Normal file
View File

@@ -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
}

87
elements/slider.go Normal file
View File

@@ -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
}

33
elements/spotlight.go Normal file
View File

@@ -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)
}

BIN
fonts/bitbybit.ttf Normal file

Binary file not shown.

41
fonts/fonts.go Normal file
View File

@@ -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
}

BIN
fonts/karenfat.ttf Normal file

Binary file not shown.

267
game/game.go Normal file
View File

@@ -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()
}
}

6
gamedata/gamedata.go Normal file
View File

@@ -0,0 +1,6 @@
package gamedata
type Coordinates struct {
X float64
Y float64
}

26
go.mod Normal file
View File

@@ -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
)

23
main.go Normal file
View File

@@ -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)
}
}