Kasianov Nikolai Alekseevich
6 months ago
18 changed files with 605 additions and 124 deletions
@ -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 |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
@ -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) |
||||
} |
||||
} |
||||
} |
@ -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 |
||||
} |
@ -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()) |
||||
} |
||||
|
||||
} |
@ -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) |
||||
} |
Binary file not shown.
After Width: | Height: | Size: 705 B |
After Width: | Height: | Size: 778 B |
Binary file not shown.
After Width: | Height: | Size: 723 B |
After Width: | Height: | Size: 624 B |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading…
Reference in new issue