Browse Source

CODE: Debloated files by moving parts of logic into separate packages

master
parent
commit
d40fc0e6b8
  1. 291
      src/game/game.go
  2. 1
      src/go.mod
  3. 1
      src/go.sum
  4. 281
      src/main.go
  5. 12
      src/resources/resources.go
  6. 0
      src/resources/resources/PixeloidMono.otf
  7. 0
      src/resources/resources/PixeloidSans-Bold.otf
  8. 0
      src/resources/resources/Thirteen-Pixel-Fonts.ttf
  9. 0
      src/resources/resources/background_1.png
  10. 0
      src/resources/resources/background_2.png
  11. 0
      src/resources/resources/boop.wav
  12. 0
      src/resources/resources/capybara_1.png
  13. 0
      src/resources/resources/capybara_2.png
  14. 0
      src/resources/resources/capybara_3.png
  15. 0
      src/resources/resources/leather.wav
  16. 0
      src/resources/resources/levelup.wav
  17. 0
      src/resources/resources/menu_switch.wav
  18. 0
      src/resources/resources/pixeloid.zip
  19. 0
      src/resources/resources/thirteen-pixel-fonts.zip
  20. 0
      src/resources/resources/woop.wav

291
src/game/game.go

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

1
src/go.mod

@ -12,6 +12,7 @@ require (
github.com/ebitengine/purego v0.5.0 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200707082815-5321531c36a2 // indirect
github.com/hajimehoshi/ebiten v1.12.12 // indirect
github.com/hajimehoshi/oto v0.6.8 // indirect
github.com/jezek/xgb v1.1.0 // indirect
golang.org/x/exp/shiny v0.0.0-20230817173708-d852ddb80c63 // indirect
golang.org/x/mobile v0.0.0-20230922142353-e2f452493d57 // indirect

1
src/go.sum

@ -17,6 +17,7 @@ github.com/hajimehoshi/ebiten/v2 v2.6.5/go.mod h1:TZtorL713an00UW4LyvMeKD8uXWnuI
github.com/hajimehoshi/file2byteslice v0.0.0-20200812174855-0e5e8a80490e/go.mod h1:CqqAHp7Dk/AqQiwuhV1yT2334qbA/tFWQW0MD2dGqUE=
github.com/hajimehoshi/go-mp3 v0.3.1/go.mod h1:qMJj/CSDxx6CGHiZeCgbiq2DSUkbK0UbtXShQcnfyMM=
github.com/hajimehoshi/oto v0.6.1/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/hajimehoshi/oto v0.6.8 h1:yRb3EJQ4lAkBgZYheqmdH6Lr77RV9nSWFsK/jwWdTNY=
github.com/hajimehoshi/oto v0.6.8/go.mod h1:0QXGEkbuJRohbJaxr7ZQSxnju7hEhseiPx2hrh6raOI=
github.com/jakecoffman/cp v1.0.0/go.mod h1:JjY/Fp6d8E1CHnu74gWNnU0+b9VzEdUVPoJxg2PsTQg=
github.com/jezek/xgb v1.1.0 h1:wnpxJzP1+rkbGclEkmwpVFQWpuE2PUGNUzP8SbfFobk=

281
src/main.go

@ -20,24 +20,19 @@ package main
import (
"Unbewohnte/capyclick/conf"
"Unbewohnte/capyclick/game"
"Unbewohnte/capyclick/logger"
"Unbewohnte/capyclick/resources"
"Unbewohnte/capyclick/save"
"Unbewohnte/capyclick/util"
"flag"
"fmt"
"image/color"
"io"
"os"
"path/filepath"
"strings"
"time"
"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"
)
const Version string = "v0.1"
@ -53,272 +48,6 @@ const (
SaveFileName string = "capyclickSave.json"
)
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 := ResourceGetFont("PixeloidSans-Bold.otf")
return &Game{
WorkingDir: ".",
Config: conf.Default(),
Save: save.Default(),
AudioContext: audioCtx,
AudioPlayers: map[string]*audio.Player{
"boop": GetAudioPlayer(audioCtx, "boop.wav"),
"woop": GetAudioPlayer(audioCtx, "woop.wav"),
"menu_switch": GetAudioPlayer(audioCtx, "menu_switch.wav"),
"levelup": GetAudioPlayer(audioCtx, "levelup.wav"),
},
ImageResources: map[string]*ebiten.Image{
"capybara1": ebiten.NewImageFromImage(ImageFromFile("capybara_1.png")),
"capybara2": ebiten.NewImageFromImage(ImageFromFile("capybara_2.png")),
"capybara3": ebiten.NewImageFromImage(ImageFromFile("capybara_3.png")),
"background1": ebiten.NewImageFromImage(ImageFromFile("background_1.png")),
"background2": ebiten.NewImageFromImage(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() 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
}
func getPointsForLevel(level uint32) uint64 {
return 100 * uint64(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("boop")
}
// 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 >= getPointsForLevel(g.Save.Level) {
// 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", g.Save.Level)
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)
}
func main() {
// Set logging output
logger.SetOutput(os.Stdout)
@ -337,7 +66,7 @@ func main() {
}
// Create a game instance
var game *Game = NewGame()
var game *game.Game = game.NewGame()
if *saveFiles {
// Work out working directory
@ -372,7 +101,7 @@ func main() {
}
// Set up window options
ebiten.SetWindowIcon(util.GenerateIcons(ImageFromFile("capybara_2.png"), [][2]uint{
ebiten.SetWindowIcon(util.GenerateIcons(resources.ImageFromFile("capybara_2.png"), [][2]uint{
{32, 32},
}))
ebiten.SetWindowClosingHandled(true) // So we can save data
@ -414,7 +143,7 @@ func main() {
if err == ebiten.Termination || err == nil {
logger.Info("[Main] Shutting down!")
if *saveFiles {
game.SaveData()
game.SaveData(SaveFileName, ConfigurationFileName)
}
os.Exit(0)
} else {

12
src/resources.go → src/resources/resources.go

@ -16,7 +16,7 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package main
package resources
import (
"bytes"
@ -32,7 +32,7 @@ import (
var ResourcesFS embed.FS
// Reads file with given filename from embedded resources FS and returns its contents
func ResourceGet(filename string) []byte {
func Get(filename string) []byte {
data, err := ResourcesFS.ReadFile("resources/" + filename)
if err != nil {
return nil
@ -43,7 +43,7 @@ func ResourceGet(filename string) []byte {
// Returns a decoded image from an image file
func ImageFromFile(filename string) image.Image {
data := ResourceGet(filename)
data := Get(filename)
if data == nil {
return nil
}
@ -57,8 +57,8 @@ func ImageFromFile(filename string) image.Image {
return img
}
func ResourceGetFont(fontFile string) *sfnt.Font {
tt, err := opentype.Parse(ResourceGet(fontFile))
func GetFont(fontFile string) *sfnt.Font {
tt, err := opentype.Parse(Get(fontFile))
if err != nil {
return nil
}
@ -67,7 +67,7 @@ func ResourceGetFont(fontFile string) *sfnt.Font {
}
func GetAudioPlayer(audioContext *audio.Context, audioFile string) *audio.Player {
data := bytes.NewReader(ResourceGet(audioFile))
data := bytes.NewReader(Get(audioFile))
player, err := audioContext.NewPlayer(data)
if err != nil {
return nil

0
src/resources/PixeloidMono.otf → src/resources/resources/PixeloidMono.otf

0
src/resources/PixeloidSans-Bold.otf → src/resources/resources/PixeloidSans-Bold.otf

0
src/resources/Thirteen-Pixel-Fonts.ttf → src/resources/resources/Thirteen-Pixel-Fonts.ttf

0
src/resources/background_1.png → src/resources/resources/background_1.png

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

0
src/resources/background_2.png → src/resources/resources/background_2.png

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

0
src/resources/boop.wav → src/resources/resources/boop.wav

0
src/resources/capybara_1.png → src/resources/resources/capybara_1.png

Before

Width:  |  Height:  |  Size: 202 B

After

Width:  |  Height:  |  Size: 202 B

0
src/resources/capybara_2.png → src/resources/resources/capybara_2.png

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 457 B

0
src/resources/capybara_3.png → src/resources/resources/capybara_3.png

Before

Width:  |  Height:  |  Size: 508 B

After

Width:  |  Height:  |  Size: 508 B

0
src/resources/leather.wav → src/resources/resources/leather.wav

0
src/resources/levelup.wav → src/resources/resources/levelup.wav

0
src/resources/menu_switch.wav → src/resources/resources/menu_switch.wav

0
src/resources/pixeloid.zip → src/resources/resources/pixeloid.zip

0
src/resources/thirteen-pixel-fonts.zip → src/resources/resources/thirteen-pixel-fonts.zip

0
src/resources/woop.wav → src/resources/resources/woop.wav

Loading…
Cancel
Save