Compare commits
4 Commits
99d9d09c2f
...
3c99e5881b
Author | SHA1 | Date | |
---|---|---|---|
3c99e5881b | |||
e3527eb580 | |||
cbd08cdc12 | |||
c47f9383c3 |
6
TODO.md
6
TODO.md
@ -1,12 +1,14 @@
|
|||||||
- [X] Increase difficulty.
|
- [X] Increase difficulty.
|
||||||
- [ ] Add music & sounds.
|
- [X] Add music & sounds.
|
||||||
- [X] Keep score/difficulty level (resume & restart).
|
- [X] Keep score/difficulty level (resume & restart).
|
||||||
- [X] ~~Explain controls on info page~~ add settings for controls.
|
- [X] ~~Explain controls on info page~~ add settings for controls.
|
||||||
- [X] Fix usage of go/embed (and remove rice again).
|
- [X] Fix usage of go/embed (and remove rice again).
|
||||||
- [X] Add monster animations (~~jumping on tile &~~ towards new tile).
|
- [X] Add monster animations (~~jumping on tile &~~ towards new tile).
|
||||||
- [ ] Scale icons (heart & star on right side) when playing.
|
- [X] ~~Scale icons (heart & star on right side) when playing.~~
|
||||||
- [ ] Change layout when playing in portrait mode.
|
- [ ] Change layout when playing in portrait mode.
|
||||||
- [X] Fix wobble animation.
|
- [X] Fix wobble animation.
|
||||||
- [ ] Add more unit tests?
|
- [ ] Add more unit tests?
|
||||||
- [ ] Fix z-fighting of monsters.
|
- [ ] Fix z-fighting of monsters.
|
||||||
- [ ] Add exploding animation of monsters.
|
- [ ] Add exploding animation of monsters.
|
||||||
|
- [ ] Add audio settings (music & sound volume).
|
||||||
|
- [X] Hearts must be saved as well for resume.
|
@ -61,6 +61,21 @@ func (a *app) Init(ctx ui.Context) error {
|
|||||||
})
|
})
|
||||||
a.context.ShowMainMenu(ctx)
|
a.context.ShowMainMenu(ctx)
|
||||||
|
|
||||||
|
err := a.context.Audio.LoadSample(
|
||||||
|
"level_completed.mp3",
|
||||||
|
"level_game_over.mp3",
|
||||||
|
"level_new_high_score.mp3",
|
||||||
|
"menu_interaction.mp3",
|
||||||
|
"monster_jump.mp3",
|
||||||
|
"player_collect_heart.mp3",
|
||||||
|
"player_collect_star.mp3",
|
||||||
|
"player_hurt.mp3",
|
||||||
|
"player_move.mp3",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
"opslag.de/schobers/tins2021"
|
"opslag.de/schobers/tins2021"
|
||||||
"opslag.de/schobers/zntg/ui"
|
"opslag.de/schobers/zntg/ui"
|
||||||
)
|
)
|
||||||
@ -14,8 +17,12 @@ type appContext struct {
|
|||||||
|
|
||||||
StarTexture tins2021.AnimatedTexture
|
StarTexture tins2021.AnimatedTexture
|
||||||
HeartTexture tins2021.AnimatedTexture
|
HeartTexture tins2021.AnimatedTexture
|
||||||
|
|
||||||
MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
|
MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
|
||||||
|
|
||||||
|
Audio *AudioPlayer
|
||||||
|
MenuMusic *Music
|
||||||
|
GameMusic *Music
|
||||||
|
GameMusicSong int
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.ScoreState, setView func(ui.Control)) *appContext {
|
func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.ScoreState, setView func(ui.Control)) *appContext {
|
||||||
@ -23,6 +30,7 @@ func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.
|
|||||||
app := &appContext{
|
app := &appContext{
|
||||||
setView: setView,
|
setView: setView,
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
|
Audio: NewAudioPlayer(ctx.Resources(), "resources/sounds/"),
|
||||||
Score: score,
|
Score: score,
|
||||||
StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star),
|
StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star),
|
||||||
HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart),
|
HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart),
|
||||||
@ -38,12 +46,16 @@ func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.
|
|||||||
|
|
||||||
const numberOfStars = 5
|
const numberOfStars = 5
|
||||||
|
|
||||||
|
func (app *appContext) MenuInteraction() {
|
||||||
|
app.Audio.PlaySample("menu_interaction.mp3")
|
||||||
|
}
|
||||||
|
|
||||||
func (app *appContext) Play(ctx ui.Context) {
|
func (app *appContext) Play(ctx ui.Context) {
|
||||||
level := tins2021.NewLevel()
|
level := tins2021.NewLevel()
|
||||||
level.Randomize(0, numberOfStars)
|
level.Randomize(0, numberOfStars)
|
||||||
|
|
||||||
app.Score.Current = tins2021.Score{}
|
app.ResetCurrentScore()
|
||||||
app.show(newLevelControl(app, ctx, level))
|
app.show(ctx, newLevelControl(app, ctx, level))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
|
func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
|
||||||
@ -62,31 +74,95 @@ func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
|
|||||||
func (app *appContext) PlayResume(ctx ui.Context) {
|
func (app *appContext) PlayResume(ctx ui.Context) {
|
||||||
level := tins2021.NewLevel()
|
level := tins2021.NewLevel()
|
||||||
level.Score = app.Score.Current.Score
|
level.Score = app.Score.Current.Score
|
||||||
|
level.Lives = app.Score.CurrentLives
|
||||||
level.Randomize(app.Score.Current.Difficulty, numberOfStars)
|
level.Randomize(app.Score.Current.Difficulty, numberOfStars)
|
||||||
|
|
||||||
app.show(newLevelControl(app, ctx, level))
|
app.show(ctx, newLevelControl(app, ctx, level))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) show(control ui.Control) {
|
func (app *appContext) ResetCurrentScore() {
|
||||||
|
app.Score.Current = tins2021.Score{}
|
||||||
|
app.Score.CurrentLives = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) SetCurrentScore(level *tins2021.Level) {
|
||||||
|
app.Score.Current = tins2021.NewScore(level.Score, level.Difficulty+1)
|
||||||
|
app.Score.CurrentLives = level.Lives
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) show(ctx ui.Context, control ui.Control) {
|
||||||
app.setView(control)
|
app.setView(control)
|
||||||
|
if _, ok := control.(*levelController); ok {
|
||||||
|
app.switchToPlayMusic(ctx)
|
||||||
|
} else {
|
||||||
|
app.switchToMenuMusic(ctx)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) ShowCredits(ctx ui.Context) {
|
func (app *appContext) ShowCredits(ctx ui.Context) {
|
||||||
app.setView(newCredits(app, ctx))
|
app.show(ctx, newCredits(app, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) ShowSettings(ctx ui.Context) {
|
func (app *appContext) ShowSettings(ctx ui.Context) {
|
||||||
app.setView(newSettings(app, ctx))
|
app.show(ctx, newSettings(app, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) ShowHighscores(ctx ui.Context) {
|
func (app *appContext) ShowHighscores(ctx ui.Context) {
|
||||||
app.setView(newHighscores(app, ctx))
|
app.show(ctx, newHighscores(app, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) ShowInfo(ctx ui.Context) {
|
func (app *appContext) ShowInfo(ctx ui.Context) {
|
||||||
app.setView(newInfo(app, ctx))
|
app.show(ctx, newInfo(app, ctx))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) ShowMainMenu(ctx ui.Context) {
|
func (app *appContext) ShowMainMenu(ctx ui.Context) {
|
||||||
app.show(newMainMenu(app, ctx))
|
app.show(ctx, newMainMenu(app, ctx))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) playNextGameMusic(ctx ui.Context) {
|
||||||
|
if app.GameMusic != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const songs = 4
|
||||||
|
pick := func() int {
|
||||||
|
for {
|
||||||
|
s := rand.Intn(songs) + 1
|
||||||
|
if s == app.GameMusicSong {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
app.GameMusicSong = s
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
song := fmt.Sprintf("song_game_%d.mp3", pick())
|
||||||
|
app.GameMusic, _ = app.Audio.PlayMusic(song, func(m *Music) {
|
||||||
|
m.OnFinished = func() {
|
||||||
|
if app.GameMusic == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.GameMusic = nil
|
||||||
|
app.playNextGameMusic(ctx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) switchToPlayMusic(ctx ui.Context) {
|
||||||
|
app.playNextGameMusic(ctx)
|
||||||
|
menuMusic := app.MenuMusic
|
||||||
|
app.MenuMusic = nil
|
||||||
|
if menuMusic != nil {
|
||||||
|
menuMusic.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *appContext) switchToMenuMusic(ctx ui.Context) {
|
||||||
|
if app.MenuMusic != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
app.MenuMusic, _ = app.Audio.PlayMusic("song_menu.mp3", func(m *Music) {})
|
||||||
|
gameMusic := app.GameMusic
|
||||||
|
app.GameMusic = nil
|
||||||
|
if gameMusic != nil {
|
||||||
|
gameMusic.Stop()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
181
cmd/tins2021/audio.go
Normal file
181
cmd/tins2021/audio.go
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/faiface/beep"
|
||||||
|
"github.com/faiface/beep/effects"
|
||||||
|
"github.com/faiface/beep/mp3"
|
||||||
|
"github.com/faiface/beep/speaker"
|
||||||
|
"opslag.de/schobers/zntg/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AudioPlayer struct {
|
||||||
|
resources ui.Resources
|
||||||
|
prefix string
|
||||||
|
|
||||||
|
SampleRate beep.SampleRate
|
||||||
|
|
||||||
|
Samples map[string]Sample
|
||||||
|
|
||||||
|
SampleVolume float64
|
||||||
|
MusicVolume float64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAudioPlayer(resources ui.Resources, prefix string) *AudioPlayer {
|
||||||
|
var rate = beep.SampleRate(48000)
|
||||||
|
speaker.Init(rate, rate.N(time.Second/20))
|
||||||
|
|
||||||
|
return &AudioPlayer{
|
||||||
|
resources: resources,
|
||||||
|
prefix: prefix,
|
||||||
|
SampleRate: rate,
|
||||||
|
Samples: map[string]Sample{},
|
||||||
|
SampleVolume: 1,
|
||||||
|
MusicVolume: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AudioPlayer) LoadSample(name ...string) error {
|
||||||
|
for _, name := range name {
|
||||||
|
if _, err := p.openSample(name); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AudioPlayer) openSample(name string) (Sample, error) {
|
||||||
|
sample, ok := p.Samples[name]
|
||||||
|
if ok {
|
||||||
|
return sample, nil
|
||||||
|
}
|
||||||
|
stream, format, err := p.openResource(name)
|
||||||
|
if err != nil {
|
||||||
|
return Sample{}, err
|
||||||
|
}
|
||||||
|
defer stream.Close()
|
||||||
|
|
||||||
|
buffer := beep.NewBuffer(format)
|
||||||
|
buffer.Append(stream)
|
||||||
|
sample = Sample{Buffer: buffer, SampleRate: format.SampleRate}
|
||||||
|
p.Samples[name] = sample
|
||||||
|
return sample, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AudioPlayer) PlaySample(name string) error {
|
||||||
|
sample, err := p.openSample(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
speaker.Play(p.resample(sample.Stream(), sample.SampleRate))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AudioPlayer) resample(stream beep.Streamer, sampleRate beep.SampleRate) beep.Streamer {
|
||||||
|
return Resample(stream, p.SampleRate, sampleRate)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AudioPlayer) PlayMusic(name string, init func(*Music)) (*Music, error) {
|
||||||
|
stream, format, err := p.openResource(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
closer := &streamCloser{stream}
|
||||||
|
music := &Music{
|
||||||
|
Name: name,
|
||||||
|
AutoRepeat: false,
|
||||||
|
Stream: closer,
|
||||||
|
Volume: &effects.Volume{
|
||||||
|
Streamer: p.resample(closer, format.SampleRate),
|
||||||
|
Base: 2,
|
||||||
|
Volume: 1,
|
||||||
|
Silent: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if init != nil {
|
||||||
|
init(music)
|
||||||
|
}
|
||||||
|
speaker.Play(beep.Seq(music.Volume, beep.Callback(func() { go music.Finished(p) })))
|
||||||
|
return music, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *AudioPlayer) openResource(name string) (beep.StreamSeekCloser, beep.Format, error) {
|
||||||
|
path := fmt.Sprintf("%s%s", p.prefix, name)
|
||||||
|
audio, err := p.resources.OpenResource(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, beep.Format{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, format, err := mp3.Decode(audio)
|
||||||
|
if err != nil {
|
||||||
|
return nil, beep.Format{}, err
|
||||||
|
}
|
||||||
|
return stream, format, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type Music struct {
|
||||||
|
Name string
|
||||||
|
AutoRepeat bool
|
||||||
|
Stream beep.StreamCloser
|
||||||
|
Volume *effects.Volume
|
||||||
|
OnFinished func()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Music) Stop() {
|
||||||
|
m.AutoRepeat = false
|
||||||
|
m.Stream.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m Music) Finished(player *AudioPlayer) {
|
||||||
|
m.Stream.Close()
|
||||||
|
if m.AutoRepeat {
|
||||||
|
player.PlayMusic(m.Name, func(m *Music) { m.AutoRepeat = true })
|
||||||
|
}
|
||||||
|
onFinished := m.OnFinished
|
||||||
|
if onFinished != nil {
|
||||||
|
onFinished()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Sample struct {
|
||||||
|
*beep.Buffer
|
||||||
|
|
||||||
|
SampleRate beep.SampleRate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Sample) Stream() beep.Streamer {
|
||||||
|
return s.Buffer.Streamer(0, s.Buffer.Len())
|
||||||
|
}
|
||||||
|
|
||||||
|
type streamCloser struct {
|
||||||
|
beep.StreamCloser
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *streamCloser) Err() error {
|
||||||
|
if c.StreamCloser == nil {
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
return c.StreamCloser.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *streamCloser) Stream(samples [][2]float64) (n int, ok bool) {
|
||||||
|
if c.StreamCloser == nil {
|
||||||
|
return 0, false
|
||||||
|
}
|
||||||
|
return c.StreamCloser.Stream(samples)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *streamCloser) Close() error {
|
||||||
|
c.StreamCloser = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Resample(stream beep.Streamer, expected, actual beep.SampleRate) beep.Streamer {
|
||||||
|
if expected == actual {
|
||||||
|
return stream
|
||||||
|
}
|
||||||
|
return beep.Resample(3, actual, expected, stream)
|
||||||
|
}
|
64
cmd/tins2021/background.go
Normal file
64
cmd/tins2021/background.go
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image"
|
||||||
|
"image/color"
|
||||||
|
"image/png"
|
||||||
|
"math/rand"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/llgcode/draw2d/draw2dimg"
|
||||||
|
"github.com/nfnt/resize"
|
||||||
|
"golang.org/x/image/draw"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
"opslag.de/schobers/tins2021"
|
||||||
|
"opslag.de/schobers/zntg/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateBackground(resources ui.Resources, path string) error {
|
||||||
|
const cubeTextureWidth = 100
|
||||||
|
cube := resize.Resize(cubeTextureWidth, 0, tins2021.GenerateCube(tins2021.Blue), resize.Bilinear)
|
||||||
|
inverted := resize.Resize(cubeTextureWidth, 0, tins2021.GenerateHole(tins2021.Blue), resize.Bilinear)
|
||||||
|
|
||||||
|
const twelfth = (1. / 6) * geom.Pi
|
||||||
|
centerTopSquare := geom.PtF(.5, .5*geom.Sin(twelfth))
|
||||||
|
delta := geom.PtF(geom.Cos(twelfth), .5+centerTopSquare.Y).Mul(cubeTextureWidth).Mul(.5).ToInt().Mul(2)
|
||||||
|
|
||||||
|
bounds := image.Rect(2560, 1440, 0, 0)
|
||||||
|
im := image.NewRGBA(bounds)
|
||||||
|
var odd bool
|
||||||
|
for y := -cubeTextureWidth; y < bounds.Max.Y; y += delta.Y {
|
||||||
|
left := -cubeTextureWidth
|
||||||
|
if odd {
|
||||||
|
left += delta.X / 2
|
||||||
|
}
|
||||||
|
for x := left; x < bounds.Max.X; x += delta.X {
|
||||||
|
currentCube := cube
|
||||||
|
n := rand.Intn(2)
|
||||||
|
if n == 0 {
|
||||||
|
currentCube = inverted
|
||||||
|
}
|
||||||
|
draw.Copy(im, image.Pt(x, y), currentCube, image.Rect(0, 0, cubeTextureWidth, cubeTextureWidth), draw.Over, nil)
|
||||||
|
}
|
||||||
|
odd = !odd
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := draw2dimg.NewGraphicContext(im)
|
||||||
|
font, err := parseTitleFont(resources)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
setDraw2DFont(ctx, font)
|
||||||
|
ctx.SetFontSize(224)
|
||||||
|
const text = "QBITTER"
|
||||||
|
center := draw2DCenterString(ctx, text)
|
||||||
|
ctx.SetFillColor(color.White)
|
||||||
|
ctx.FillStringAt(text, .5*float64(bounds.Dx())+center.X, .5*float64(bounds.Dy())+center.Y)
|
||||||
|
|
||||||
|
out, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return png.Encode(out, im)
|
||||||
|
}
|
47
cmd/tins2021/draw2dfont.go
Normal file
47
cmd/tins2021/draw2dfont.go
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/golang/freetype/truetype"
|
||||||
|
"github.com/llgcode/draw2d"
|
||||||
|
"github.com/llgcode/draw2d/draw2dimg"
|
||||||
|
"opslag.de/schobers/geom"
|
||||||
|
"opslag.de/schobers/zntg/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
func draw2DCenterString(ctx *draw2dimg.GraphicContext, s string) geom.PointF {
|
||||||
|
left, top, right, bottom := ctx.GetStringBounds(s)
|
||||||
|
return geom.PtF(-.5*(right-left), .5*(bottom-top))
|
||||||
|
}
|
||||||
|
|
||||||
|
type draw2DFontCache struct{ *truetype.Font }
|
||||||
|
|
||||||
|
func (f draw2DFontCache) Load(draw2d.FontData) (*truetype.Font, error) { return f.Font, nil }
|
||||||
|
func (draw2DFontCache) Store(draw2d.FontData, *truetype.Font) {}
|
||||||
|
|
||||||
|
func parseTrueTypeFont(resources ui.Resources, name string) (*truetype.Font, error) {
|
||||||
|
ttf, err := resources.OpenResource(name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer ttf.Close()
|
||||||
|
data, err := ioutil.ReadAll(ttf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return truetype.Parse(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseScoreFont(resources ui.Resources) (*truetype.Font, error) {
|
||||||
|
return parseTrueTypeFont(resources, "resources/fonts/FiraMono-Regular.ttf")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseTitleFont(resources ui.Resources) (*truetype.Font, error) {
|
||||||
|
return parseTrueTypeFont(resources, "resources/fonts/escher.ttf")
|
||||||
|
}
|
||||||
|
|
||||||
|
func setDraw2DFont(ctx *draw2dimg.GraphicContext, font *truetype.Font) {
|
||||||
|
ctx.FontCache = draw2DFontCache{font}
|
||||||
|
// ctx.SetFont(font) // is ignored anyway
|
||||||
|
}
|
@ -91,7 +91,7 @@ func (r *levelController) updateHighscore() bool {
|
|||||||
if highscore {
|
if highscore {
|
||||||
r.app.Score.Highscores = highscores
|
r.app.Score.Highscores = highscores
|
||||||
}
|
}
|
||||||
r.app.Score.Current = tins2021.Score{} // reset score
|
r.app.ResetCurrentScore()
|
||||||
return highscore
|
return highscore
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
|||||||
switch e.Key {
|
switch e.Key {
|
||||||
case ui.KeyEscape:
|
case ui.KeyEscape:
|
||||||
if r.Level.StarsCollected == r.Level.Stars {
|
if r.Level.StarsCollected == r.Level.Stars {
|
||||||
r.app.Score.Current = tins2021.NewScore(r.Level.Score, r.Level.Difficulty+1)
|
r.app.SetCurrentScore(r.Level)
|
||||||
}
|
}
|
||||||
r.app.ShowMainMenu(ctx)
|
r.app.ShowMainMenu(ctx)
|
||||||
}
|
}
|
||||||
@ -116,20 +116,42 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
|||||||
case *ui.KeyDownEvent:
|
case *ui.KeyDownEvent:
|
||||||
switch e.Key {
|
switch e.Key {
|
||||||
case ui.KeyEnter:
|
case ui.KeyEnter:
|
||||||
r.app.Score.Current = tins2021.NewScore(r.Level.Score, r.Level.Difficulty+1)
|
r.app.SetCurrentScore(r.Level)
|
||||||
r.app.PlayNext(ctx, r)
|
r.app.PlayNext(ctx, r)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkGameOver := func() {
|
||||||
|
if r.Level.GameOver {
|
||||||
|
r.Highscore = r.updateHighscore()
|
||||||
|
if r.Highscore {
|
||||||
|
r.app.Audio.PlaySample("level_new_high_score.mp3")
|
||||||
|
} else {
|
||||||
|
r.app.Audio.PlaySample("level_game_over.mp3")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
switch e := e.(type) {
|
switch e := e.(type) {
|
||||||
case *ui.KeyDownEvent:
|
case *ui.KeyDownEvent:
|
||||||
dir, ok := r.Controls[e.Key]
|
dir, ok := r.Controls[e.Key]
|
||||||
if ok {
|
if ok {
|
||||||
|
stars, lives := r.Level.StarsCollected, r.Level.Lives
|
||||||
r.Level.MovePlayer(dir)
|
r.Level.MovePlayer(dir)
|
||||||
if r.Level.GameOver {
|
switch {
|
||||||
r.Highscore = r.updateHighscore()
|
case r.Level.StarsCollected > stars:
|
||||||
|
r.app.Audio.PlaySample("player_collect_star.mp3")
|
||||||
|
case r.Level.Lives < lives:
|
||||||
|
r.app.Audio.PlaySample("player_hurt.mp3")
|
||||||
|
case r.Level.Lives > lives:
|
||||||
|
r.app.Audio.PlaySample("player_collect_heart.mp3")
|
||||||
|
}
|
||||||
|
r.app.Audio.PlaySample("player_move.mp3")
|
||||||
|
checkGameOver()
|
||||||
|
if r.Level.StarsCollected == r.Level.Stars {
|
||||||
|
r.app.Audio.PlaySample("level_completed.mp3")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,9 +171,8 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
|||||||
r.Level.DestroyMonster(pos)
|
r.Level.DestroyMonster(pos)
|
||||||
jumped = append(jumped, pos)
|
jumped = append(jumped, pos)
|
||||||
r.Level.DecrementLive()
|
r.Level.DecrementLive()
|
||||||
if r.Level.GameOver {
|
r.app.Audio.PlaySample("player_hurt.mp3")
|
||||||
r.Highscore = r.updateHighscore()
|
checkGameOver()
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if animation.Frame < 50 { // after 50 frames the animation has finished
|
if animation.Frame < 50 { // after 50 frames the animation has finished
|
||||||
@ -177,6 +198,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
|||||||
r.Level.MonsterTargets[pos] = target
|
r.Level.MonsterTargets[pos] = target
|
||||||
r.MovingMonsters.Frame(pos)
|
r.MovingMonsters.Frame(pos)
|
||||||
jumping = append(jumping, pos)
|
jumping = append(jumping, pos)
|
||||||
|
r.app.Audio.PlaySample("monster_jump.mp3")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,8 @@ func (c *center) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.P
|
|||||||
|
|
||||||
type mainMenu struct {
|
type mainMenu struct {
|
||||||
ui.StackPanel
|
ui.StackPanel
|
||||||
|
|
||||||
|
music *Music
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
|
func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
|
||||||
@ -37,7 +39,8 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
|
|||||||
menu.Add("Play", func(ctx ui.Context) {
|
menu.Add("Play", func(ctx ui.Context) {
|
||||||
app.Play(ctx)
|
app.Play(ctx)
|
||||||
})
|
})
|
||||||
if app.Score.Current.Difficulty > 0 {
|
resume := app.Score.Current.Difficulty > 0
|
||||||
|
if resume {
|
||||||
menu.Add("Resume", func(ctx ui.Context) { app.PlayResume(ctx) })
|
menu.Add("Resume", func(ctx ui.Context) { app.PlayResume(ctx) })
|
||||||
}
|
}
|
||||||
menu.Add("Highscores", func(ctx ui.Context) { app.ShowHighscores(ctx) })
|
menu.Add("Highscores", func(ctx ui.Context) { app.ShowHighscores(ctx) })
|
||||||
@ -45,7 +48,14 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
|
|||||||
menu.Add("Credits", func(ctx ui.Context) { app.ShowCredits(ctx) })
|
menu.Add("Credits", func(ctx ui.Context) { app.ShowCredits(ctx) })
|
||||||
menu.Add("Quit", func(ctx ui.Context) { ctx.Quit() })
|
menu.Add("Quit", func(ctx ui.Context) { ctx.Quit() })
|
||||||
|
|
||||||
|
if resume {
|
||||||
|
menu.Activate(2) // resume
|
||||||
|
} else {
|
||||||
menu.Activate(1) // play
|
menu.Activate(1) // play
|
||||||
|
}
|
||||||
|
menu.ActiveChanged.AddHandlerEmpty((func(ui.Context) {
|
||||||
|
app.MenuInteraction()
|
||||||
|
}))
|
||||||
ctx.Animate()
|
ctx.Animate()
|
||||||
|
|
||||||
return Center(&mainMenu{
|
return Center(&mainMenu{
|
||||||
|
@ -11,6 +11,8 @@ type Menu struct {
|
|||||||
|
|
||||||
active int
|
active int
|
||||||
buttons []*MenuButton
|
buttons []*MenuButton
|
||||||
|
|
||||||
|
ActiveChanged ui.Events
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewMenu() *Menu {
|
func NewMenu() *Menu {
|
||||||
@ -25,7 +27,7 @@ func (m *Menu) Activate(i int) {
|
|||||||
if len(m.buttons) == 0 || i < 0 {
|
if len(m.buttons) == 0 || i < 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
m.updateActiveButton(i % len(m.buttons))
|
m.updateActiveButton(nil, i%len(m.buttons))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) Add(text string, click func(ui.Context)) {
|
func (m *Menu) Add(text string, click func(ui.Context)) {
|
||||||
@ -54,29 +56,33 @@ func (m *Menu) Handle(ctx ui.Context, e ui.Event) bool {
|
|||||||
onEscape(ctx)
|
onEscape(ctx)
|
||||||
}
|
}
|
||||||
case ui.KeyDown:
|
case ui.KeyDown:
|
||||||
m.updateActiveButton((m.active + 1) % len(m.buttons))
|
m.updateActiveButton(ctx, (m.active+1)%len(m.buttons))
|
||||||
case ui.KeyUp:
|
case ui.KeyUp:
|
||||||
m.updateActiveButton((m.active + len(m.buttons) - 1) % len(m.buttons))
|
m.updateActiveButton(ctx, (m.active+len(m.buttons)-1)%len(m.buttons))
|
||||||
case ui.KeyEnter:
|
case ui.KeyEnter:
|
||||||
m.buttons[m.active].InvokeClick(ctx)
|
m.buttons[m.active].InvokeClick(ctx)
|
||||||
}
|
}
|
||||||
case *ui.MouseMoveEvent:
|
case *ui.MouseMoveEvent:
|
||||||
for i, button := range m.buttons {
|
for i, button := range m.buttons {
|
||||||
if button.IsOver() {
|
if button.IsOver() {
|
||||||
m.updateActiveButton(i)
|
m.updateActiveButton(ctx, i)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
m.updateActiveButton(m.active)
|
m.updateActiveButton(ctx, m.active)
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) updateActiveButton(active int) {
|
func (m *Menu) updateActiveButton(ctx ui.Context, active int) {
|
||||||
|
change := m.active != active
|
||||||
m.active = active
|
m.active = active
|
||||||
for i, btn := range m.buttons {
|
for i, btn := range m.buttons {
|
||||||
btn.Over = i == m.active
|
btn.Over = i == m.active
|
||||||
}
|
}
|
||||||
|
if change && ctx != nil {
|
||||||
|
m.ActiveChanged.Notify(ctx, nil)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type MenuButton struct {
|
type MenuButton struct {
|
||||||
|
@ -4,10 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
"io/ioutil"
|
|
||||||
|
|
||||||
"github.com/golang/freetype/truetype"
|
"github.com/golang/freetype/truetype"
|
||||||
"github.com/llgcode/draw2d"
|
|
||||||
"github.com/llgcode/draw2d/draw2dimg"
|
"github.com/llgcode/draw2d/draw2dimg"
|
||||||
"opslag.de/schobers/geom"
|
"opslag.de/schobers/geom"
|
||||||
"opslag.de/schobers/tins2021"
|
"opslag.de/schobers/tins2021"
|
||||||
@ -53,21 +51,15 @@ func drawKey(ctx *draw2dimg.GraphicContext, font *truetype.Font, center geom.Poi
|
|||||||
ctx.Close()
|
ctx.Close()
|
||||||
ctx.Stroke()
|
ctx.Stroke()
|
||||||
|
|
||||||
ctx.FontCache = fontCache{font}
|
setDraw2DFont(ctx, font)
|
||||||
ctx.SetFont(font)
|
|
||||||
ctx.SetFontSize(keyHeight_5)
|
ctx.SetFontSize(keyHeight_5)
|
||||||
|
|
||||||
text := fmt.Sprintf("%c", key)
|
text := fmt.Sprintf("%c", key)
|
||||||
textLeft, textTop, textRight, textBottom := ctx.GetStringBounds(text)
|
textCenter := draw2DCenterString(ctx, text)
|
||||||
textX, textY := skewed(-.5*(textRight-textLeft), .5*(textBottom-textTop))
|
textX, textY := skewed(textCenter.X, textCenter.Y)
|
||||||
ctx.FillStringAt(text, textX, textY)
|
ctx.FillStringAt(text, textX, textY)
|
||||||
}
|
}
|
||||||
|
|
||||||
type fontCache struct{ *truetype.Font }
|
|
||||||
|
|
||||||
func (f fontCache) Load(draw2d.FontData) (*truetype.Font, error) { return f.Font, nil }
|
|
||||||
func (fontCache) Store(draw2d.FontData, *truetype.Font) {}
|
|
||||||
|
|
||||||
func generateArrowKeys(resources ui.Resources) image.Image {
|
func generateArrowKeys(resources ui.Resources) image.Image {
|
||||||
return generateKeys(resources,
|
return generateKeys(resources,
|
||||||
keyboardLayoutKey{Position: geom.PtF(.53, .25), Key: '↑'},
|
keyboardLayoutKey{Position: geom.PtF(.53, .25), Key: '↑'},
|
||||||
@ -106,7 +98,7 @@ func generateKeys(resources ui.Resources, keys ...keyboardLayoutKey) image.Image
|
|||||||
im := image.NewRGBA(image.Rect(0, 0, keyboardLayoutTextureWidth, keyboardLayoutTextureHeight))
|
im := image.NewRGBA(image.Rect(0, 0, keyboardLayoutTextureWidth, keyboardLayoutTextureHeight))
|
||||||
ctx := draw2dimg.NewGraphicContext(im)
|
ctx := draw2dimg.NewGraphicContext(im)
|
||||||
|
|
||||||
font, err := parseFont(resources)
|
font, err := parseScoreFont(resources)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
@ -138,19 +130,6 @@ type keyboardLayoutKey struct {
|
|||||||
Highlight bool
|
Highlight bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseFont(resources ui.Resources) (*truetype.Font, error) {
|
|
||||||
ttf, err := resources.OpenResource("resources/fonts/FiraMono-Regular.ttf")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer ttf.Close()
|
|
||||||
data, err := ioutil.ReadAll(ttf)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return truetype.Parse(data)
|
|
||||||
}
|
|
||||||
|
|
||||||
type settings struct {
|
type settings struct {
|
||||||
ui.StackPanel
|
ui.StackPanel
|
||||||
|
|
||||||
@ -228,6 +207,7 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
|
|||||||
switch e.Key {
|
switch e.Key {
|
||||||
case ui.KeyEscape:
|
case ui.KeyEscape:
|
||||||
s.SelectingCustom = 0
|
s.SelectingCustom = 0
|
||||||
|
s.app.MenuInteraction()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
key, ok := supportedCustomKeys[e.Key]
|
key, ok := supportedCustomKeys[e.Key]
|
||||||
@ -250,18 +230,46 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
|
|||||||
s.SelectedLayout = 2
|
s.SelectedLayout = 2
|
||||||
s.app.Settings.Controls.Type = controlsTypeCustom
|
s.app.Settings.Controls.Type = controlsTypeCustom
|
||||||
}
|
}
|
||||||
|
|
||||||
|
s.app.MenuInteraction()
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
switch e.Key {
|
switch e.Key {
|
||||||
case ui.KeyEscape:
|
case ui.KeyEscape:
|
||||||
s.app.ShowMainMenu(ctx)
|
s.app.ShowMainMenu(ctx)
|
||||||
|
s.app.MenuInteraction()
|
||||||
return true
|
return true
|
||||||
case ui.KeyLeft:
|
case ui.KeyLeft:
|
||||||
s.ActiveLayout = (s.ActiveLayout + 2) % 3
|
s.setActiveLayout(s.ActiveLayout - 1)
|
||||||
case ui.KeyRight:
|
case ui.KeyRight:
|
||||||
s.ActiveLayout = (s.ActiveLayout + 1) % 3
|
s.setActiveLayout(s.ActiveLayout + 1)
|
||||||
case ui.KeyEnter:
|
case ui.KeyEnter:
|
||||||
|
s.selectLayout()
|
||||||
|
}
|
||||||
|
case *ui.MouseMoveEvent:
|
||||||
|
if s.SelectingCustom == 0 {
|
||||||
|
layout := s.isOverLayout(ctx, e.Pos())
|
||||||
|
if layout > -1 {
|
||||||
|
s.setActiveLayout(layout)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case *ui.MouseButtonDownEvent:
|
||||||
|
if s.SelectingCustom == 0 {
|
||||||
|
if e.Button == ui.MouseButtonLeft {
|
||||||
|
layout := s.isOverLayout(ctx, e.Pos())
|
||||||
|
if layout > -1 {
|
||||||
|
s.setActiveLayout(layout) // to be sure, mouse move should've set this already
|
||||||
|
s.selectLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settings) selectLayout() {
|
||||||
|
s.app.MenuInteraction()
|
||||||
switch s.ActiveLayout {
|
switch s.ActiveLayout {
|
||||||
case 0:
|
case 0:
|
||||||
s.SelectedLayout = 0
|
s.SelectedLayout = 0
|
||||||
@ -273,8 +281,35 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
|
|||||||
s.SelectingCustom = 1
|
s.SelectingCustom = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *settings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int {
|
||||||
|
bounds := s.Bounds()
|
||||||
|
center := bounds.Center()
|
||||||
|
width := bounds.Dx()
|
||||||
|
|
||||||
|
scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width)
|
||||||
|
|
||||||
|
font := ctx.Fonts().Font("default")
|
||||||
|
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
left := (.04 + .32*float32(i)) * width
|
||||||
|
right := left + .28*width
|
||||||
|
top := center.Y - 2*font.Height()
|
||||||
|
bottom := center.Y + scale*keyboardLayoutTextureWidth
|
||||||
|
if mouse.In(geom.RectF32(left, top, right, bottom)) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *settings) setActiveLayout(layout int) {
|
||||||
|
layout = (layout + 3) % 3
|
||||||
|
change := layout != s.ActiveLayout
|
||||||
|
s.ActiveLayout = (layout + 3) % 3
|
||||||
|
if change {
|
||||||
|
s.app.MenuInteraction()
|
||||||
}
|
}
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *settings) Render(ctx ui.Context) {
|
func (s *settings) Render(ctx ui.Context) {
|
||||||
|
@ -29,14 +29,20 @@ func openResources() ui.Resources {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func run() error {
|
func run() error {
|
||||||
|
var background string
|
||||||
var extract bool
|
var extract bool
|
||||||
|
flag.StringVar(&background, "background", "", "generates a background")
|
||||||
flag.BoolVar(&extract, "extract", false, "extracts all resources to the current working directory")
|
flag.BoolVar(&extract, "extract", false, "extracts all resources to the current working directory")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
if extract {
|
if extract {
|
||||||
return copyBoxToDisk(resources)
|
return copyBoxToDisk(resources)
|
||||||
}
|
}
|
||||||
|
|
||||||
res := openResources()
|
res := openResources()
|
||||||
|
if background != "" {
|
||||||
|
return GenerateBackground(res, background)
|
||||||
|
}
|
||||||
|
|
||||||
ptPtr := func(x, y int) *geom.Point {
|
ptPtr := func(x, y int) *geom.Point {
|
||||||
p := geom.Pt(x, y)
|
p := geom.Pt(x, y)
|
||||||
|
1
score.go
1
score.go
@ -58,6 +58,7 @@ func (s *Score) Validate() bool {
|
|||||||
|
|
||||||
type ScoreState struct {
|
type ScoreState struct {
|
||||||
Current Score
|
Current Score
|
||||||
|
CurrentLives int
|
||||||
Highscores Highscores
|
Highscores Highscores
|
||||||
}
|
}
|
||||||
|
|
||||||
|
1
scripts/install.bat
Normal file
1
scripts/install.bat
Normal file
@ -0,0 +1 @@
|
|||||||
|
go build -tags static -ldflags "-s -w" -o "%GOPATH%/bin/qbitter.exe" opslag.de/schobers/tins2021/cmd/tins2021
|
Loading…
Reference in New Issue
Block a user