@ -0,0 +1,291 @@ |
|||||||
|
package game |
||||||
|
|
||||||
|
import ( |
||||||
|
"Unbewohnte/capyclick/conf" |
||||||
|
"Unbewohnte/capyclick/logger" |
||||||
|
"Unbewohnte/capyclick/resources" |
||||||
|
"Unbewohnte/capyclick/save" |
||||||
|
"Unbewohnte/capyclick/util" |
||||||
|
"fmt" |
||||||
|
"image/color" |
||||||
|
"path/filepath" |
||||||
|
"strings" |
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten/v2" |
||||||
|
"github.com/hajimehoshi/ebiten/v2/audio" |
||||||
|
"github.com/hajimehoshi/ebiten/v2/inpututil" |
||||||
|
"github.com/hajimehoshi/ebiten/v2/text" |
||||||
|
"golang.org/x/image/font" |
||||||
|
"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 |
||||||
|
} |
||||||
|
|
||||||
|
func NewGame() *Game { |
||||||
|
audioCtx := audio.NewContext(44000) |
||||||
|
fnt := resources.GetFont("PixeloidSans-Bold.otf") |
||||||
|
|
||||||
|
return &Game{ |
||||||
|
WorkingDir: ".", |
||||||
|
Config: conf.Default(), |
||||||
|
Save: save.Default(), |
||||||
|
AudioContext: audioCtx, |
||||||
|
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")), |
||||||
|
}, |
||||||
|
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() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Saves configuration information and game data
|
||||||
|
func (g *Game) SaveData(saveFileName string, configurationFileName string) error { |
||||||
|
// Save configuration information and game data
|
||||||
|
err := save.Create(filepath.Join(g.WorkingDir, saveFileName), g.Save) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[SaveData] Failed to save game data before closing: %s!", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
err = conf.Create(filepath.Join(g.WorkingDir, configurationFileName), g.Config) |
||||||
|
if err != nil { |
||||||
|
logger.Error("[SaveData] Failed to save game configuration before closing: %s!", err) |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
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) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
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) { |
||||||
|
// 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) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) || |
||||||
|
len(inpututil.AppendJustPressedTouchIDs(nil)) != 0 { |
||||||
|
// 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 |
||||||
|
g.Save.Points += g.Save.PassiveIncome |
||||||
|
} else { |
||||||
|
g.PassiveIncomeTicker++ |
||||||
|
} |
||||||
|
|
||||||
|
if g.Save.Points > 0 && g.Save.Points >= pointsForLevel(g.Save.Level+1) { |
||||||
|
// Level progression
|
||||||
|
g.Save.Level++ |
||||||
|
g.Save.PassiveIncome++ |
||||||
|
g.PlaySound("levelup") |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
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()), |
||||||
|
) |
||||||
|
screen.DrawImage(g.ImageResources["background1"], op) |
||||||
|
|
||||||
|
// Capybara
|
||||||
|
var capybaraKey string |
||||||
|
switch g.Save.Level { |
||||||
|
case 1: |
||||||
|
capybaraKey = "capybara1" |
||||||
|
case 2: |
||||||
|
capybaraKey = "capybara2" |
||||||
|
case 3: |
||||||
|
capybaraKey = "capybara3" |
||||||
|
default: |
||||||
|
capybaraKey = "capybara3" |
||||||
|
} |
||||||
|
|
||||||
|
op = &ebiten.DrawImageOptions{} |
||||||
|
if g.AnimationData.BounceDirectionFlag { |
||||||
|
g.AnimationData.Theta += 0.001 |
||||||
|
} 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
|
||||||
|
msg := fmt.Sprintf("Points: %d", g.Save.Points) |
||||||
|
text.Draw( |
||||||
|
screen, |
||||||
|
msg, |
||||||
|
g.FontFace, |
||||||
|
10, |
||||||
|
g.FontFace.Metrics().Height.Ceil(), |
||||||
|
color.White, |
||||||
|
) |
||||||
|
|
||||||
|
// Level
|
||||||
|
msg = fmt.Sprintf( |
||||||
|
"Level: %d (+%d)", |
||||||
|
g.Save.Level, |
||||||
|
pointsForLevel(g.Save.Level+1)-g.Save.Points, |
||||||
|
) |
||||||
|
text.Draw( |
||||||
|
screen, |
||||||
|
msg, |
||||||
|
g.FontFace, |
||||||
|
10, |
||||||
|
g.FontFace.Metrics().Height.Ceil()*2, |
||||||
|
color.White, |
||||||
|
) |
||||||
|
|
||||||
|
// Times Clicked
|
||||||
|
msg = fmt.Sprintf("Clicks: %d", g.Save.TimesClicked) |
||||||
|
text.Draw( |
||||||
|
screen, |
||||||
|
msg, |
||||||
|
g.FontFace, |
||||||
|
10, |
||||||
|
screen.Bounds().Dy()-g.FontFace.Metrics().Height.Ceil()*2, |
||||||
|
color.White, |
||||||
|
) |
||||||
|
|
||||||
|
// Volume
|
||||||
|
msg = fmt.Sprintf("Volume: %d%% (← or →)", int(g.Config.Volume*100.0)) |
||||||
|
text.Draw( |
||||||
|
screen, |
||||||
|
msg, |
||||||
|
g.FontFace, |
||||||
|
10, |
||||||
|
screen.Bounds().Dy()-g.FontFace.Metrics().Height.Ceil(), |
||||||
|
color.White, |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (screenWidth, screenHeight int) { |
||||||
|
scaleFactor := ebiten.DeviceScaleFactor() |
||||||
|
return int(float64(outsideWidth) * scaleFactor), int(float64(outsideHeight) * scaleFactor) |
||||||
|
} |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 202 B After Width: | Height: | Size: 202 B |
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 457 B |
Before Width: | Height: | Size: 508 B After Width: | Height: | Size: 508 B |