Browse Source

FEATURE: Mandarin rain!; First release version

master
parent
commit
071857f639
  1. 46
      src/game/audio.go
  2. 73
      src/game/capybara.go
  3. 173
      src/game/game.go
  4. 6
      src/game/level.go
  5. 258
      src/game/mandarinRain.go
  6. 40
      src/game/physical.go
  7. 92
      src/game/sprite.go
  8. 31
      src/game/window.go
  9. 10
      src/main.go
  10. BIN
      src/resources/resources/leather.wav
  11. BIN
      src/resources/resources/mandarin_box_empty.png
  12. BIN
      src/resources/resources/mandarin_box_full.png
  13. BIN
      src/resources/resources/mandarin_box_full.wav
  14. BIN
      src/resources/resources/mandarin_box_not_empty.png
  15. BIN
      src/resources/resources/mandarin_orange.png
  16. BIN
      src/resources/resources/mandarin_rain_completed.wav
  17. BIN
      src/resources/resources/menu_switch.wav
  18. BIN
      src/resources/resources/orange_put.wav

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

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

173
src/game/game.go

@ -9,7 +9,6 @@ import (
"fmt" "fmt"
"image/color" "image/color"
"path/filepath" "path/filepath"
"strings"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio" "github.com/hajimehoshi/ebiten/v2/audio"
@ -19,65 +18,44 @@ import (
"golang.org/x/image/font/opentype" "golang.org/x/image/font/opentype"
) )
type AnimationData struct {
Squish float64
Theta float64
BounceDirectionFlag bool
}
type Game struct { type Game struct {
WorkingDir string WorkingDir string
Config conf.Configuration Config conf.Configuration
Save save.Save Save save.Save
AudioContext *audio.Context
AudioPlayers map[string]*audio.Player AudioPlayers map[string]*audio.Player
ImageResources map[string]*ebiten.Image
FontFace font.Face FontFace font.Face
AnimationData AnimationData
PassiveIncomeTicker int PassiveIncomeTicker int
Capybara *Capybara
Background *Sprite
MandarinRain *MandarinRain
} }
func NewGame() *Game { func NewGame() Game {
audioCtx := audio.NewContext(44000) audioCtx := audio.NewContext(44000)
fnt := resources.GetFont("PixeloidSans-Bold.otf") fnt := resources.GetFont("PixeloidSans-Bold.otf")
return &Game{ return Game{
WorkingDir: ".", WorkingDir: ".",
Config: conf.Default(), Config: conf.Default(),
Save: save.Default(), Save: save.Default(),
AudioContext: audioCtx,
AudioPlayers: map[string]*audio.Player{ AudioPlayers: map[string]*audio.Player{
"boop": resources.GetAudioPlayer(audioCtx, "boop.wav"), "boop": resources.GetAudioPlayer(audioCtx, "boop.wav"),
"woop": resources.GetAudioPlayer(audioCtx, "woop.wav"), "woop": resources.GetAudioPlayer(audioCtx, "woop.wav"),
"menu_switch": resources.GetAudioPlayer(audioCtx, "menu_switch.wav"), "menu_switch": resources.GetAudioPlayer(audioCtx, "menu_switch.wav"),
"levelup": resources.GetAudioPlayer(audioCtx, "levelup.wav"), "levelup": resources.GetAudioPlayer(audioCtx, "levelup.wav"),
}, "mandarin_box_full": resources.GetAudioPlayer(audioCtx, "mandarin_box_full.wav"),
ImageResources: map[string]*ebiten.Image{ "orange_put": resources.GetAudioPlayer(audioCtx, "orange_put.wav"),
"capybara1": ebiten.NewImageFromImage(resources.ImageFromFile("capybara_1.png")), "mandarin_rain_completed": resources.GetAudioPlayer(audioCtx, "mandarin_rain_completed.wav"),
"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")),
}, },
Capybara: NewCapybara(NewSpriteFromFile("capybara_1.png")),
Background: NewSpriteFromFile("background_1.png"),
FontFace: util.NewFace(fnt, &opentype.FaceOptions{ FontFace: util.NewFace(fnt, &opentype.FaceOptions{
Size: 32, Size: 32,
DPI: 72, DPI: 72,
Hinting: font.HintingVertical, Hinting: font.HintingVertical,
}), }),
AnimationData: AnimationData{
Theta: 0.0,
BounceDirectionFlag: true,
Squish: 0,
},
PassiveIncomeTicker: 0, PassiveIncomeTicker: 0,
} MandarinRain: NewMandarinRain(3, 8),
}
// Plays sound and rewinds the player
func (g *Game) PlaySound(soundKey string) {
if strings.TrimSpace(soundKey) != "" {
g.AudioPlayers[soundKey].Rewind()
g.AudioPlayers[soundKey].Play()
} }
} }
@ -99,54 +77,30 @@ func (g *Game) SaveData(saveFileName string, configurationFileName string) error
return nil 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 { func (g *Game) Update() error {
if ebiten.IsWindowBeingClosed() { if ebiten.IsWindowBeingClosed() {
return ebiten.Termination 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) { if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
// Exit // Exit
return ebiten.Termination return ebiten.Termination
} }
if inpututil.IsKeyJustPressed(ebiten.KeyF12) { if inpututil.IsKeyJustPressed(ebiten.KeyF12) {
if ebiten.IsFullscreen() { g.ToggleFullscreen()
// Turn fullscreen off
ebiten.SetFullscreen(false)
} else {
// Go fullscreen
ebiten.SetFullscreen(true)
}
} }
g.SaveWindowGeometry()
if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) { if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) {
// Decrease volume // Decrease volume
if g.Config.Volume-0.2 >= 0 { g.DecreaseVolume(0.2)
g.Config.Volume -= 0.2 }
for _, player := range g.AudioPlayers {
player.SetVolume(g.Config.Volume) if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
}
}
} else if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
// Increase volume // Increase volume
if g.Config.Volume+0.2 <= 1.0 { g.IncreaseVolume(0.2)
g.Config.Volume += 0.2
for _, player := range g.AudioPlayers {
player.SetVolume(g.Config.Volume)
}
}
} }
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) || if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) ||
@ -154,21 +108,9 @@ func (g *Game) Update() error {
// Click! // Click!
g.Save.TimesClicked++ g.Save.TimesClicked++
g.Save.Points++ g.Save.Points++
g.AnimationData.Squish += 0.5
g.PlaySound("woop") 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 // Passive points income
if g.PassiveIncomeTicker == ebiten.TPS() { if g.PassiveIncomeTicker == ebiten.TPS() {
g.PassiveIncomeTicker = 0 g.PassiveIncomeTicker = 0
@ -184,6 +126,25 @@ func (g *Game) Update() error {
g.PlaySound("levelup") 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 return nil
} }
@ -191,51 +152,21 @@ func (g *Game) Draw(screen *ebiten.Image) {
// Background // Background
screen.Fill(color.Black) screen.Fill(color.Black)
backBounds := g.ImageResources["background1"].Bounds()
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
op.GeoM.Scale( op.GeoM.Scale(
float64(screen.Bounds().Dx())/float64(backBounds.Dx()), float64(screen.Bounds().Dx())/float64(g.Background.Img.Bounds().Dx()),
float64(screen.Bounds().Dy())/float64(backBounds.Dy()), float64(screen.Bounds().Dy())/float64(g.Background.Img.Bounds().Dy()),
) )
screen.DrawImage(g.ImageResources["background1"], op) screen.DrawImage(g.Background.Img, op)
// Capybara // Capybara
var capybaraKey string g.Capybara.Draw(screen, g.Save.Level)
switch g.Save.Level {
case 1:
capybaraKey = "capybara1"
case 2:
capybaraKey = "capybara2"
case 3:
capybaraKey = "capybara3"
default:
capybaraKey = "capybara3"
}
op = &ebiten.DrawImageOptions{} // Mandarin rain
if g.AnimationData.BounceDirectionFlag { if g.MandarinRain.InProgress {
g.AnimationData.Theta += 0.001 g.MandarinRain.Draw(screen)
} else {
g.AnimationData.Theta -= 0.001
} }
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 // Points
msg := fmt.Sprintf("Points: %d", g.Save.Points) msg := fmt.Sprintf("Points: %d", g.Save.Points)
text.Draw( text.Draw(

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

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

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

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

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

10
src/main.go

@ -28,6 +28,7 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"math/rand"
"os" "os"
"path/filepath" "path/filepath"
"time" "time"
@ -35,7 +36,7 @@ import (
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
) )
const Version string = "v0.1" const Version string = "v0.1-release"
var ( var (
silent *bool = flag.Bool("silent", false, "Set to true in order to discard all logging") 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 // Create a game instance
var game *game.Game = game.NewGame() var game game.Game = game.NewGame()
if *saveFiles { if *saveFiles {
// Work out working directory // Work out working directory
@ -138,8 +139,11 @@ func main() {
player.SetVolume(game.Config.Volume) player.SetVolume(game.Config.Volume)
} }
// Set up RNG
rand.Seed(time.Now().UnixNano())
// Run the game // Run the game
err := ebiten.RunGame(game) err := ebiten.RunGame(&game)
if err == ebiten.Termination || err == nil { if err == ebiten.Termination || err == nil {
logger.Info("[Main] Shutting down!") logger.Info("[Main] Shutting down!")
if *saveFiles { if *saveFiles {

BIN
src/resources/resources/leather.wav

Binary file not shown.

BIN
src/resources/resources/mandarin_box_empty.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

BIN
src/resources/resources/mandarin_box_full.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 778 B

BIN
src/resources/resources/mandarin_box_full.wav

Binary file not shown.

BIN
src/resources/resources/mandarin_box_not_empty.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

BIN
src/resources/resources/mandarin_orange.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 624 B

BIN
src/resources/resources/mandarin_rain_completed.wav

Binary file not shown.

BIN
src/resources/resources/menu_switch.wav

Binary file not shown.

BIN
src/resources/resources/orange_put.wav

Binary file not shown.
Loading…
Cancel
Save