diff --git a/src/game/audio.go b/src/game/audio.go new file mode 100644 index 0000000..b79a7a5 --- /dev/null +++ b/src/game/audio.go @@ -0,0 +1,46 @@ +package game + +import "strings" + +// Plays sound and rewinds the player +func (g *Game) PlaySound(soundKey string) { + if strings.TrimSpace(soundKey) != "" { + g.AudioPlayers[soundKey].Rewind() + g.AudioPlayers[soundKey].Play() + } +} + +func (g *Game) SetVolume(volume float64) { + if volume > 1.0 || volume < 0.0 { + return + } + + g.Config.Volume = volume + for _, player := range g.AudioPlayers { + player.SetVolume(volume) + } +} + +func (g *Game) IncreaseVolume(volumeDelta float64) { + for _, player := range g.AudioPlayers { + volume := player.Volume() + volumeDelta + if volume > 1.0 || volume < 0.0 { + continue + } + + player.SetVolume(volume) + g.Config.Volume = volume + } +} + +func (g *Game) DecreaseVolume(volumeDelta float64) { + for _, player := range g.AudioPlayers { + volume := player.Volume() - volumeDelta + if volume > 1.0 || volume < 0.0 { + continue + } + + player.SetVolume(volume) + g.Config.Volume = volume + } +} diff --git a/src/game/capybara.go b/src/game/capybara.go new file mode 100644 index 0000000..1b70397 --- /dev/null +++ b/src/game/capybara.go @@ -0,0 +1,73 @@ +package game + +import ( + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +type Capybara struct { + Sprite *Sprite +} + +func NewCapybara(sprite *Sprite) *Capybara { + return &Capybara{ + Sprite: sprite, + } +} + +func (c *Capybara) Update() { + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) || + len(inpututil.AppendJustPressedTouchIDs(nil)) != 0 { + c.Sprite.Animation.Squish += 0.5 + } + + // Capybara Animation + capyAniData := &c.Sprite.Animation + if capyAniData.Theta >= 0.03 { + capyAniData.BounceDirectionFlag = false + } else if capyAniData.Theta <= -0.03 { + capyAniData.BounceDirectionFlag = true + } + + if capyAniData.Squish >= 0 { + capyAniData.Squish -= 0.05 + } + + if capyAniData.BounceDirectionFlag { + capyAniData.Theta += 0.001 + } else { + capyAniData.Theta -= 0.001 + } +} + +func (c *Capybara) Draw(screen *ebiten.Image, level uint32) { + // Capybara + switch level { + case 1: + c.Sprite.ChangeImageByName("capybara_1.png") + case 2: + c.Sprite.ChangeImageByName("capybara_2.png") + case 3: + c.Sprite.ChangeImageByName("capybara_3.png") + default: + c.Sprite.ChangeImageByName("capybara_3.png") + } + + op := &ebiten.DrawImageOptions{} + capybaraBounds := c.Sprite.Img.Bounds() + scale := float64(screen.Bounds().Dx()) / float64(capybaraBounds.Dx()) / 2.5 + c.Sprite.Scale = scale + op.GeoM.Scale( + scale+c.Sprite.Animation.Squish, + scale-c.Sprite.Animation.Squish, + ) + op.GeoM.Rotate(c.Sprite.Animation.Theta) + + capyWidth := float64(c.Sprite.RealBounds().Dx()) + capyHeight := float64(c.Sprite.RealBounds().Dy()) + c.Sprite.MoveTo(float64(screen.Bounds().Dx()/2)-capyWidth/2, float64(screen.Bounds().Dy()/2)-capyHeight/2) + + op.GeoM.Translate(c.Sprite.X, c.Sprite.Y) + + screen.DrawImage(c.Sprite.Img, op) +} diff --git a/src/game/game.go b/src/game/game.go index c6ca2f8..95e840a 100644 --- a/src/game/game.go +++ b/src/game/game.go @@ -9,7 +9,6 @@ import ( "fmt" "image/color" "path/filepath" - "strings" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/audio" @@ -19,65 +18,44 @@ import ( "golang.org/x/image/font/opentype" ) -type AnimationData struct { - Squish float64 - Theta float64 - BounceDirectionFlag bool -} - type Game struct { WorkingDir string Config conf.Configuration Save save.Save - AudioContext *audio.Context AudioPlayers map[string]*audio.Player - ImageResources map[string]*ebiten.Image FontFace font.Face - AnimationData AnimationData PassiveIncomeTicker int + Capybara *Capybara + Background *Sprite + MandarinRain *MandarinRain } -func NewGame() *Game { +func NewGame() Game { audioCtx := audio.NewContext(44000) fnt := resources.GetFont("PixeloidSans-Bold.otf") - return &Game{ - WorkingDir: ".", - Config: conf.Default(), - Save: save.Default(), - AudioContext: audioCtx, + return Game{ + WorkingDir: ".", + Config: conf.Default(), + Save: save.Default(), AudioPlayers: map[string]*audio.Player{ - "boop": resources.GetAudioPlayer(audioCtx, "boop.wav"), - "woop": resources.GetAudioPlayer(audioCtx, "woop.wav"), - "menu_switch": resources.GetAudioPlayer(audioCtx, "menu_switch.wav"), - "levelup": resources.GetAudioPlayer(audioCtx, "levelup.wav"), - }, - ImageResources: map[string]*ebiten.Image{ - "capybara1": ebiten.NewImageFromImage(resources.ImageFromFile("capybara_1.png")), - "capybara2": ebiten.NewImageFromImage(resources.ImageFromFile("capybara_2.png")), - "capybara3": ebiten.NewImageFromImage(resources.ImageFromFile("capybara_3.png")), - "background1": ebiten.NewImageFromImage(resources.ImageFromFile("background_1.png")), - "background2": ebiten.NewImageFromImage(resources.ImageFromFile("background_2.png")), + "boop": resources.GetAudioPlayer(audioCtx, "boop.wav"), + "woop": resources.GetAudioPlayer(audioCtx, "woop.wav"), + "menu_switch": resources.GetAudioPlayer(audioCtx, "menu_switch.wav"), + "levelup": resources.GetAudioPlayer(audioCtx, "levelup.wav"), + "mandarin_box_full": resources.GetAudioPlayer(audioCtx, "mandarin_box_full.wav"), + "orange_put": resources.GetAudioPlayer(audioCtx, "orange_put.wav"), + "mandarin_rain_completed": resources.GetAudioPlayer(audioCtx, "mandarin_rain_completed.wav"), }, + Capybara: NewCapybara(NewSpriteFromFile("capybara_1.png")), + Background: NewSpriteFromFile("background_1.png"), FontFace: util.NewFace(fnt, &opentype.FaceOptions{ Size: 32, DPI: 72, Hinting: font.HintingVertical, }), - AnimationData: AnimationData{ - Theta: 0.0, - BounceDirectionFlag: true, - Squish: 0, - }, PassiveIncomeTicker: 0, - } -} - -// Plays sound and rewinds the player -func (g *Game) PlaySound(soundKey string) { - if strings.TrimSpace(soundKey) != "" { - g.AudioPlayers[soundKey].Rewind() - g.AudioPlayers[soundKey].Play() + MandarinRain: NewMandarinRain(3, 8), } } @@ -99,54 +77,30 @@ func (g *Game) SaveData(saveFileName string, configurationFileName string) error return nil } -// Returns how many points required to be considered of level -func pointsForLevel(level uint32) uint64 { - return 25 * uint64(level*level) -} - func (g *Game) Update() error { if ebiten.IsWindowBeingClosed() { return ebiten.Termination } - // Update configuration and save information - width, height := ebiten.WindowSize() - g.Config.WindowSize = [2]int{width, height} - - x, y := ebiten.WindowPosition() - g.Config.LastWindowPosition = [2]int{x, y} - if inpututil.IsKeyJustPressed(ebiten.KeyEscape) { // Exit return ebiten.Termination } if inpututil.IsKeyJustPressed(ebiten.KeyF12) { - if ebiten.IsFullscreen() { - // Turn fullscreen off - ebiten.SetFullscreen(false) - } else { - // Go fullscreen - ebiten.SetFullscreen(true) - } + g.ToggleFullscreen() } + g.SaveWindowGeometry() + if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) { // Decrease volume - if g.Config.Volume-0.2 >= 0 { - g.Config.Volume -= 0.2 - for _, player := range g.AudioPlayers { - player.SetVolume(g.Config.Volume) - } - } - } else if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) { + g.DecreaseVolume(0.2) + } + + if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) { // Increase volume - if g.Config.Volume+0.2 <= 1.0 { - g.Config.Volume += 0.2 - for _, player := range g.AudioPlayers { - player.SetVolume(g.Config.Volume) - } - } + g.IncreaseVolume(0.2) } if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) || @@ -154,21 +108,9 @@ func (g *Game) Update() error { // Click! g.Save.TimesClicked++ g.Save.Points++ - g.AnimationData.Squish += 0.5 g.PlaySound("woop") } - // Capybara Animation - if g.AnimationData.Theta >= 0.03 { - g.AnimationData.BounceDirectionFlag = false - } else if g.AnimationData.Theta <= -0.03 { - g.AnimationData.BounceDirectionFlag = true - } - - if g.AnimationData.Squish >= 0 { - g.AnimationData.Squish -= 0.05 - } - // Passive points income if g.PassiveIncomeTicker == ebiten.TPS() { g.PassiveIncomeTicker = 0 @@ -184,6 +126,25 @@ func (g *Game) Update() error { g.PlaySound("levelup") } + // Capybara animation update + g.Capybara.Update() + + if !g.MandarinRain.InProgress && g.Save.TimesClicked > 0 && g.Save.TimesClicked%100 == 0 { + // Have some oranges! + g.MandarinRain.Run() + logger.Info("Started mandarin rain at %d points!", g.Save.Points) + } + + if g.MandarinRain.InProgress { + // Calculate mandarin rain logic for this step + g.MandarinRain.Update(g) + } + + if g.MandarinRain.Completed { + // Prepare a new mandarin rain + g.MandarinRain = NewMandarinRain(3, 8) + } + return nil } @@ -191,51 +152,21 @@ func (g *Game) Draw(screen *ebiten.Image) { // Background screen.Fill(color.Black) - backBounds := g.ImageResources["background1"].Bounds() op := &ebiten.DrawImageOptions{} op.GeoM.Scale( - float64(screen.Bounds().Dx())/float64(backBounds.Dx()), - float64(screen.Bounds().Dy())/float64(backBounds.Dy()), + float64(screen.Bounds().Dx())/float64(g.Background.Img.Bounds().Dx()), + float64(screen.Bounds().Dy())/float64(g.Background.Img.Bounds().Dy()), ) - screen.DrawImage(g.ImageResources["background1"], op) + screen.DrawImage(g.Background.Img, op) // Capybara - var capybaraKey string - switch g.Save.Level { - case 1: - capybaraKey = "capybara1" - case 2: - capybaraKey = "capybara2" - case 3: - capybaraKey = "capybara3" - default: - capybaraKey = "capybara3" - } + g.Capybara.Draw(screen, g.Save.Level) - op = &ebiten.DrawImageOptions{} - if g.AnimationData.BounceDirectionFlag { - g.AnimationData.Theta += 0.001 - } else { - g.AnimationData.Theta -= 0.001 + // Mandarin rain + if g.MandarinRain.InProgress { + g.MandarinRain.Draw(screen) } - capybaraBounds := g.ImageResources[capybaraKey].Bounds() - scale := float64(screen.Bounds().Dx()) / float64(capybaraBounds.Dx()) / 2.5 - op.GeoM.Scale( - scale+g.AnimationData.Squish, - scale-g.AnimationData.Squish, - ) - op.GeoM.Rotate(g.AnimationData.Theta) - - capyWidth := float64(g.ImageResources[capybaraKey].Bounds().Dx()) * scale - capyHeight := float64(g.ImageResources[capybaraKey].Bounds().Dy()) * scale - op.GeoM.Translate( - float64(screen.Bounds().Dx()/2)-capyWidth/2, - float64(screen.Bounds().Dy()/2)-capyHeight/2, - ) - - screen.DrawImage(g.ImageResources[capybaraKey], op) - // Points msg := fmt.Sprintf("Points: %d", g.Save.Points) text.Draw( diff --git a/src/game/level.go b/src/game/level.go new file mode 100644 index 0000000..314a448 --- /dev/null +++ b/src/game/level.go @@ -0,0 +1,6 @@ +package game + +// Returns how many points required to be considered of level +func pointsForLevel(level uint32) uint64 { + return 25 * uint64(level*level) +} diff --git a/src/game/mandarinRain.go b/src/game/mandarinRain.go new file mode 100644 index 0000000..edaa04f --- /dev/null +++ b/src/game/mandarinRain.go @@ -0,0 +1,258 @@ +package game + +import ( + "image" + "math/rand" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/inpututil" +) + +type MandarinRain struct { + InProgress bool + MandarinBox *Physical + Mandarins []*Physical + Completed bool + mandarinCount uint16 + mandarinInitialCount uint16 + mandarinsInBox uint16 + boxFull bool + mandarinCountRange [2]uint16 + screenBounds image.Rectangle +} + +func NewMandarinRain(from uint16, to uint16) *MandarinRain { + rain := MandarinRain{} + rain.screenBounds = WindowBounds() + rain.InProgress = false + rain.mandarinInitialCount = uint16(rand.Int31n(int32(to-from)) + int32(from)) + rain.mandarinCountRange = [2]uint16{from, to} + rain.mandarinCount = rain.mandarinInitialCount + rain.mandarinsInBox = 0 + rain.boxFull = false + rain.Completed = false + + rain.Mandarins = make([]*Physical, rain.mandarinInitialCount) + for i := 0; i < int(rain.mandarinInitialCount); i++ { + rain.Mandarins[i] = NewPhysical(NewSpriteFromFile("mandarin_orange.png"), 10.0) + } + + rain.MandarinBox = NewPhysical(NewSpriteFromFile("mandarin_box_empty.png"), 5.5) + + return &rain +} + +func (mr *MandarinRain) Run() { + if mr.InProgress { + return + } + + mr.screenBounds = WindowBounds() + mr.InProgress = true + + // Move oranges to random positions on the top of the screen + for _, orange := range mr.Mandarins { + orange.Sprite.MoveTo(float64(rand.Int31n(int32(mr.screenBounds.Bounds().Dx()-orange.Sprite.RealBounds().Dx()))), 10.0) + } + + // Create mandarin box + mr.MandarinBox.Sprite.MoveTo( + float64(rand.Int31n(int32(mr.screenBounds.Bounds().Dx()-mr.MandarinBox.Sprite.RealBounds().Dx()))), + 10.0, + ) +} + +func (mr *MandarinRain) Update(game *Game) { + mr.screenBounds = WindowBounds() + + cPosX, cPosY := ebiten.CursorPosition() + var tPosX int = 0 + var tPosY int = 0 + if len(ebiten.AppendTouchIDs(nil)) != 0 { + tPosX, tPosY = ebiten.TouchPosition(ebiten.AppendTouchIDs(nil)[0]) + } + + // Oranges + temp := mr.Mandarins[:0] + for _, orange := range mr.Mandarins { + orange.Acceleration.Vx = 0.0 + orange.Acceleration.Vy = 9.81 / orange.Mass + + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) && + orange.InVicinity(float64(cPosX), float64(cPosY), 75.0) { + difference := newVec2f( + (float64(cPosX-orange.Sprite.RealBounds().Dx()/2)-orange.Sprite.X)*4.5, + (float64(cPosY-orange.Sprite.RealBounds().Dy()/2)-orange.Sprite.Y)*4.5, + ) + + orange.Acceleration.Vx = difference.Vx / orange.Mass + orange.Acceleration.Vy = difference.Vy / orange.Mass + } else if len(inpututil.AppendJustPressedTouchIDs(nil)) != 0 && + orange.InVicinity(float64(tPosX), float64(tPosY), 75.0) { + + tPosX, tPosY := ebiten.TouchPosition(ebiten.AppendTouchIDs(nil)[0]) + difference := newVec2f( + (float64(tPosX-orange.Sprite.RealBounds().Dx()/2)-orange.Sprite.X)*4.5, + (float64(tPosY-orange.Sprite.RealBounds().Dy()/2)-orange.Sprite.Y)*4.5, + ) + + orange.Acceleration.Vx = difference.Vx / orange.Mass + orange.Acceleration.Vy = difference.Vy / orange.Mass + } + + orange.Velocity.Vx = orange.Velocity.Vx + orange.Acceleration.Vx*0.05 + orange.Velocity.Vy = orange.Velocity.Vy + orange.Acceleration.Vy*0.05 + + oBounds := orange.Sprite.RealBounds() + oX := orange.Sprite.X + oY := orange.Sprite.Y + + // Constraints + // Right + if oX+float64(oBounds.Dx()) >= float64(mr.screenBounds.Dx()) { + orange.Velocity.Vx = -orange.Velocity.Vx * 0.4 + } + + // Left + if oX <= 0 { + orange.Velocity.Vx = -orange.Velocity.Vx * 0.4 + } + + // Up + if oY <= 0.0 { + orange.Velocity.Vy = -orange.Velocity.Vy * 0.4 + } + + // Bottom + if oY+float64(oBounds.Dy()) >= float64(mr.screenBounds.Dy()) { + orange.Velocity.Vx = orange.Velocity.Vx * 0.4 // friction on the floor + orange.Velocity.Vy = -orange.Velocity.Vy * 0.4 + } + + orange.Sprite.X += orange.Velocity.Vx + orange.Sprite.Y += orange.Velocity.Vy + + // Move the orange + orange.Sprite.MoveTo(orange.Sprite.X, orange.Sprite.Y) + + // Check whether it touches mandarin box + if orange.InVicinity(mr.MandarinBox.Sprite.X, mr.MandarinBox.Sprite.Y, float64(mr.MandarinBox.Sprite.RealBounds().Dx())) { + // Yes! + mr.mandarinsInBox++ + mr.mandarinCount-- + game.PlaySound("orange_put") + } else { + // Do not include this orange in the next update (effectively, delete it) + temp = append(temp, orange) + } + } + mr.Mandarins = temp + + // Orange box + mr.MandarinBox.Acceleration.Vx = 0.0 + mr.MandarinBox.Acceleration.Vy = 9.81 / mr.MandarinBox.Mass + + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) && + mr.MandarinBox.InVicinity(float64(cPosX), float64(cPosY), 75.0) { + difference := newVec2f( + (float64(cPosX-mr.MandarinBox.Sprite.RealBounds().Dx()/2)-mr.MandarinBox.Sprite.X)*3.5, + (float64(cPosY-mr.MandarinBox.Sprite.RealBounds().Dy()/2)-mr.MandarinBox.Sprite.Y)*3.5, + ) + + mr.MandarinBox.Acceleration.Vx = difference.Vx / mr.MandarinBox.Mass + mr.MandarinBox.Acceleration.Vy = difference.Vy / mr.MandarinBox.Mass + } else if len(inpututil.AppendJustPressedTouchIDs(nil)) != 0 && + mr.MandarinBox.InVicinity(float64(tPosX), float64(tPosY), 75.0) { + + tPosX, tPosY := ebiten.TouchPosition(ebiten.AppendTouchIDs(nil)[0]) + difference := newVec2f( + (float64(tPosX-mr.MandarinBox.Sprite.RealBounds().Dx()/2)-mr.MandarinBox.Sprite.X)*3.5, + (float64(tPosY-mr.MandarinBox.Sprite.RealBounds().Dy()/2)-mr.MandarinBox.Sprite.Y)*3.5, + ) + + mr.MandarinBox.Acceleration.Vx = difference.Vx / mr.MandarinBox.Mass + mr.MandarinBox.Acceleration.Vy = difference.Vy / mr.MandarinBox.Mass + } + + mr.MandarinBox.Velocity.Vx = mr.MandarinBox.Velocity.Vx + mr.MandarinBox.Acceleration.Vx*0.05 + mr.MandarinBox.Velocity.Vy = mr.MandarinBox.Velocity.Vy + mr.MandarinBox.Acceleration.Vy*0.05 + + mBounds := mr.MandarinBox.Sprite.RealBounds() + mX := mr.MandarinBox.Sprite.X + mY := mr.MandarinBox.Sprite.Y + + // Right + if mX+float64(mBounds.Dx()) >= float64(mr.screenBounds.Dx()) { + mr.MandarinBox.Velocity.Vx = -mr.MandarinBox.Velocity.Vx * 0.3 + } + + // Left + if mX <= 0 { + mr.MandarinBox.Velocity.Vx = -mr.MandarinBox.Velocity.Vx * 0.3 + } + + // Up + if mY <= 0.0 { + mr.MandarinBox.Velocity.Vy = -mr.MandarinBox.Velocity.Vy * 0.3 + } + + // Bottom + if mY+float64(mBounds.Dy()) >= float64(mr.screenBounds.Dy()) { + mr.MandarinBox.Velocity.Vx = mr.MandarinBox.Velocity.Vx * 0.3 // friction on the floor + mr.MandarinBox.Velocity.Vy = -mr.MandarinBox.Velocity.Vy * 0.3 + } + + mr.MandarinBox.Sprite.X += mr.MandarinBox.Velocity.Vx + mr.MandarinBox.Sprite.Y += mr.MandarinBox.Velocity.Vy + + // Move box + mr.MandarinBox.Sprite.MoveTo(mr.MandarinBox.Sprite.X, mr.MandarinBox.Sprite.Y) + + if mr.mandarinsInBox == mr.mandarinInitialCount && !mr.boxFull { + // All oranges are in a box! + mr.boxFull = true + game.PlaySound("mandarin_box_full") + } + + // If the box is full with mandarines and is near capybara - end mandarin rain and reward with points! + if mr.boxFull && mr.MandarinBox.InVicinity( + game.Capybara.Sprite.X+float64(game.Capybara.Sprite.RealBounds().Dx()/2), + game.Capybara.Sprite.Y+float64(game.Capybara.Sprite.RealBounds().Dy()/2), + float64(mr.screenBounds.Dx())/7) { + // Give a reward and finish this mandarin rain! + game.Save.Points += pointsForLevel(game.Save.Level+1) / 5 + game.PlaySound("mandarin_rain_completed") + mr.InProgress = false + mr.Completed = true + } +} + +func (mr *MandarinRain) Draw(screen *ebiten.Image) { + if mr.InProgress { + // Mandarin box + if mr.mandarinsInBox < mr.mandarinInitialCount && mr.mandarinsInBox > 0 { + mr.MandarinBox.Sprite.ChangeImageByName("mandarin_box_not_empty.png") + } else if mr.mandarinsInBox == mr.mandarinInitialCount { + mr.MandarinBox.Sprite.ChangeImageByName("mandarin_box_full.png") + } else { + mr.MandarinBox.Sprite.ChangeImageByName("mandarin_box_empty.png") + } + + op := &ebiten.DrawImageOptions{} + scale := float64(screen.Bounds().Dx()) / float64(mr.MandarinBox.Sprite.Img.Bounds().Dx()) / 6.0 + mr.MandarinBox.Sprite.Scale = scale // Save current scale for proper collision detection + op.GeoM.Scale(scale, scale) + op.GeoM.Translate(mr.MandarinBox.Sprite.X, mr.MandarinBox.Sprite.Y) + screen.DrawImage(mr.MandarinBox.Sprite.Img, op) + + // Oranges + for _, orange := range mr.Mandarins { + op = &ebiten.DrawImageOptions{} + scale = float64(screen.Bounds().Dx()) / float64(orange.Sprite.Img.Bounds().Dx()) / 11.5 + orange.Sprite.Scale = scale // Save current scale for proper collision detection + op.GeoM.Scale(scale, scale) + op.GeoM.Translate(orange.Sprite.X, orange.Sprite.Y) + screen.DrawImage(orange.Sprite.Img, op) + } + } +} diff --git a/src/game/physical.go b/src/game/physical.go new file mode 100644 index 0000000..94c10cb --- /dev/null +++ b/src/game/physical.go @@ -0,0 +1,40 @@ +package game + +import "math" + +type Vec2f struct { + Vx float64 + Vy float64 +} + +func newVec2f(x float64, y float64) Vec2f { + return Vec2f{ + Vx: x, + Vy: y, + } +} + +type Physical struct { + Sprite *Sprite + Velocity Vec2f + Acceleration Vec2f + Mass float64 +} + +func NewPhysical(sprite *Sprite, mass float64) *Physical { + return &Physical{ + Sprite: sprite, + Velocity: newVec2f(0.0, 0.0), + Acceleration: newVec2f(0.0, 0.0), + Mass: 10.0, + } +} + +// Returns true if x and y coordinates are in the radius of the physical sprite +func (ph *Physical) InVicinity(x float64, y float64, radius float64) bool { + distance := math.Sqrt( + math.Pow(ph.Sprite.X-x, 2.0) + math.Pow(ph.Sprite.Y-y, 2.0), + ) + + return distance <= radius +} diff --git a/src/game/sprite.go b/src/game/sprite.go new file mode 100644 index 0000000..ffff24d --- /dev/null +++ b/src/game/sprite.go @@ -0,0 +1,92 @@ +package game + +import ( + "Unbewohnte/capyclick/resources" + "image" + + "github.com/hajimehoshi/ebiten/v2" +) + +type AnimationData struct { + Squish float64 + Theta float64 + BounceDirectionFlag bool +} + +// Drawable image structure +type Sprite struct { + Img *ebiten.Image + X float64 + Y float64 + Animation AnimationData + Scale float64 + Dragged bool +} + +func NewSprite(img image.Image) *Sprite { + return &Sprite{ + Img: ebiten.NewImageFromImage(img), + X: 0.0, + Y: 0.0, + Animation: AnimationData{ + Squish: 0.0, + Theta: 0.0, + BounceDirectionFlag: false, + }, + Scale: 1.0, + Dragged: false, + } +} + +func NewSpriteFromFile(fileName string) *Sprite { + return NewSprite(resources.ImageFromFile(fileName)) +} + +func (s *Sprite) ChangeImageByName(fileName string) { + s.Img = ebiten.NewImageFromImage(resources.ImageFromFile(fileName)) +} + +// Returns how big the image is with applied scale factor +func (s *Sprite) RealBounds() image.Rectangle { + bounds := s.Img.Bounds() + realBounds := image.Rect(0, 0, bounds.Dx()*int(s.Scale), bounds.Dy()*int(s.Scale)) + return realBounds +} + +func (s *Sprite) IsIn(x int, y int) bool { + if x >= int(s.X) && x <= (int(s.X)+s.RealBounds().Dx()) && + y >= int(s.Y) && y <= (int(s.Y)+s.RealBounds().Dy()) { + return true + } + + return false +} + +// Moves sprite to given positions. Respects window constraints +func (s *Sprite) MoveTo(x float64, y float64) { + s.X = x + s.Y = y + screenBounds := WindowBounds() + + // Constraints + // Right + if s.X+float64(s.RealBounds().Dx()) >= float64(screenBounds.Dx()) { + s.X = float64(screenBounds.Dx()) - float64(s.RealBounds().Dx()) + } + + // Left + if s.X <= 0 { + s.X = 0 + } + + // Up + if s.Y <= 0.0 { + s.Y = 0.0 + } + + // Bottom + if s.Y+float64(s.RealBounds().Dy()) >= float64(screenBounds.Dy()) { + s.Y = float64(screenBounds.Dy()) - float64(s.RealBounds().Dy()) + } + +} diff --git a/src/game/window.go b/src/game/window.go new file mode 100644 index 0000000..9b19d44 --- /dev/null +++ b/src/game/window.go @@ -0,0 +1,31 @@ +package game + +import ( + "image" + + "github.com/hajimehoshi/ebiten/v2" +) + +func (g *Game) ToggleFullscreen() { + if ebiten.IsFullscreen() { + // Turn fullscreen off + ebiten.SetFullscreen(false) + } else { + // Go fullscreen + ebiten.SetFullscreen(true) + } +} + +func (g *Game) SaveWindowGeometry() { + // Update configuration and save information + width, height := ebiten.WindowSize() + g.Config.WindowSize = [2]int{width, height} + + x, y := ebiten.WindowPosition() + g.Config.LastWindowPosition = [2]int{x, y} +} + +func WindowBounds() image.Rectangle { + x, y := ebiten.WindowSize() + return image.Rect(0, 0, x, y) +} diff --git a/src/main.go b/src/main.go index f3f8962..c4dfc50 100644 --- a/src/main.go +++ b/src/main.go @@ -28,6 +28,7 @@ import ( "flag" "fmt" "io" + "math/rand" "os" "path/filepath" "time" @@ -35,7 +36,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" ) -const Version string = "v0.1" +const Version string = "v0.1-release" var ( silent *bool = flag.Bool("silent", false, "Set to true in order to discard all logging") @@ -66,7 +67,7 @@ func main() { } // Create a game instance - var game *game.Game = game.NewGame() + var game game.Game = game.NewGame() if *saveFiles { // Work out working directory @@ -138,8 +139,11 @@ func main() { player.SetVolume(game.Config.Volume) } + // Set up RNG + rand.Seed(time.Now().UnixNano()) + // Run the game - err := ebiten.RunGame(game) + err := ebiten.RunGame(&game) if err == ebiten.Termination || err == nil { logger.Info("[Main] Shutting down!") if *saveFiles { diff --git a/src/resources/resources/leather.wav b/src/resources/resources/leather.wav deleted file mode 100644 index 02d1ba3..0000000 Binary files a/src/resources/resources/leather.wav and /dev/null differ diff --git a/src/resources/resources/mandarin_box_empty.png b/src/resources/resources/mandarin_box_empty.png new file mode 100644 index 0000000..9bc486f Binary files /dev/null and b/src/resources/resources/mandarin_box_empty.png differ diff --git a/src/resources/resources/mandarin_box_full.png b/src/resources/resources/mandarin_box_full.png new file mode 100644 index 0000000..dc9c4d1 Binary files /dev/null and b/src/resources/resources/mandarin_box_full.png differ diff --git a/src/resources/resources/mandarin_box_full.wav b/src/resources/resources/mandarin_box_full.wav new file mode 100644 index 0000000..5a008a9 Binary files /dev/null and b/src/resources/resources/mandarin_box_full.wav differ diff --git a/src/resources/resources/mandarin_box_not_empty.png b/src/resources/resources/mandarin_box_not_empty.png new file mode 100644 index 0000000..f592d29 Binary files /dev/null and b/src/resources/resources/mandarin_box_not_empty.png differ diff --git a/src/resources/resources/mandarin_orange.png b/src/resources/resources/mandarin_orange.png new file mode 100644 index 0000000..3bf54e9 Binary files /dev/null and b/src/resources/resources/mandarin_orange.png differ diff --git a/src/resources/resources/mandarin_rain_completed.wav b/src/resources/resources/mandarin_rain_completed.wav new file mode 100644 index 0000000..656dd9f Binary files /dev/null and b/src/resources/resources/mandarin_rain_completed.wav differ diff --git a/src/resources/resources/menu_switch.wav b/src/resources/resources/menu_switch.wav deleted file mode 100644 index b050e7e..0000000 Binary files a/src/resources/resources/menu_switch.wav and /dev/null differ diff --git a/src/resources/resources/orange_put.wav b/src/resources/resources/orange_put.wav new file mode 100644 index 0000000..3cb563f Binary files /dev/null and b/src/resources/resources/orange_put.wav differ