Compare commits

..

No commits in common. "master" and "qbitter_1_1_0" have entirely different histories.

31 changed files with 330 additions and 2199 deletions

View File

@ -2,25 +2,23 @@
## Content ## Content
- [Introduction](#introduction) - [Introduction](##Introduction)
- [Additional Rules](#additional-rules) - [Additional Rules](##Additional-Rules)
* [Implementation](#implementation) * [Implementation](###Implementation)
* [Definition](#definition) * [Definition](###Definition)
- [Building](#building) - [Building](##Building)
* [Using Allegro](#using-allegro) - [Sources](##Sources)
- [Sources](#sources) - [Licenses (third party)](##Licenses)
- [Licenses (third party)](#licenses) * [Go-SDL2](###Go-SDL2)
* [Go-SDL2](#go-sdl2) * [SDL 2.0](###SDL-2.0)
* [SDL 2.0](#sdl-2-0) * [Fira Mono font](###Fira-Mono-font)
* [Allegro 5](#allegro-5) * [Escher font](###Escher-font)
* [Fira Mono font](#fira-mono-font) * [Escheresk font](###Escheresk-font)
* [Escher font](#escher-font) * [fauxgl](###fauxgl)
* [Escheresk font](#escheresk-font) * [draw2d](###draw2d)
* [fauxgl](#fauxgl) * [go-colurful](###go-colurful)
* [draw2d](#draw2d) * [resize](###resize)
* [go-colurful](#go-colurful) * [testify](###testify)
* [resize](#resize)
* [testify](#testify)
## Introduction ## Introduction
@ -115,8 +113,6 @@ Prerequisites:
- GCC or a compatible compiler. - GCC or a compatible compiler.
- [Git](https://git-scm.com/download). - [Git](https://git-scm.com/download).
**Note:** the SDL 2.0 dependency can be replaced with [Allegro 5](#Using-Allegro) as well.
With all prequisites installed you can run: With all prequisites installed you can run:
``` ```
go get -u opslag.de/schobers/tins2021/cmd/tins2021 go get -u opslag.de/schobers/tins2021/cmd/tins2021
@ -128,14 +124,12 @@ go generate opslag.de/schobers/tins2021/cmd/tins2021
go install -tags static -ldflags "-s -w" opslag.de/schobers/tins2021/cmd/tins2021 go install -tags static -ldflags "-s -w" opslag.de/schobers/tins2021/cmd/tins2021
``` ```
### Using Allegro
If you want to use the Allegro backend you can add the build tag `allegro` to the `go install` command. E.g.: If you want to use the Allegro backend you can add the build tag `allegro` to the `go install` command. E.g.:
``` ```
go get -u opslag.de/schobers/allg5 go get -u opslag.de/schobers/allg5
go install -tags static,allegro -ldflags "-s -w" opslag.de/schobers/tins2021/cmd/tins2021 go install -tags static,allegro -ldflags "-s -w" opslag.de/schobers/tins2021/cmd/tins2021
``` ```
In this case the SDL 2.0 prerequisite is replaced by Allegro 5.2 (development libraries must be available for your C compiler). In this case the SDL 2.0 prerequisite is replaced by the Allegro 5.2 (development libraries must be available for your C compiler).
## Command line interface ## Command line interface
You can extract all resources embedded in the executable by running it from the command line with the `--extract` flag. The resources will be extract in the current working directory. Note that the game will use the resources on disk first if they are available. You can extract all resources embedded in the executable by running it from the command line with the `--extract` flag. The resources will be extract in the current working directory. Note that the game will use the resources on disk first if they are available.
@ -191,20 +185,6 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
### Allegro 5
Copyright © 2008-2010 the Allegro 5 Development Team
This software is provided as-is, without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
This notice may not be removed or altered from any source distribution.
### Fira Mono font ### Fira Mono font
Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A. Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.

14
TODO.md
View File

@ -1,16 +1,12 @@
- [X] Increase difficulty. - [X] Increase difficulty.
- [X] Add music & sounds. - [ ] Add music & sounds.
- [X] Keep score/difficulty level (resume & restart). - [ ] 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).
- [X] ~~Scale icons (heart & star on right side) when playing.~~ - [ ] 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?
- [X] Fix z-fighting of monsters. - [ ] Fix z-fighting of monsters.
- [X] Add exploding animation of monsters. - [ ] Add exploding animation of monsters.
- [X] Add audio settings (music & sound volume).
- [X] Hearts must be saved as well for resume.
- [ ] Add demo mode.
- [X] Add touch controls

View File

@ -1,9 +1,6 @@
package main package main
import ( import (
"log"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021" "opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/play" "opslag.de/schobers/zntg/play"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
@ -13,7 +10,6 @@ type app struct {
ui.Proxy ui.Proxy
settings *tins2021.Settings settings *tins2021.Settings
score *tins2021.ScoreState
context *appContext context *appContext
} }
@ -59,42 +55,11 @@ func (a *app) Init(ctx ui.Context) error {
ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{Align: ui.AlignRight}, false) ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{Align: ui.AlignRight}, false)
virtual := NewVirtualControls(nil) a.context = newAppContext(ctx, a.settings, func(control ui.Control) {
virtual.RegisterKey("confirm", ui.KeyEnter, tins2021.MustCreateNamedTextureImage(textures, "return-key", drawReturnKey())) a.Content = control
virtual.RegisterKey("arrow-top-left", ui.KeyUp, tins2021.MustCreateNamedTextureImage(textures, "arrow-top-left-key", drawArrowKey(.75*geom.Pi)))
virtual.RegisterKey("arrow-top-right", ui.KeyRight, tins2021.MustCreateNamedTextureImage(textures, "arrow-top-right-key", drawArrowKey(.25*geom.Pi)))
virtual.RegisterKey("arrow-bottom-left", ui.KeyLeft, tins2021.MustCreateNamedTextureImage(textures, "arrow-bottom-left-key", drawArrowKey(1.25*geom.Pi)))
virtual.RegisterKey("arrow-bottom-right", ui.KeyDown, tins2021.MustCreateNamedTextureImage(textures, "arrow-bottom-right-key", drawArrowKey(1.75*geom.Pi)))
virtual.RegisterKey("cancel", ui.KeyEscape, tins2021.MustCreateNamedTextureImage(textures, "arrow-left-key", drawArrowKey(geom.Pi)))
virtual.Enabled = a.settings.Controls.Virtual
a.Content = virtual
a.context = newAppContext(ctx, a.settings, a.score, func(view ui.Control) {
virtual.Content = view
}) })
a.context.Virtual = virtual
a.context.ShowMainMenu(ctx) a.context.ShowMainMenu(ctx)
if err := a.context.Audio.LoadSample(
"level_completed.mp3",
"level_game_over.mp3",
"level_new_high_score.mp3",
"player_collect_heart.mp3",
"player_collect_star.mp3",
"player_hurt.mp3",
); err != nil {
log.Printf("failed to load samples; %v\n", err)
}
if err := a.context.Audio.LoadSampleVolume(-.5, "menu_interaction.mp3"); err != nil {
log.Printf("failed to load samples; %v\n", err)
}
if err := a.context.Audio.LoadSampleVolume(-1,
"monster_jump.mp3",
"player_move.mp3",
); err != nil {
log.Printf("failed to load samples; %v\n", err)
}
return nil return nil
} }

View File

@ -1,9 +1,6 @@
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"
) )
@ -12,49 +9,26 @@ type appContext struct {
setView func(ui.Control) setView func(ui.Control)
Settings *tins2021.Settings Settings *tins2021.Settings
Score *tins2021.ScoreState
Virtual *VirtualControls
Debug bool Debug bool
StarTexture tins2021.AnimatedTexture StarTexture tins2021.AnimatedTexture
HeartTexture tins2021.AnimatedTexture HeartTexture tins2021.AnimatedTexture
MonsterTextureNames map[tins2021.MonsterType]string
MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
DyingMonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
Audio *AudioPlayer MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
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, setView func(ui.Control)) *appContext {
textures := textureGenerator{} textures := textureGenerator{}
app := &appContext{ app := &appContext{
setView: setView, setView: setView,
Settings: settings, Settings: settings,
Audio: NewAudioPlayer(ctx.Resources(), "resources/sounds/"),
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),
MonsterTextureNames: map[tins2021.MonsterType]string{}, MonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{
MonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{}, tins2021.MonsterTypeStraight: newAnimatedTexture(ctx, "straight-walking-monster", defaultAnimationFrames, textures.StraightWalkingMonster),
DyingMonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{}, tins2021.MonsterTypeRandom: newAnimatedTexture(ctx, "random-walking-monster", defaultAnimationFrames, textures.RandomWalkingMonster),
} tins2021.MonsterTypeChaser: newAnimatedTexture(ctx, "chasing-monster", defaultAnimationFrames, textures.ChasingMonster),
},
app.Audio.SampleVolume = settings.Audio.SoundVolume
app.Audio.MusicVolume = settings.Audio.MusicVolume
monsterNames := map[tins2021.MonsterType]string{
tins2021.MonsterTypeStraight: "straight-walking",
tins2021.MonsterTypeRandom: "random-walking",
tins2021.MonsterTypeChaser: "chasing",
}
for typ, name := range monsterNames {
textureName := fmt.Sprintf("%s-monster", name)
app.MonsterTextureNames[typ] = textureName
app.MonsterTextures[typ] = newAnimatedTexture(ctx, textureName, defaultAnimationFrames, textures.Monster(typ))
app.DyingMonsterTextures[typ] = newAnimatedTexture(ctx, fmt.Sprintf("%s-dying-monster", name), defaultAnimationFrames, textures.DyingMonster(typ))
} }
return app return app
@ -62,17 +36,11 @@ 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.ResetCurrentScore() app.show(newLevelControl(app, ctx, level))
app.show(ctx, newLevelControl(app, ctx, level))
app.SetVirtualKeys(true, true, true)
} }
func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) { func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
@ -85,141 +53,25 @@ func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
level.Score = score level.Score = score
level.Lives = lives level.Lives = lives
app.SetVirtualKeys(true, true, true)
controller.Play(level) controller.Play(level)
} }
func (app *appContext) playNextGameMusic(ctx ui.Context) { func (app *appContext) show(control ui.Control) {
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) PlayResume(ctx ui.Context) {
level := tins2021.NewLevel()
level.Score = app.Score.Current.Score
level.Lives = app.Score.CurrentLives
level.Randomize(app.Score.Current.Difficulty, numberOfStars)
app.show(ctx, newLevelControl(app, ctx, level))
app.SetVirtualKeys(true, true, true)
}
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.show(ctx, newCredits(app, ctx)) app.setView(newCredits(app, ctx))
app.SetVirtualKeys(true, true, false)
} }
func (app *appContext) ShowSettings(ctx ui.Context) { func (app *appContext) ShowSettings(ctx ui.Context) {
app.show(ctx, newSettings(app, ctx)) app.setView(newSettings(app, ctx))
app.SetVirtualKeys(true, true, false)
}
func (app *appContext) ShowHighscores(ctx ui.Context) {
app.show(ctx, newHighscores(app, ctx))
app.SetVirtualKeys(true, true, false)
} }
func (app *appContext) ShowInfo(ctx ui.Context) { func (app *appContext) ShowInfo(ctx ui.Context) {
app.show(ctx, newInfo(app, ctx)) app.setView(newInfo(app, ctx))
app.SetVirtualKeys(true, true, false)
} }
func (app *appContext) ShowMainMenu(ctx ui.Context) { func (app *appContext) ShowMainMenu(ctx ui.Context) {
app.show(ctx, newMainMenu(app, ctx)) app.show(newMainMenu(app, ctx))
app.SetVirtualKeys(false, false, false)
}
func (app *appContext) setMusicVolume(volume float64) {
app.Settings.Audio.MusicVolume = volume
app.Audio.MusicVolume = volume
menu := app.MenuMusic
if menu != nil {
menu.Volume.Volume = volume
}
game := app.GameMusic
if game != nil {
game.Volume.Volume = volume
}
}
func (app *appContext) setSoundVolume(volume float64) {
app.Settings.Audio.SoundVolume = volume
app.Audio.SampleVolume = volume
}
func (app *appContext) SetVirtualKeys(cancel, confirm, controls bool) {
var topLeft, topRight, bottomRight, bottomLeft []string
if cancel {
topLeft = append(topLeft, "cancel")
}
if confirm {
topRight = append(topRight, "confirm")
}
if controls {
bottomRight = append(bottomRight, "arrow-bottom-right", "arrow-top-right")
bottomLeft = append(bottomLeft, "arrow-bottom-left", "arrow-top-left")
}
app.Virtual.SetControls(topLeft, topRight, bottomRight, bottomLeft)
}
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) { m.AutoRepeat = true })
gameMusic := app.GameMusic
app.GameMusic = nil
if gameMusic != nil {
gameMusic.Stop()
}
} }

View File

@ -1,198 +0,0 @@
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"
"github.com/pkg/errors"
"opslag.de/schobers/ut"
"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: 0,
MusicVolume: 0,
}
}
func (p *AudioPlayer) LoadSample(name ...string) error {
return p.LoadSampleVolume(0, name...)
}
func (p *AudioPlayer) LoadSampleVolume(volume float64, name ...string) error {
var all error
for _, name := range name {
if _, err := p.openSample(name, volume); err != nil {
all = ut.ErrCombine(all, errors.Wrap(err, "failed to open sample"))
}
}
if all == nil {
return nil
}
return errors.WithMessage(all, "failed to open one or more samples")
}
func (p *AudioPlayer) openSample(name string, volume float64) (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, Volume: volume, SampleRate: format.SampleRate}
p.Samples[name] = sample
return sample, nil
}
func (p *AudioPlayer) PlaySample(name string) error {
sample, err := p.openSample(name, 0)
if err != nil {
return err
}
volume := p.SampleVolume + sample.Volume
speaker.Play(&effects.Volume{
Streamer: p.resample(sample.Stream(), sample.SampleRate),
Base: 2,
Volume: volume,
Silent: volume <= minVolume,
})
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: p.MusicVolume,
Silent: p.MusicVolume <= minVolume,
},
}
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
Volume float64
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)
}

View File

@ -1,64 +0,0 @@
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)
}

View File

@ -36,9 +36,7 @@ func newCredits(app *appContext, ctx ui.Context) *credits {
"zntg: an abstraction for rendering (UIs)", "zntg: an abstraction for rendering (UIs)",
" - https://opslag.de/schobers/zntg", "", " - https://opslag.de/schobers/zntg", "",
"allg5: an Allegro abstraction for Go", "allg5: an Allegro abstraction for Go",
" - https://opslag.de/schobers/allg5", "", " - https://opslag.de/schobers/allg5",
"ut: a utility library",
" - https://opslag.de/schobers/ut", "",
"", "",
"**... and also using**", "**... and also using**",
"Allegro: cross-platform development library", "Allegro: cross-platform development library",
@ -60,10 +58,6 @@ func newCredits(app *appContext, ctx ui.Context) *credits {
" - https://github.com/nfnt/resize", "", " - https://github.com/nfnt/resize", "",
"testify: a testing library for Go", "testify: a testing library for Go",
" - https://github.com/stretchr/testify", "", " - https://github.com/stretchr/testify", "",
"beep: a sound library",
" - https://github.com/faiface/beep", "",
"oto: a low level sound library",
" - https://https://github.com/hajimehoshi/oto", "",
"", "",
"# THANKS", "# THANKS",
"", "",
@ -85,9 +79,7 @@ func (c *credits) Handle(ctx ui.Context, e ui.Event) bool {
s := c.content[c.hovering] s := c.content[c.hovering]
if strings.HasPrefix(s, " - https://") { if strings.HasPrefix(s, " - https://") {
url := s[3:] url := s[3:]
if err := c.openBrowser(url); err != nil { c.openBrowser(url)
log.Println(err)
}
} }
} }
case *ui.KeyDownEvent: case *ui.KeyDownEvent:
@ -154,23 +146,26 @@ func (c *credits) fonts(ctx ui.Context) []ui.Font {
return fonts return fonts
} }
func (c *credits) openBrowser(url string) error { func (c *credits) openBrowser(url string) {
var err error
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
return exec.Command("xdg-open", url).Start() err = exec.Command("xdg-open", url).Start()
case "windows": case "windows":
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin": case "darwin":
return exec.Command("open", url).Start() err = exec.Command("open", url).Start()
default: default:
return fmt.Errorf("unsupported platform") err = fmt.Errorf("unsupported platform")
}
if err != nil {
log.Println(err)
} }
} }
func (c *credits) Render(ctx ui.Context) { func (c *credits) Render(ctx ui.Context) {
renderer := ctx.Renderer() renderer := ctx.Renderer()
bounds := c.Bounds() width := c.Bounds().Dx()
width := bounds.Dx()
defaultColor := ctx.Style().Palette.Text defaultColor := ctx.Style().Palette.Text
c.enumerateContent(ctx, func(s string, i int, top, height float32, font ui.Font) { c.enumerateContent(ctx, func(s string, i int, top, height float32, font ui.Font) {
color := defaultColor color := defaultColor
@ -187,6 +182,6 @@ func (c *credits) Render(ctx ui.Context) {
} }
textWidth := font.WidthOf(s) textWidth := font.WidthOf(s)
renderer.Text(font, geom.PtF32(bounds.Min.X+.5*(width-textWidth), top), color, s) renderer.Text(font, geom.PtF32(.5*(width-textWidth), top), color, s)
}) })
} }

View File

@ -1,47 +0,0 @@
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
}

View File

@ -1,52 +0,0 @@
package main
import (
"fmt"
"opslag.de/schobers/zntg/ui"
)
type highscores struct {
ui.StackPanel
app *appContext
}
func newHighscores(app *appContext, ctx ui.Context) ui.Control {
scores := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.AddChild(label("RANK", "default"))
for i, score := range app.Score.Highscores {
p.AddChild(label(fmt.Sprintf("%d. %d", i+1, score.Score), "score"))
}
})
difficulties := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.AddChild(label(" DIFFICULTY", "default"))
for _, score := range app.Score.Highscores {
p.AddChild(labelOpts(fmt.Sprintf("%d", score.Difficulty), "score", labelOptions{TextAlignment: ui.AlignRight}))
}
})
content := []ui.Control{
labelOpts("HIGHSCORES", "title", labelOptions{TextAlignment: ui.AlignCenter}),
label("", "score"),
Center(ui.BuildStackPanel(ui.OrientationHorizontal, func(p *ui.StackPanel) {
p.AddChild(scores, difficulties)
})),
}
return Center(&highscores{StackPanel: ui.StackPanel{
ContainerBase: ui.ContainerBase{
Children: content,
},
Orientation: ui.OrientationVertical,
}, app: app})
}
func (i *highscores) Handle(ctx ui.Context, e ui.Event) bool {
switch e := e.(type) {
case *ui.KeyDownEvent:
if e.Key == ui.KeyEscape || e.Key == ui.KeyEnter {
i.app.ShowMainMenu(ctx)
}
}
return i.ControlBase.Handle(ctx, e)
}

View File

@ -44,7 +44,6 @@ func (l *infoLegend) Render(ctx ui.Context) {
} }
func newInfo(app *appContext, ctx ui.Context) ui.Control { func newInfo(app *appContext, ctx ui.Context) ui.Control {
monsterName := func(typ tins2021.MonsterType) string { return app.MonsterTextureNames[typ] }
legend := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) { legend := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("star", infoLegendIconSize), Icon: ctx.Textures().ScaledByName("star", infoLegendIconSize),
@ -55,15 +54,15 @@ func newInfo(app *appContext, ctx ui.Context) ui.Control {
Description: "Gives (back) a life.", Description: "Gives (back) a life.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeStraight), infoLegendIconSize), Icon: ctx.Textures().ScaledByName("straight-walking-monster", infoLegendIconSize),
Description: "Monster that walks over a fixed diagonal.", Description: "Monster that walks over a fixed diagonal.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeRandom), infoLegendIconSize), Icon: ctx.Textures().ScaledByName("random-walking-monster", infoLegendIconSize),
Description: "Monster that walks randomly.", Description: "Monster that walks randomly.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeChaser), infoLegendIconSize), Icon: ctx.Textures().ScaledByName("chasing-monster", infoLegendIconSize),
Description: "Monster that walks towards you.", Description: "Monster that walks towards you.",
}) })
}) })

View File

@ -3,7 +3,6 @@ package main
import ( import (
"fmt" "fmt"
"image/color" "image/color"
"log"
"math/rand" "math/rand"
"strconv" "strconv"
"time" "time"
@ -27,14 +26,10 @@ type levelController struct {
IdleMonsters *tins2021.Animations IdleMonsters *tins2021.Animations
MovingMonsters *tins2021.Animations MovingMonsters *tins2021.Animations
DyingMonsters *tins2021.Animations
DyingMonsterTypes map[geom.Point]tins2021.MonsterType
SmallFont *tins2021.BitmapFont SmallFont *tins2021.BitmapFont
Controls map[ui.Key]tins2021.Direction Controls map[ui.Key]tins2021.Direction
Highscore bool
} }
func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController { func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController {
@ -80,13 +75,6 @@ func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *le
} }
} }
if app.Virtual.Enabled {
control.Controls[ui.KeyUp] = tins2021.DirectionUpLeft
control.Controls[ui.KeyLeft] = tins2021.DirectionDownLeft
control.Controls[ui.KeyDown] = tins2021.DirectionDownRight
control.Controls[ui.KeyRight] = tins2021.DirectionUpRight
}
control.Play(level) control.Play(level)
return control return control
@ -96,27 +84,11 @@ func IsModifierPressed(modifiers ui.KeyModifier, pressed ui.KeyModifier) bool {
return modifiers&pressed == pressed return modifiers&pressed == pressed
} }
func (r *levelController) updateHighscore() bool {
highscores, highscore := r.app.Score.Highscores.AddScore(r.Level.Score, r.Level.Difficulty)
if highscore {
r.app.Score.Highscores = highscores
}
r.app.ResetCurrentScore()
return highscore
}
func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool { func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
switch e := e.(type) { switch e := e.(type) {
case *ui.KeyDownEvent: case *ui.KeyDownEvent:
switch e.Key { switch e.Key {
case ui.KeyEnter:
if r.Level.GameOver {
r.app.ShowHighscores(ctx)
}
case ui.KeyEscape: case ui.KeyEscape:
if r.Level.StarsCollected == r.Level.Stars {
r.app.SetCurrentScore(r.Level)
}
r.app.ShowMainMenu(ctx) r.app.ShowMainMenu(ctx)
} }
} }
@ -130,59 +102,17 @@ 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.SetCurrentScore(r.Level)
r.app.PlayNext(ctx, r) r.app.PlayNext(ctx, r)
} }
} }
return false return false
} }
checkGameOver := func() {
if r.Level.GameOver {
r.app.SetVirtualKeys(true, true, false)
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")
}
}
}
checkLevelCompleted := func() {
if r.Level.StarsCollected == r.Level.Stars {
r.app.SetVirtualKeys(true, true, false)
r.app.Audio.PlaySample("level_completed.mp3")
}
}
monsterHit := func(hit *tins2021.MonsterHit) {
r.app.Audio.PlaySample("player_hurt.mp3")
if hit == nil {
log.Printf("player was hit by monster but we don't know exactly where?\n")
}
r.DyingMonsters.Frame(hit.Position)
r.DyingMonsterTypes[hit.Position] = hit.Type
}
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)
_, hit := r.Level.MovePlayer(dir)
switch {
case r.Level.StarsCollected > stars:
r.app.Audio.PlaySample("player_collect_star.mp3")
case r.Level.Lives < lives:
monsterHit(hit)
case r.Level.Lives > lives:
r.app.Audio.PlaySample("player_collect_heart.mp3")
}
r.app.Audio.PlaySample("player_move.mp3")
checkGameOver()
checkLevelCompleted()
} }
} }
@ -191,7 +121,6 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
} }
r.IdleMonsters.Update() r.IdleMonsters.Update()
r.MovingMonsters.Update() r.MovingMonsters.Update()
r.DyingMonsters.Update()
var jumped []geom.Point var jumped []geom.Point
for pos, animation := range r.MovingMonsters.Values { for pos, animation := range r.MovingMonsters.Values {
if animation.Frame < 40 { // after 40 frames the player hit is checked if animation.Frame < 40 { // after 40 frames the player hit is checked
@ -199,11 +128,9 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
} }
target := r.Level.MonsterTargets[pos] target := r.Level.MonsterTargets[pos]
if target == r.Level.Player { // player is hit if target == r.Level.Player { // player is hit
monsterHit(&tins2021.MonsterHit{Position: target, Type: r.Level.Monsters[pos].Type()})
r.Level.DestroyMonster(pos) r.Level.DestroyMonster(pos)
jumped = append(jumped, pos) jumped = append(jumped, pos)
r.Level.DecrementLive() r.Level.DecrementLive()
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
@ -221,7 +148,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
var jumping []geom.Point var jumping []geom.Point
for pos, animation := range r.IdleMonsters.Values { for pos, animation := range r.IdleMonsters.Values {
for animation.Frame > 0 { for animation.Frame > 0 {
if rand.Intn(100) < 37 { // P = .37 if rand.Intn(10) != 0 {
monster, ok := r.Level.Monsters[pos] monster, ok := r.Level.Monsters[pos]
if ok && monster != nil { if ok && monster != nil {
target, ok := monster.FindTarget(r.Level, pos) target, ok := monster.FindTarget(r.Level, pos)
@ -229,7 +156,6 @@ 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
} }
} }
@ -253,10 +179,8 @@ func (r *levelController) Play(level *tins2021.Level) {
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true), "heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
"monster": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true), "monster": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
} }
r.IdleMonsters = tins2021.NewAnimations(40*time.Millisecond, 100, false, false) r.IdleMonsters = tins2021.NewAnimations(200*time.Millisecond, 100, false, false)
r.MovingMonsters = tins2021.NewAnimations(16*time.Millisecond, 50, false, false) r.MovingMonsters = tins2021.NewAnimations(16*time.Millisecond, 50, false, false)
r.DyingMonsters = tins2021.NewAnimations(16*time.Millisecond, 20, false, false)
r.DyingMonsterTypes = map[geom.Point]tins2021.MonsterType{}
for monster := range level.Monsters { for monster := range level.Monsters {
r.IdleMonsters.Frame(monster) r.IdleMonsters.Frame(monster)
} }
@ -270,8 +194,7 @@ func (r levelController) Render(ctx ui.Context) {
centerTopSquare := geom.PtF32(.5, .5*geom.Sin32(twelfth)) centerTopSquare := geom.PtF32(.5, .5*geom.Sin32(twelfth))
delta := geom.PtF32(geom.Cos32(twelfth), .5+centerTopSquare.Y) delta := geom.PtF32(geom.Cos32(twelfth), .5+centerTopSquare.Y)
bounds := r.Bounds() view := r.Bounds().Size()
view := bounds.Size()
levelView := geom.PtF32(float32(r.Level.Bounds.Dx()+2)*delta.X, float32(r.Level.Bounds.Dy()+2)*delta.Y) levelView := geom.PtF32(float32(r.Level.Bounds.Dx()+2)*delta.X, float32(r.Level.Bounds.Dy()+2)*delta.Y)
textureWidth := geom.Min32( textureWidth := geom.Min32(
geom.Floor32(tins2021.TextureSize*view.X*.75/(levelView.X*tins2021.TextureSize)), geom.Floor32(tins2021.TextureSize*view.X*.75/(levelView.X*tins2021.TextureSize)),
@ -281,15 +204,15 @@ func (r levelController) Render(ctx ui.Context) {
delta = delta.Mul(textureWidth) delta = delta.Mul(textureWidth)
centerTopSquare = centerTopSquare.Mul(textureWidth) centerTopSquare = centerTopSquare.Mul(textureWidth)
scoreView := geom.RectF32(levelView.X*textureWidth, offsetY+delta.Y, view.X, view.Y-delta.Y-offsetY).Add(bounds.Min) scoreView := geom.RectF32(levelView.X*textureWidth, offsetY+delta.Y, view.X, view.Y-delta.Y-offsetY)
delta.X = geom.Round32(delta.X) delta.X = geom.Round32(delta.X)
delta.Y = geom.Round32(delta.Y) delta.Y = geom.Round32(delta.Y)
toScreen := func(p geom.Point) geom.PointF32 { toScreen := func(p geom.Point) geom.PointF32 {
if p.Y%2 == 0 { if p.Y%2 == 0 {
return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, offsetY).Add(bounds.Min) return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, offsetY)
} }
return p.ToF32().Mul2D(delta.XY()).Add2D(0, offsetY).Add(bounds.Min) return p.ToF32().Mul2D(delta.XY()).Add2D(0, offsetY)
} }
renderer := ctx.Renderer() renderer := ctx.Renderer()
@ -306,10 +229,6 @@ func (r levelController) Render(ctx ui.Context) {
for typ, animation := range r.app.MonsterTextures { for typ, animation := range r.app.MonsterTextures {
monsterTextures[typ] = animation.Scale(scale * .4) monsterTextures[typ] = animation.Scale(scale * .4)
} }
dyingMonsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{}
for typ, animation := range r.app.DyingMonsterTextures {
dyingMonsterTextures[typ] = animation.Scale(scale * .4)
}
propHeight := star.FrameSize(0).Y propHeight := star.FrameSize(0).Y
propOffset := geom.PtF32(-.5*float32(propHeight), -.8*float32(propHeight)) propOffset := geom.PtF32(-.5*float32(propHeight), -.8*float32(propHeight))
@ -357,13 +276,7 @@ func (r levelController) Render(ctx ui.Context) {
renderer.DrawTexturePoint(player, playerPosition.Add(centerTopSquare)) renderer.DrawTexturePoint(player, playerPosition.Add(centerTopSquare))
} }
for y := r.Level.Bounds.Min.Y; y < r.Level.Bounds.Max.Y; y++ { for pos, monsterType := range r.Level.Monsters {
for x := r.Level.Bounds.Min.X; x < r.Level.Bounds.Max.X; x++ {
pos := geom.Pt(x, y)
monsterType, ok := r.Level.Monsters[pos]
if !ok {
continue
}
tile := r.Level.Tiles[pos] tile := r.Level.Tiles[pos]
if tile == nil { if tile == nil {
continue continue
@ -381,23 +294,6 @@ func (r levelController) Render(ctx ui.Context) {
texture.Draw(renderer, platformPos.Add(propOffset), r.Animations["monster"].Frame(pos)) texture.Draw(renderer, platformPos.Add(propOffset), r.Animations["monster"].Frame(pos))
} }
} }
}
var died []geom.Point
for pos, monster := range r.DyingMonsterTypes {
frame := r.DyingMonsters.Frame(pos)
if frame == 20 {
died = append(died, pos)
continue
}
texture := dyingMonsterTextures[monster]
_, platformPos := positionOfTile(pos)
texture.Draw(renderer, platformPos.Add(propOffset), frame)
}
for _, pos := range died {
delete(r.DyingMonsters.Values, pos)
delete(r.DyingMonsterTypes, pos)
}
textColor := ctx.Style().Palette.Text textColor := ctx.Style().Palette.Text
scoreFont := ctx.Fonts().Font("score") scoreFont := ctx.Fonts().Font("score")
@ -423,34 +319,24 @@ func (r levelController) Render(ctx ui.Context) {
scoreTopLeft.Y -= scoreFont.Height() scoreTopLeft.Y -= scoreFont.Height()
renderer.Text(scoreFont, scoreTopLeft, textColor, "Score:") renderer.Text(scoreFont, scoreTopLeft, textColor, "Score:")
centerX := bounds.Min.X + .5*bounds.Dx() bounds := r.Bounds()
centerX := .5 * bounds.Dx()
titleFont := ctx.Fonts().Font("title") titleFont := ctx.Fonts().Font("title")
screenSize := ctx.Renderer().Size().ToF32()
screen := geom.RectF32(0, 0, screenSize.X, screenSize.Y)
if r.Level.GameOver { if r.Level.GameOver {
renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`)) renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`))
offsetY := .5*bounds.Dy() - titleFont.Height() offsetY := .5*bounds.Dy() - titleFont.Height()
renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "GAME OVER", ui.AlignCenter) renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "GAME OVER", ui.AlignCenter)
if r.Highscore {
highscoreFont := ctx.Fonts().Font("default")
offsetY += titleFont.Height() + scoreFont.Height()
renderer.TextAlign(highscoreFont, geom.PtF32(centerX, offsetY), ctx.Style().Palette.Primary, "NEW HIGHSCORE", ui.AlignCenter)
}
offsetY += titleFont.Height() + scoreFont.Height() offsetY += titleFont.Height() + scoreFont.Height()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Final score: %d", r.Level.Score), ui.AlignCenter) renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Final score: %d", r.Level.Score), ui.AlignCenter)
offsetY += 2 * scoreFont.Height()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [enter] to show highscores.", ui.AlignCenter)
offsetY += 2 * scoreFont.Height() offsetY += 2 * scoreFont.Height()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [escape] to quit.", ui.AlignCenter) renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [escape] to quit.", ui.AlignCenter)
} else if r.Level.StarsCollected == r.Level.Stars { } else if r.Level.StarsCollected == r.Level.Stars {
renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`)) renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`))
offsetY := .5*bounds.Dy() - titleFont.Height() offsetY := .5*bounds.Dy() - titleFont.Height()
renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "COMPLETED", ui.AlignCenter) renderer.TextAlign(titleFont, geom.PtF32(.5*bounds.Dx(), offsetY), textColor, "COMPLETED", ui.AlignCenter)
offsetY += titleFont.Height() + scoreFont.Height() offsetY += titleFont.Height() + scoreFont.Height()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Score: %d", r.Level.Score), ui.AlignCenter) renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Score: %d", r.Level.Score), ui.AlignCenter)

View File

@ -29,8 +29,6 @@ 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 {
@ -39,23 +37,11 @@ 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)
}) })
resume := app.Score.Current.Difficulty > 0 menu.Add("Controls", func(ctx ui.Context) { app.ShowSettings(ctx) })
if resume {
menu.Add("Resume", func(ctx ui.Context) { app.PlayResume(ctx) })
}
menu.Add("Highscores", func(ctx ui.Context) { app.ShowHighscores(ctx) })
menu.Add("Settings", func(ctx ui.Context) { app.ShowSettings(ctx) })
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{

View File

@ -11,8 +11,6 @@ type Menu struct {
active int active int
buttons []*MenuButton buttons []*MenuButton
ActiveChanged ui.Events
} }
func NewMenu() *Menu { func NewMenu() *Menu {
@ -27,7 +25,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(nil, i%len(m.buttons)) m.updateActiveButton(i % len(m.buttons))
} }
func (m *Menu) Add(text string, click func(ui.Context)) { func (m *Menu) Add(text string, click func(ui.Context)) {
@ -56,34 +54,29 @@ func (m *Menu) Handle(ctx ui.Context, e ui.Event) bool {
onEscape(ctx) onEscape(ctx)
} }
case ui.KeyDown: case ui.KeyDown:
m.updateActiveButton(ctx, (m.active+1)%len(m.buttons)) m.updateActiveButton((m.active + 1) % len(m.buttons))
case ui.KeyUp: case ui.KeyUp:
m.updateActiveButton(ctx, (m.active+len(m.buttons)-1)%len(m.buttons)) m.updateActiveButton((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(ctx, i) m.updateActiveButton(i)
ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer)
break break
} }
} }
m.updateActiveButton(ctx, m.active) m.updateActiveButton(m.active)
} }
return false return false
} }
func (m *Menu) updateActiveButton(ctx ui.Context, active int) { func (m *Menu) updateActiveButton(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 {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

View File

@ -4,6 +4,7 @@ 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"
@ -14,12 +15,6 @@ import (
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
) )
const (
controlsTypeWASD = "wasd"
controlsTypeArrows = "arrows"
controlsTypeCustom = "custom"
)
const keyboardKeyCornerRadius = .1 * keyboardKeyWidth const keyboardKeyCornerRadius = .1 * keyboardKeyWidth
const keyboardKeyHeight = .2 * keyboardLayoutTextureWidth const keyboardKeyHeight = .2 * keyboardLayoutTextureWidth
const keyboardKeySkew = .15 const keyboardKeySkew = .15
@ -27,40 +22,15 @@ const keyboardKeyWidth = .25 * keyboardLayoutTextureWidth
const keyboardLayoutTextureHeight = 256 const keyboardLayoutTextureHeight = 256
const keyboardLayoutTextureWidth = 2 * keyboardLayoutTextureHeight const keyboardLayoutTextureWidth = 2 * keyboardLayoutTextureHeight
const maxVolume = 2
const minVolume = -8
const volumeControlBarWidth = 154
const volumeControlKnobWidth = 154
const volumeControlTextureHeight = 256
const volumeControlTextureWidth = 2*volumeControlKnobWidth + 2*volumeControlBarWidth
func drawKey(ctx *draw2dimg.GraphicContext, font *truetype.Font, center geom.PointF, key rune, color color.Color) { func drawKey(ctx *draw2dimg.GraphicContext, font *truetype.Font, center geom.PointF, key rune, color color.Color) {
const keyHeight_5 = .5 * keyboardKeyHeight
skewed := func(x, y float64) (float64, float64) {
return center.X + x - keyboardKeySkew*y, center.Y + y
}
drawKeyOutline(ctx, center, geom.PtF(keyboardKeyWidth, keyboardKeyHeight), keyboardKeySkew, color)
setDraw2DFont(ctx, font)
ctx.SetFontSize(keyHeight_5)
text := fmt.Sprintf("%c", key)
textCenter := draw2DCenterString(ctx, text)
textX, textY := skewed(textCenter.X, textCenter.Y)
ctx.FillStringAt(text, textX, textY)
}
func drawKeyOutline(ctx *draw2dimg.GraphicContext, center, size geom.PointF, skew float64, color color.Color) {
const cornerRadius = keyboardKeyCornerRadius const cornerRadius = keyboardKeyCornerRadius
var keyHeight_5 = .5 * size.Y const keyHeight_5 = .5 * keyboardKeyHeight
var keyWidth_5 = .5 * size.X const keyWidth_5 = .5 * keyboardKeyWidth
skewed := func(x, y float64) (float64, float64) { skewed := func(x, y float64) (float64, float64) {
return center.X + x - skew*y, center.Y + y x, y = skewedKeyboardCoordinates(x, y)
return center.X + x, center.Y + y
} }
corner := func(x, y, start float64) { corner := func(x, y, start float64) {
for a := start; a <= start+.25; a += .025 { for a := start; a <= start+.25; a += .025 {
aa := a * 2 * geom.Pi aa := a * 2 * geom.Pi
@ -82,8 +52,22 @@ func drawKeyOutline(ctx *draw2dimg.GraphicContext, center, size geom.PointF, ske
corner(-keyWidth_5+cornerRadius, keyHeight_5-cornerRadius, .5) corner(-keyWidth_5+cornerRadius, keyHeight_5-cornerRadius, .5)
ctx.Close() ctx.Close()
ctx.Stroke() ctx.Stroke()
ctx.FontCache = fontCache{font}
ctx.SetFont(font)
ctx.SetFontSize(keyHeight_5)
text := fmt.Sprintf("%c", key)
textLeft, textTop, textRight, textBottom := ctx.GetStringBounds(text)
textX, textY := skewed(-.5*(textRight-textLeft), .5*(textBottom-textTop))
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: '↑'},
@ -102,7 +86,7 @@ func generateArrowKeysHighlight(resources ui.Resources, highlight [4]bool) image
} }
return generateKeys(resources, return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.53, .25), Key: spaceOrRune('↑', !highlight[0]), Highlight: highlight[0]}, keyboardLayoutKey{Position: geom.PtF(.45, .25), Key: spaceOrRune('↑', !highlight[0]), Highlight: highlight[0]},
keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: spaceOrRune('←', !highlight[1]), Highlight: highlight[1]}, keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: spaceOrRune('←', !highlight[1]), Highlight: highlight[1]},
keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: spaceOrRune('↓', !highlight[2]), Highlight: highlight[2]}, keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: spaceOrRune('↓', !highlight[2]), Highlight: highlight[2]},
keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: spaceOrRune('→', !highlight[3]), Highlight: highlight[3]}, keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: spaceOrRune('→', !highlight[3]), Highlight: highlight[3]},
@ -122,7 +106,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 := parseScoreFont(resources) font, err := parseFont(resources)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -139,68 +123,6 @@ func generateKeys(resources ui.Resources, keys ...keyboardLayoutKey) image.Image
return im return im
} }
func generateVolumeControlTexture() (image.Image, map[string]geom.RectangleF) {
im := image.NewRGBA(image.Rect(0, 0, volumeControlTextureWidth, volumeControlTextureHeight))
ctx := draw2dimg.NewGraphicContext(im)
const unitMultiplier = float64(volumeControlTextureHeight)
var left float64
coord := func(x, y float64) (float64, float64) {
return left + x*unitMultiplier, y * unitMultiplier
}
regions := map[string]geom.RectangleF{}
ctx.SetFillColor(color.White)
ctx.SetStrokeColor(color.White)
ctx.SetLineCap(draw2d.SquareCap)
ctx.SetLineWidth(16)
ctx.MoveTo(coord(.1, .5))
ctx.LineTo(coord(.4, .2))
ctx.LineTo(coord(.52, .32))
ctx.LineTo(coord(.52, .68))
ctx.LineTo(coord(.4, .8))
ctx.Close()
ctx.Stroke()
regions["leftKnob"] = geom.RectF(0, 0, volumeControlKnobWidth, volumeControlTextureHeight)
left += volumeControlKnobWidth
ctx.SetLineWidth(16)
ctx.MoveTo(coord(.1, .2))
ctx.LineTo(coord(.3, .0))
ctx.LineTo(coord(.5, .2))
ctx.LineTo(coord(.5, .8))
ctx.LineTo(coord(.3, 1))
ctx.LineTo(coord(.1, .8))
ctx.Close()
ctx.Stroke()
regions["bar"] = geom.RectF(left, 0, left+volumeControlBarWidth, volumeControlTextureHeight)
left += volumeControlBarWidth
ctx.MoveTo(coord(.1, .2))
ctx.LineTo(coord(.3, .0))
ctx.LineTo(coord(.5, .2))
ctx.LineTo(coord(.5, .8))
ctx.LineTo(coord(.3, 1))
ctx.LineTo(coord(.1, .8))
ctx.Close()
ctx.FillStroke()
regions["barFilled"] = geom.RectF(left, 0, left+volumeControlBarWidth, volumeControlTextureHeight)
left += volumeControlBarWidth
ctx.SetLineWidth(16)
ctx.MoveTo(coord(.5, .5))
ctx.LineTo(coord(.2, .2))
ctx.LineTo(coord(.08, .32))
ctx.LineTo(coord(.08, .68))
ctx.LineTo(coord(.2, .8))
ctx.Close()
ctx.Stroke()
regions["rightKnob"] = geom.RectF(left, 0, left+volumeControlKnobWidth, volumeControlTextureHeight)
return im, regions
}
func generateWASDKeys(resources ui.Resources) image.Image { func generateWASDKeys(resources ui.Resources) image.Image {
return generateKeys(resources, return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.45, .25), Key: 'W'}, keyboardLayoutKey{Position: geom.PtF(.45, .25), Key: 'W'},
@ -216,20 +138,37 @@ type keyboardLayoutKey struct {
Highlight bool Highlight bool
} }
type keyboardLayoutSettings struct { func parseFont(resources ui.Resources) (*truetype.Font, error) {
ui.ControlBase 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 {
ui.StackPanel
app *appContext app *appContext
handler SettingHandler
Active bool
ActiveLayout int ActiveLayout int
SelectedLayout int SelectedLayout int
SelectingCustom int SelectingCustom int
} }
func newKeyboardLayoutSettings(app *appContext, ctx ui.Context, handler SettingHandler) *keyboardLayoutSettings { const (
controlsTypeWASD = "wasd"
controlsTypeArrows = "arrows"
controlsTypeCustom = "custom"
)
func newSettings(app *appContext, ctx ui.Context) *settings {
ctx.Textures().CreateTextureGo("layout-wasd", generateWASDKeys(ctx.Resources()), true) ctx.Textures().CreateTextureGo("layout-wasd", generateWASDKeys(ctx.Resources()), true)
ctx.Textures().CreateTextureGo("layout-arrows", generateArrowKeys(ctx.Resources()), true) ctx.Textures().CreateTextureGo("layout-arrows", generateArrowKeys(ctx.Resources()), true)
ctx.Textures().CreateTextureGo("layout-select-1", generateArrowKeysHighlight(ctx.Resources(), [4]bool{true, false, false, false}), true) ctx.Textures().CreateTextureGo("layout-select-1", generateArrowKeysHighlight(ctx.Resources(), [4]bool{true, false, false, false}), true)
@ -245,366 +184,11 @@ func newKeyboardLayoutSettings(app *appContext, ctx ui.Context, handler SettingH
layout = 2 layout = 2
} }
settings := &keyboardLayoutSettings{app: app, handler: handler, ActiveLayout: layout, SelectedLayout: layout} settings := &settings{app: app, ActiveLayout: layout, SelectedLayout: layout}
settings.renderCustomLayout(ctx) settings.renderCustomLayout(ctx)
return settings return settings
} }
func (s *keyboardLayoutSettings) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 {
scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*size.X)
font := ctx.Fonts().Font("default")
return geom.PtF32(geom.NaN32(), 2*font.Height()+scale*keyboardLayoutTextureHeight)
}
func (s *keyboardLayoutSettings) Handle(ctx ui.Context, e ui.Event) bool {
if s.ControlBase.Handle(ctx, e) {
return true
}
switch e := e.(type) {
case *ui.KeyDownEvent:
if s.SelectingCustom > 0 {
switch e.Key {
case ui.KeyEscape:
s.SelectingCustom = 0
s.app.MenuInteraction()
return true
}
key, ok := supportedCustomKeys[e.Key]
if ok {
switch s.SelectingCustom {
case 1:
s.app.Settings.Controls.MoveUpLeft = key
case 2:
s.app.Settings.Controls.MoveDownLeft = key
case 3:
s.app.Settings.Controls.MoveDownRight = key
case 4:
s.app.Settings.Controls.MoveUpRight = key
}
s.renderCustomLayout(ctx)
s.SelectingCustom++
if s.SelectingCustom == 5 {
s.SelectingCustom = 0
s.SelectedLayout = 2
s.app.Settings.Controls.Type = controlsTypeCustom
}
s.app.MenuInteraction()
}
return true
}
switch e.Key {
case ui.KeyEscape:
s.app.ShowMainMenu(ctx)
s.app.MenuInteraction()
return true
case ui.KeyLeft:
if s.Active {
s.setActiveLayout(s.ActiveLayout - 1)
}
case ui.KeyRight:
if s.Active {
s.setActiveLayout(s.ActiveLayout + 1)
}
case ui.KeyEnter:
if s.Active {
s.selectLayout()
}
}
case *ui.MouseMoveEvent:
if s.SelectingCustom == 0 {
layout := s.isOverLayout(ctx, e.Pos())
if layout > -1 {
s.setActiveLayout(layout)
s.handler.Activated(ctx)
ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer)
}
}
s.Active = s.IsOver()
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 *keyboardLayoutSettings) IsActive() bool { return s.Active }
func (s *keyboardLayoutSettings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int {
bounds := s.Bounds()
width := bounds.Dx()
mouse = mouse.Sub(s.Offset())
scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width)
font := ctx.Fonts().Font("default")
top := bounds.Min.Y
bottom := top + 2*font.Height() + scale*keyboardLayoutTextureHeight
for i := 0; i < 3; i++ {
left := (.04 + .32*float32(i)) * width
right := left + .28*width
if mouse.In(geom.RectF32(left, top, right, bottom)) {
return i
}
}
return -1
}
func (s *keyboardLayoutSettings) PostRender(ctx ui.Context) {
bounds := s.Bounds()
width := bounds.Dx()
renderer := ctx.Renderer()
scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width)
font := ctx.Fonts().Font("default")
normalColor := ctx.Style().Palette.Text
top := bounds.Min.Y
layoutTop := top + 2*font.Height()
if s.SelectingCustom > 0 {
renderer.FillRectangle(geom.ZeroPtF32.RectRel(renderer.Size().ToF32()), zntg.MustHexColor(`#000000DF`))
selectTexture := fmt.Sprintf("layout-select-%d", s.SelectingCustom)
layoutLeft := .36 * width
layoutCenter := layoutLeft + .14*width
renderer.TextAlign(font, geom.PtF32(layoutCenter, top), normalColor, "PRESS KEY TO ASSIGN", ui.AlignCenter)
renderer.DrawTexturePoint(ctx.Textures().ScaledByName(selectTexture, scale), geom.PtF32(layoutLeft, layoutTop))
}
}
func (s *keyboardLayoutSettings) Render(ctx ui.Context) {
bounds := s.Bounds()
width := bounds.Dx()
renderer := ctx.Renderer()
scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width)
font := ctx.Fonts().Font("default")
layouts := []string{
"WASD",
"ARROWS",
"CUSTOM",
}
layoutTextures := []string{"layout-wasd", "layout-arrows", "layout-custom"}
normalColor := ctx.Style().Palette.Text
highlightColor := ctx.Style().Palette.Primary
top := bounds.Min.Y
layoutTop := top + 2*font.Height()
for i, layout := range layouts {
layoutLeft := (.04 + .32*float32(i)) * width
layoutCenter := layoutLeft + .14*width
textColor := normalColor
layoutColor := normalColor
if s.ActiveLayout == i {
textColor = highlightColor
}
if s.SelectedLayout == i {
layoutColor = highlightColor
}
renderer.TextAlign(font, geom.PtF32(layoutCenter, top), textColor, layout, ui.AlignCenter)
renderer.DrawTexturePointOptions(ctx.Textures().ScaledByName(layoutTextures[i], scale), geom.PtF32(layoutLeft, layoutTop), ui.DrawOptions{Tint: layoutColor})
}
}
func (s *keyboardLayoutSettings) renderCustomLayout(ctx ui.Context) {
runeOrQuestionMark := func(s string) rune {
if len(s) == 0 {
return '?'
}
return []rune(s)[0]
}
customKeys := [4]rune{
runeOrQuestionMark(s.app.Settings.Controls.MoveUpLeft),
runeOrQuestionMark(s.app.Settings.Controls.MoveDownLeft),
runeOrQuestionMark(s.app.Settings.Controls.MoveDownRight),
runeOrQuestionMark(s.app.Settings.Controls.MoveUpRight),
}
ctx.Textures().CreateTextureGo("layout-custom", generateCustomKeys(ctx.Resources(), customKeys), true)
}
func (s *keyboardLayoutSettings) selectLayout() {
s.app.MenuInteraction()
switch s.ActiveLayout {
case 0:
s.SelectedLayout = 0
s.app.Settings.Controls.Type = controlsTypeWASD
case 1:
s.SelectedLayout = 1
s.app.Settings.Controls.Type = controlsTypeArrows
case 2:
s.SelectingCustom = 1
}
}
func (s *keyboardLayoutSettings) SetActive(_ ui.Context, active bool) { s.Active = active }
func (s *keyboardLayoutSettings) setActiveLayout(layout int) {
layout = (layout + 3) % 3
change := layout != s.ActiveLayout
s.Active = true
s.ActiveLayout = (layout + 3) % 3
if change {
s.app.MenuInteraction()
}
}
type Setting interface {
IsActive() bool
SetActive(ui.Context, bool)
}
type SettingHandler interface {
Interacted()
Activated(ui.Context)
}
type settingHandler struct {
app *appContext
settings *settings
Setting int
}
func (h *settingHandler) Interacted() {
h.app.MenuInteraction()
}
func (h *settingHandler) Activated(ctx ui.Context) {
h.settings.setActive(ctx, h.Setting, false)
}
type settings struct {
ui.Proxy
active int
app *appContext
overflow ui.ScrollControl
musicVolume *volumeControl
soundVolume *volumeControl
virtual *toggleControl
keyboard *keyboardLayoutSettings
}
func newSettings(app *appContext, ctx ui.Context) *settings {
volumeControlTexture := generateTextureMapFromImage(ctx.Textures(), "volume-control", generateVolumeControlTexture)
settings := &settings{app: app}
var s int
handler := func() *settingHandler {
defer func() { s++ }()
return &settingHandler{Setting: s, settings: settings, app: app}
}
settings.musicVolume = newVolumeControl(handler(), volumeControlTexture, "Music", app.Settings.Audio.MusicVolume, func(volume float64) { app.setMusicVolume(volume) })
settings.soundVolume = newVolumeControl(handler(), volumeControlTexture, "Sounds", app.Settings.Audio.SoundVolume, func(volume float64) { app.setSoundVolume(volume) })
settings.virtual = newToggleControl(handler(), "VIRTUAL CONTROLS", app.Settings.Controls.Virtual, func(on bool) {
app.Settings.Controls.Virtual = on
app.Virtual.Enabled = on
})
settings.keyboard = newKeyboardLayoutSettings(app, ctx, handler())
settings.Content = ui.StretchWidth(ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.AddChild(Center(label("SETTINGS", "title")))
p.AddChild(label("", "score"))
settings.overflow = ui.Overflow(ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.AddChild(Center(settings.musicVolume))
p.AddChild(label("", "score"))
p.AddChild(Center(settings.soundVolume))
p.AddChild(label("", "score"))
p.AddChild(Center(settings.virtual))
p.AddChild(label("", "score"))
p.AddChild(settings.keyboard)
}))
p.AddChild(settings.overflow)
}))
settings.musicVolume.Active = true
return settings
}
func (s *settings) currentActive() int {
switch {
case s.musicVolume.Active:
return 0
case s.soundVolume.Active:
return 1
case s.virtual.Active:
return 2
case s.keyboard.Active:
return 3
default:
return 0
}
}
func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
if s.keyboard.Active && s.keyboard.SelectingCustom > 0 {
return s.Proxy.Handle(ctx, e)
}
switch e := e.(type) {
case *ui.KeyDownEvent:
switch e.Key {
case ui.KeyUp:
s.setActive(ctx, s.currentActive()-1, true)
case ui.KeyDown:
s.setActive(ctx, s.currentActive()+1, true)
}
}
return s.Proxy.Handle(ctx, e)
}
func (s *settings) Render(ctx ui.Context) {
s.Proxy.Render(ctx)
s.keyboard.PostRender(ctx)
}
func (s *settings) setActive(ctx ui.Context, active int, key bool) {
controls := []Setting{
s.musicVolume,
s.soundVolume,
s.virtual,
s.keyboard,
}
for active < 0 {
active += len(controls)
}
if active > 0 {
active = active % len(controls)
}
if s.active != active {
s.active = active
if key {
controls[active].(ui.Control).ScrollIntoView(ctx, nil)
}
s.app.MenuInteraction()
}
for i := range controls {
controls[i].SetActive(ctx, i == active)
}
}
var supportedCustomKeys = map[ui.Key]string{ var supportedCustomKeys = map[ui.Key]string{
ui.KeyA: "A", ui.KeyA: "A",
ui.KeyB: "B", ui.KeyB: "B",
@ -634,266 +218,130 @@ var supportedCustomKeys = map[ui.Key]string{
ui.KeyZ: "Z", ui.KeyZ: "Z",
} }
type toggleControl struct { func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
ui.StackPanel if s.StackPanel.Handle(ctx, e) {
handler SettingHandler
caption *ui.Label
on *ui.Label
changed func(bool)
Active bool
On bool
}
func newToggleControl(handler SettingHandler, name string, on bool, changed func(bool)) *toggleControl {
center := func(l *ui.Label) { l.TextAlignment = ui.AlignCenter }
toggle := &toggleControl{handler: handler, On: on, caption: ui.BuildLabel(name, center), on: ui.BuildLabel("", center), changed: changed}
toggle.AddChild(toggle.caption)
toggle.AddChild(toggle.on)
toggle.updateOn()
return toggle
}
func (c *toggleControl) toggle() {
c.On = !c.On
c.updateOn()
c.handler.Interacted()
changed := c.changed
if changed != nil {
changed(c.On)
}
}
func (c *toggleControl) Handle(ctx ui.Context, e ui.Event) bool {
if c.ControlBase.Handle(ctx, e) {
return true
}
switch e := e.(type) {
case *ui.MouseMoveEvent:
if !c.Active && c.IsOver() {
c.Active = true
c.handler.Activated(ctx)
}
if c.IsOver() {
ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer)
}
case *ui.MouseButtonDownEvent:
if e.Button == ui.MouseButtonLeft && c.Active && c.IsOver() {
c.toggle()
}
case *ui.KeyDownEvent:
if c.Active && e.Key == ui.KeyEnter {
c.toggle()
}
}
return false
}
func (c *toggleControl) IsActive() bool { return c.Active }
func (c *toggleControl) SetActive(ctx ui.Context, active bool) {
c.Active = active
if active {
c.caption.Font.Color = ctx.Style().Palette.Primary
} else {
c.caption.Font.Color = ctx.Style().Palette.Text
}
}
func (c *toggleControl) updateOn() {
if c.On {
c.on.Text = "ON"
} else {
c.on.Text = "OFF"
}
}
type volumeControl struct {
ui.ControlBase
handler SettingHandler
texture *TextureMap
Active bool
overLeftKnob bool
overBar int
overRightKnob bool
Name string
Volume float64
VolumeChanged func(float64)
}
func newVolumeControl(handler SettingHandler, texture *TextureMap, name string, volume float64, changed func(float64)) *volumeControl {
control := &volumeControl{handler: handler, texture: texture, overBar: -1, Name: name, Volume: volume, VolumeChanged: changed}
return control
}
func (c *volumeControl) changeVolume(delta float64) {
c.setVolume(c.Volume + delta)
}
func (c *volumeControl) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 {
font := ctx.Fonts().Font("default")
return geom.PtF32(geom.NaN32(), 2.5*font.Height())
}
func (c *volumeControl) Handle(ctx ui.Context, e ui.Event) bool {
if c.ControlBase.Handle(ctx, e) {
return true return true
} }
switch e := e.(type) { switch e := e.(type) {
case *ui.KeyDownEvent: case *ui.KeyDownEvent:
if s.SelectingCustom > 0 {
switch e.Key { switch e.Key {
case ui.KeyEscape:
s.SelectingCustom = 0
return true
}
key, ok := supportedCustomKeys[e.Key]
if ok {
switch s.SelectingCustom {
case 1:
s.app.Settings.Controls.MoveUpLeft = key
case 2:
s.app.Settings.Controls.MoveDownLeft = key
case 3:
s.app.Settings.Controls.MoveDownRight = key
case 4:
s.app.Settings.Controls.MoveUpRight = key
}
s.renderCustomLayout(ctx)
s.SelectingCustom++
if s.SelectingCustom == 5 {
s.SelectingCustom = 0
s.SelectedLayout = 2
s.app.Settings.Controls.Type = controlsTypeCustom
}
}
return true
}
switch e.Key {
case ui.KeyEscape:
s.app.ShowMainMenu(ctx)
return true
case ui.KeyLeft: case ui.KeyLeft:
if c.Active { s.ActiveLayout = (s.ActiveLayout + 2) % 3
c.changeVolume(-1)
}
case ui.KeyRight: case ui.KeyRight:
if c.Active { s.ActiveLayout = (s.ActiveLayout + 1) % 3
c.changeVolume(1) case ui.KeyEnter:
} switch s.ActiveLayout {
}
case *ui.MouseMoveEvent:
over := c.isOver(ctx, e.Pos())
switch over {
case -1:
c.overLeftKnob = false
c.overBar = -1
c.overRightKnob = false
case 0: case 0:
if !c.overLeftKnob { s.SelectedLayout = 0
c.handler.Interacted() s.app.Settings.Controls.Type = controlsTypeWASD
} case 1:
c.overLeftKnob = true s.SelectedLayout = 1
c.overBar = -1 s.app.Settings.Controls.Type = controlsTypeArrows
c.overRightKnob = false case 2:
case 11: s.SelectingCustom = 1
if !c.overRightKnob {
c.handler.Interacted()
}
c.overLeftKnob = false
c.overBar = -1
c.overRightKnob = true
default:
if over-1 != c.overBar {
c.handler.Interacted()
}
c.overLeftKnob = false
c.overBar = over - 1
c.overRightKnob = false
}
if over > -1 {
ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer)
}
active := c.Active
c.Active = c.IsOver()
if !active && c.Active {
c.handler.Activated(ctx)
}
case *ui.MouseButtonDownEvent:
if e.Button == ui.MouseButtonLeft {
over := c.isOver(ctx, e.Pos())
switch over {
case -1:
case 0:
c.changeVolume(-1)
case 11:
c.changeVolume(1)
default:
c.setVolume(float64(over + minVolume))
} }
} }
} }
return false return false
} }
func (c *volumeControl) IsActive() bool { return c.Active } func (s *settings) Render(ctx ui.Context) {
bounds := s.Bounds()
center := bounds.Center()
width := bounds.Dx()
renderer := ctx.Renderer()
scale := tins2021.FindScaleRound(keyboardLayoutTextureWidth, .28*width)
func (c *volumeControl) isOver(ctx ui.Context, p geom.PointF32) int {
bounds := c.Bounds()
p = p.Sub(c.Offset())
font := ctx.Fonts().Font("default") font := ctx.Fonts().Font("default")
top := bounds.Min.Y + 1.5*font.Height() layouts := []string{
bottom := top + font.Height() "WASD",
if p.Y < top || p.Y >= bottom { "ARROWS",
return -1 "CUSTOM",
} }
layoutTextures := []string{"layout-wasd", "layout-arrows", "layout-custom"}
scale := tins2021.FindScaleRound(volumeControlTextureHeight, font.Height())
knobWidth := scale * volumeControlKnobWidth
barWidth := scale * volumeControlBarWidth
left := bounds.Center().X - 5*barWidth - knobWidth
if p.X < left {
return -1
}
widths := []float32{knobWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, barWidth, knobWidth}
for i, w := range widths {
right := left + w
if p.X < right {
return i
}
left = right
}
return -1
}
func (c *volumeControl) Render(ctx ui.Context) {
font := ctx.Fonts().Font("default")
scale := tins2021.FindScaleRound(volumeControlTextureHeight, font.Height())
normalColor := ctx.Style().Palette.Text normalColor := ctx.Style().Palette.Text
activeColor := ctx.Style().Palette.Primary highlightColor := ctx.Style().Palette.Primary
bounds := c.Bounds() for i, layout := range layouts {
center := bounds.Center() layoutLeft := (.04 + .32*float32(i)) * width
renderer := ctx.Renderer() layoutCenter := layoutLeft + .14*width
top := bounds.Min.Y
fontColor := normalColor textColor := normalColor
if c.Active { layoutColor := normalColor
fontColor = activeColor if s.ActiveLayout == i {
textColor = highlightColor
} }
renderer.TextAlign(font, geom.PtF32(center.X, top), fontColor, c.Name, ui.AlignCenter) if s.SelectedLayout == i {
tint := func(active bool) color.Color { layoutColor = highlightColor
if active {
return activeColor
}
return nil
} }
leftKnob, leftKnobRegion := c.texture.Scaled("leftKnob", scale) renderer.TextAlign(font, geom.PtF32(layoutCenter, center.Y-2*font.Height()), textColor, layout, ui.AlignCenter)
leftKnobWidth := leftKnobRegion.Dx() renderer.DrawTexturePointOptions(ctx.Textures().ScaledByName(layoutTextures[i], scale), geom.PtF32(layoutLeft, center.Y), ui.DrawOptions{Tint: layoutColor})
bar, barRegion := c.texture.Scaled("bar", scale)
barFilled, barFilledRegion := c.texture.Scaled("barFilled", scale)
barWidth := barRegion.Dx()
rightKnob, rightKnobRegion := c.texture.Scaled("rightKnob", scale)
left := center.X - 5*barWidth - leftKnobWidth
top += 1.5 * font.Height()
ctx.Renderer().DrawTexturePointOptions(leftKnob, geom.PtF32(left, top), ui.DrawOptions{Source: &leftKnobRegion, Tint: tint(c.overLeftKnob)})
left += leftKnobWidth
for i := 0; i < 10; i++ {
volume := float64(i + minVolume + 1)
if volume <= c.Volume {
ctx.Renderer().DrawTexturePointOptions(barFilled, geom.PtF32(left, top), ui.DrawOptions{Source: &barFilledRegion, Tint: tint(i == c.overBar)})
} else {
ctx.Renderer().DrawTexturePointOptions(bar, geom.PtF32(left, top), ui.DrawOptions{Source: &barRegion, Tint: tint(i == c.overBar)})
}
left += barWidth
}
ctx.Renderer().DrawTexturePointOptions(rightKnob, geom.PtF32(left, top), ui.DrawOptions{Source: &rightKnobRegion, Tint: tint(c.overRightKnob)})
} }
func (c *volumeControl) SetActive(_ ui.Context, active bool) { c.Active = active } if s.SelectingCustom > 0 {
renderer.FillRectangle(bounds, zntg.MustHexColor(`#000000CF`))
func (c *volumeControl) setVolume(volume float64) { selectTexture := fmt.Sprintf("layout-select-%d", s.SelectingCustom)
volume = geom.Min(maxVolume, geom.Max(minVolume, volume))
if c.Volume == volume { layoutLeft := .36 * width
return layoutCenter := layoutLeft + .14*width
renderer.TextAlign(font, geom.PtF32(layoutCenter, center.Y-2*font.Height()), normalColor, "PRESS KEY TO ASSIGN", ui.AlignCenter)
renderer.DrawTexturePoint(ctx.Textures().ScaledByName(selectTexture, scale), geom.PtF32(layoutLeft, center.Y))
} }
c.Volume = volume }
c.VolumeChanged(c.Volume)
c.handler.Interacted() func (s *settings) renderCustomLayout(ctx ui.Context) {
runeOrQuestionMark := func(s string) rune {
if len(s) == 0 {
return '?'
}
return []rune(s)[0]
}
customKeys := [4]rune{
runeOrQuestionMark(s.app.Settings.Controls.MoveUpLeft),
runeOrQuestionMark(s.app.Settings.Controls.MoveDownLeft),
runeOrQuestionMark(s.app.Settings.Controls.MoveDownRight),
runeOrQuestionMark(s.app.Settings.Controls.MoveUpRight),
}
ctx.Textures().CreateTextureGo("layout-custom", generateCustomKeys(ctx.Resources(), customKeys), true)
}
func skewedKeyboardCoordinates(x, y float64) (float64, float64) {
return x - keyboardKeySkew*y, y
} }

View File

@ -16,29 +16,12 @@ func (textureGenerator) Heart() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, defaultAnimationFrames, tins2021.MeshRotateAnimation{}) return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, defaultAnimationFrames, tins2021.MeshRotateAnimation{})
} }
func (g textureGenerator) MonsterTypeColor(typ tins2021.MonsterType) string { func (textureGenerator) monster(color string) image.Image {
switch typ {
case tins2021.MonsterTypeStraight:
return tins2021.Green
case tins2021.MonsterTypeRandom:
return tins2021.Blue
case tins2021.MonsterTypeChaser:
return tins2021.Purple
default:
panic("monster does not have a color")
}
}
func (g textureGenerator) Monster(typ tins2021.MonsterType) func() image.Image {
color := g.MonsterTypeColor(typ)
return func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), color, defaultAnimationFrames, tins2021.MeshWobbleTransformation{Wobble: 30}) return tins2021.AnimatePolygon(tins2021.CreateHexagon(), color, defaultAnimationFrames, tins2021.MeshWobbleTransformation{Wobble: 30})
} }
}
func (g textureGenerator) DyingMonster(typ tins2021.MonsterType) func() image.Image { func (g textureGenerator) ChasingMonster() image.Image { return g.monster(tins2021.Purple) }
color := g.MonsterTypeColor(typ)
return func() image.Image { func (g textureGenerator) RandomWalkingMonster() image.Image { return g.monster(tins2021.Blue) }
return tins2021.Animate(color, defaultAnimationFrames, &tins2021.ExplodingHexagonAnimation{})
} func (g textureGenerator) StraightWalkingMonster() image.Image { return g.monster(tins2021.Green) }
}

View File

@ -1,42 +0,0 @@
package main
import (
"image"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui"
)
type TextureMap struct {
texture tins2021.NamedTexture
regions map[string]geom.RectangleF32
}
func newTextureMapFromImage(textures *ui.Textures, name string, im image.Image, regions map[string]geom.RectangleF) *TextureMap {
texture, err := tins2021.CreateNamedTextureImage(textures, name, im)
if err != nil {
panic(err)
}
regionMap := map[string]geom.RectangleF32{}
for name, region := range regions {
regionMap[name] = region.ToF32()
}
return &TextureMap{
texture: texture,
regions: regionMap,
}
}
func generateTextureMapFromImage(textures *ui.Textures, name string, generate func() (image.Image, map[string]geom.RectangleF)) *TextureMap {
im, regions := generate()
return newTextureMapFromImage(textures, name, im, regions)
}
func (m *TextureMap) Texture(name string) (ui.Texture, geom.RectangleF32) {
return m.texture.Texture(), m.regions[name]
}
func (m *TextureMap) Scaled(name string, scale float32) (ui.Texture, geom.RectangleF32) {
return m.texture.Scaled(scale), geom.RectangleF32{Min: m.regions[name].Min.Mul(scale), Max: m.regions[name].Max.Mul(scale)}
}

View File

@ -29,20 +29,14 @@ 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)
@ -56,11 +50,6 @@ func run() error {
} }
defer settings.Store() defer settings.Store()
score, err := tins2021.LoadScores()
if err != nil {
log.Printf("unable to load score; error: %v", err)
}
var location *geom.PointF32 var location *geom.PointF32
if settings.Window.Location != nil { if settings.Window.Location != nil {
location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)} location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)}
@ -85,10 +74,7 @@ func run() error {
app := &app{ app := &app{
settings: settings, settings: settings,
score: &score,
} }
defer tins2021.SaveScores(app.score)
style := ui.DefaultStyle() style := ui.DefaultStyle()
style.Palette = &ui.Palette{ style.Palette = &ui.Palette{
Background: zntg.MustHexColor(`#494949`), Background: zntg.MustHexColor(`#494949`),

View File

@ -1,238 +0,0 @@
package main
import (
"fmt"
"image"
"image/color"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg"
"opslag.de/schobers/geom"
"opslag.de/schobers/geom/ints"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui"
)
func drawArrowKey(a float64) image.Image {
a = -a
const keyHeight_5 = .5 * tins2021.TextureSize
const keyWidth_5 = .5 * tins2021.TextureSize
im := image.NewRGBA(image.Rect(0, 0, tins2021.TextureSize, tins2021.TextureSize))
ctx := draw2dimg.NewGraphicContext(im)
center := geom.PtF(keyWidth_5, keyHeight_5)
drawKeyOutline(ctx, center, geom.PtF(.95*tins2021.TextureSize, .76*tins2021.TextureSize), 0, color.White)
point := center.Add(tins2021.Polar(a, .25*tins2021.TextureSize))
back := center.Add(tins2021.Polar(a+geom.Pi, .25*tins2021.TextureSize))
ctx.SetLineWidth(3)
ctx.SetStrokeColor(color.White)
ctx.SetLineCap(draw2d.RoundCap)
ctx.MoveTo(back.XY())
ctx.LineTo(point.XY())
ctx.LineTo(point.Add(tins2021.Polar(a+.75*geom.Pi, .25*tins2021.TextureSize)).XY())
ctx.Stroke()
ctx.MoveTo(point.XY())
ctx.LineTo(point.Add(tins2021.Polar(a-.75*geom.Pi, .25*tins2021.TextureSize)).XY())
ctx.Stroke()
return im
}
func drawReturnKey() image.Image {
const keyHeight_5 = .5 * tins2021.TextureSize
const keyWidth_5 = .5 * tins2021.TextureSize
im := image.NewRGBA(image.Rect(0, 0, tins2021.TextureSize, tins2021.TextureSize))
ctx := draw2dimg.NewGraphicContext(im)
drawKeyOutline(ctx, geom.PtF(keyWidth_5, keyHeight_5), geom.PtF(.95*tins2021.TextureSize, .76*tins2021.TextureSize), 0, color.White)
unity := func(f float64) float64 { return f * tins2021.TextureSize }
ctx.SetLineWidth(3)
ctx.SetStrokeColor(color.White)
ctx.SetLineCap(draw2d.RoundCap)
ctx.MoveTo(unity(.75), unity(.3))
ctx.LineTo(unity(.75), unity(.55))
ctx.LineTo(unity(.25), unity(.55))
ctx.LineTo(unity(.4), unity(.7))
ctx.Stroke()
ctx.MoveTo(unity(.25), unity(.55))
ctx.LineTo(unity(.4), unity(.4))
ctx.Stroke()
return im
}
type VirtualControls struct {
ui.Proxy
bounds geom.RectangleF32
Enabled bool
Keys map[string]VirtualKey
TopLeftControls []string
TopRightControls []string
BottomRightControls []string
BottomLeftControls []string
}
func NewVirtualControls(content ui.Control) *VirtualControls {
return &VirtualControls{
Proxy: ui.Proxy{Content: content},
Keys: map[string]VirtualKey{},
}
}
func (c *VirtualControls) allKeys() map[string]geom.RectangleF32 {
bounds := c.bounds
size, _, margin := c.lengths()
keySize := geom.PtF32(size, size)
keys := map[string]geom.RectangleF32{}
left := float32(margin)
top := bounds.Min.Y
for _, name := range c.TopLeftControls {
keys[name] = geom.PtF32(left, top).RectRel(keySize)
top += size + margin
}
left = bounds.Max.X - size - margin
top = bounds.Min.Y
for _, name := range c.TopRightControls {
keys[name] = geom.PtF32(left, top).RectRel(keySize)
top += size + margin
}
left = float32(margin)
top = bounds.Max.Y - size
for _, name := range c.BottomLeftControls {
keys[name] = geom.PtF32(left, top).RectRel(keySize)
top -= size + margin
}
left = bounds.Max.X - size - margin
top = bounds.Max.Y - size
for _, name := range c.BottomRightControls {
keys[name] = geom.PtF32(left, top).RectRel(keySize)
top -= size + margin
}
return keys
}
func (c *VirtualControls) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) {
c.bounds = bounds
if c.Enabled {
virtualControlSize, _ := c.desiredVirtualControlSize(bounds.Size())
bounds.Min.X += virtualControlSize
bounds.Max.X -= virtualControlSize
if bounds.Max.X < bounds.Min.X {
center := .5 * (bounds.Min.X + bounds.Max.X)
bounds.Min.X = center
bounds.Max.X = center
}
}
c.Content.Arrange(ctx, bounds, offset, parent)
}
func (c *VirtualControls) desiredVirtualControlSize(size geom.PointF32) (float32, int) {
if size.X > 2*size.Y {
return size.Y / 5, 2
}
return size.Y / 7, 3
}
func (c *VirtualControls) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 {
desired := c.Content.DesiredSize(ctx, size)
if geom.IsNaN32(desired.X) {
return desired
}
if c.Enabled {
virtualControlSize, rows := c.desiredVirtualControlSize(size)
columns := ints.Max(len(c.TopLeftControls)+rows-1/rows, len(c.BottomLeftControls)+rows-1/rows) + ints.Max(len(c.TopRightControls)+rows-1/rows, len(c.BottomRightControls)+rows-1/rows)
fmt.Println(columns)
desired.X = geom.Min32(desired.X+float32(columns)*virtualControlSize, size.X)
}
return desired
}
func (c *VirtualControls) Handle(ctx ui.Context, e ui.Event) bool {
if c.Enabled {
switch e := e.(type) {
case *ui.MouseMoveEvent:
pos := e.Pos()
for _, bounds := range c.allKeys() {
if pos.In(bounds) {
ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer)
break
}
}
case *ui.MouseButtonDownEvent:
if e.Button == ui.MouseButtonLeft {
pos := e.Pos()
for name, bounds := range c.allKeys() {
if pos.In(bounds) {
key := c.Keys[name]
c.Content.Handle(ctx, &ui.KeyDownEvent{
EventBase: ui.EventBase{StampInSeconds: e.StampInSeconds},
Key: key.Key,
})
break
}
}
}
}
}
return c.Content.Handle(ctx, e)
}
func (c *VirtualControls) lengths() (size, scale, margin float32) {
bounds := c.bounds
margin = 8
size, _ = c.desiredVirtualControlSize(bounds.Size())
size -= margin
scale = tins2021.FindScaleRound(tins2021.TextureSize, size)
return
}
func (c *VirtualControls) RegisterKey(id string, key ui.Key, symbol tins2021.NamedTexture) {
c.Keys[id] = VirtualKey{Key: key, Symbol: symbol}
}
func (c *VirtualControls) Render(ctx ui.Context) {
c.Content.Render(ctx)
if c.Enabled {
_, scale, _ := c.lengths()
mouse := ctx.MousePosition()
normalColor := ctx.Style().Palette.Text
overColor := ctx.Style().Palette.Primary
renderer := ctx.Renderer()
for name, keyBounds := range c.allKeys() {
key := c.Keys[name]
texture := key.Symbol.Scaled(scale)
color := normalColor
if mouse.In(keyBounds) {
color = overColor
}
renderer.DrawTexturePointOptions(texture, keyBounds.Min, ui.DrawOptions{Tint: color})
}
}
}
func (c *VirtualControls) SetControls(topLeft, topRight, bottomRight, bottomLeft []string) {
c.TopLeftControls = topLeft
c.TopRightControls = topRight
c.BottomRightControls = bottomRight
c.BottomLeftControls = bottomLeft
}
type VirtualKey struct {
Symbol tins2021.NamedTexture
Key ui.Key
}

36
io.go
View File

@ -1,43 +1,11 @@
package tins2021 package tins2021
import ( import (
"encoding/json"
"os"
"opslag.de/schobers/zntg" "opslag.de/schobers/zntg"
) )
const appName = "tins2021_qbitter" const appName = "tins2021_qbitter"
func UserDir() (string, error) { return zntg.UserConfigDir(appName) } func UserDir() (string, error) { return zntg.UserDir(appName) }
func UserFile(name string) (string, error) { return zntg.UserConfigFile(appName, name) } func UserFile(name string) (string, error) { return zntg.UserFile(appName, name) }
func LoadUserFileJSON(name string, v interface{}) error {
path, err := UserFile(name)
if err != nil {
return err
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return os.ErrNotExist
}
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
return json.NewDecoder(f).Decode(v)
}
func SaveUserFileJSON(name string, v interface{}) error {
path, err := UserFile(name)
if err != nil {
return err
}
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return json.NewEncoder(f).Encode(v)
}

View File

@ -92,15 +92,10 @@ func (l *Level) MoveMonster(target, source geom.Point) {
l.DestroyMonster(source) l.DestroyMonster(source)
} }
type MonsterHit struct { func (l *Level) MovePlayer(dir Direction) bool {
Position geom.Point
Type MonsterType
}
func (l *Level) MovePlayer(dir Direction) (bool, *MonsterHit) {
towards, allowed := l.CanPlayerMove(dir) towards, allowed := l.CanPlayerMove(dir)
if !allowed { if !allowed {
return false, nil return false
} }
l.Player = towards l.Player = towards
tile := l.Tiles[towards] tile := l.Tiles[towards]
@ -114,18 +109,13 @@ func (l *Level) MovePlayer(dir Direction) (bool, *MonsterHit) {
tile.Star = false tile.Star = false
l.Score += 25 l.Score += 25
} }
var hit *MonsterHit
if l.Monsters[towards] != nil { if l.Monsters[towards] != nil {
hit = &MonsterHit{
Position: towards,
Type: l.Monsters[towards].Type(),
}
l.DecrementLive() l.DecrementLive()
l.DestroyMonster(towards) l.DestroyMonster(towards)
l.Score -= 5 l.Score -= 5
} }
l.Score -= 1 // for every move l.Score -= 1 // for every move
return true, hit return true
} }
func (l *Level) Randomize(difficulty int, stars int) { func (l *Level) Randomize(difficulty int, stars int) {

15
math.go
View File

@ -1,15 +0,0 @@
package tins2021
import "opslag.de/schobers/geom"
func Polar(a, r float64) geom.PointF {
return geom.PtF(r*geom.Cos(a), r*geom.Sin(a))
}
func PolarUnity(a float64) geom.PointF {
return geom.PtF(geom.Cos(a), geom.Sin(a))
}
func Percentage(i, n int) float64 {
return float64(i) / float64(n)
}

View File

@ -28,17 +28,19 @@ var (
light = fauxgl.V(.5, 1, .75).Normalize() // light direction light = fauxgl.V(.5, 1, .75).Normalize() // light direction
) )
func Animate(hexColor string, frames int, animator MeshAnimator) image.Image { func animateMesh(mesh *fauxgl.Mesh, hexColor string, frames int, transform MeshAnimationTransformer) image.Image {
const scale = 4 const scale = 4
const s = 1.1 const s = 1.1
mesh.BiUnitCube()
matrix := fauxgl.Orthographic(-s, s, -s, s, near, far).Mul(fauxgl.LookAt(eye, center, up)) matrix := fauxgl.Orthographic(-s, s, -s, s, near, far).Mul(fauxgl.LookAt(eye, center, up))
animation := image.NewNRGBA(image.Rect(0, 0, TextureSize*frames, TextureSize)) animation := image.NewNRGBA(image.Rect(0, 0, TextureSize*frames, TextureSize))
threads := ints.Max(1, runtime.NumCPU()) threads := ints.Max(1, runtime.NumCPU())
framesC := make(chan int, threads) framesC := make(chan int, threads)
wait := parallel(1, func() { wait := parallel(threads, func() {
context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale) context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale)
color := fauxgl.HexColor(hexColor) color := fauxgl.HexColor(hexColor)
@ -51,8 +53,9 @@ func Animate(hexColor string, frames int, animator MeshAnimator) image.Image {
shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`)) shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`))
context.Shader = shader context.Shader = shader
mesh := animator.animate(FrameState{Current: i, TotalFrames: frames}) copy := mesh.Copy()
context.DrawMesh(mesh) transform.transform(copy, FrameState{Current: i, TotalFrames: frames})
context.DrawMesh(copy)
frame := resize.Resize(TextureSize, TextureSize, context.Image(), resize.Bilinear) frame := resize.Resize(TextureSize, TextureSize, context.Image(), resize.Bilinear)
draw.Copy(animation, image.Pt(i*TextureSize, 0), frame, frame.Bounds(), draw.Src, nil) draw.Copy(animation, image.Pt(i*TextureSize, 0), frame, frame.Bounds(), draw.Src, nil)
@ -67,12 +70,12 @@ func Animate(hexColor string, frames int, animator MeshAnimator) image.Image {
return animation return animation
} }
func AnimatePolygon(polygon geom.PolygonF, hexColor string, frames int, transformer MeshTransformer) image.Image { func AnimatePolygon(polygon geom.PolygonF, hexColor string, frames int, transform MeshAnimationTransformer) image.Image {
animation := newMeshAnimation(generateMeshFromPolygon(polygon, .2), transformer) mesh := generateMeshFromPolygon(polygon, .2)
return Animate(hexColor, frames, animation) return animateMesh(mesh, hexColor, frames, transform)
} }
func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, frames int, transformer MeshTransformer) image.Image { func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, frames int, transform MeshAnimationTransformer) image.Image {
path, err := resources.FetchResource(name) path, err := resources.FetchResource(name)
if err != nil { if err != nil {
panic(err) panic(err)
@ -81,53 +84,7 @@ func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, frames in
if err != nil { if err != nil {
panic(err) panic(err)
} }
return Animate(hexColor, frames, newMeshAnimation(mesh, transformer)) return animateMesh(mesh, hexColor, frames, transform)
}
type ExplodingHexagonAnimation struct {
Biunit fauxgl.Matrix
}
func rotateMeshAround(mesh *fauxgl.Mesh, around fauxgl.Vector, angle float64) {
mesh.Transform(fauxgl.Translate(fauxgl.V(-around.X, -around.Y, -around.Z)))
mesh.Transform(fauxgl.Rotate(fauxgl.V(0, 1, 0), angle))
mesh.Transform(fauxgl.Translate(around))
}
func (a *ExplodingHexagonAnimation) animate(s FrameState) *fauxgl.Mesh {
ani := s.Animation()
mesh := fauxgl.NewEmptyMesh()
const parts = 6
const oneThird = float64(1) / 3
const twoThirds = float64(2) / 3
const da = twoThirds * geom.Pi
partHeight := geom.Sqrt(3) / 2
for part := 0; part < parts; part++ {
a := 2 * geom.Pi * float64(part) / float64(parts)
closeLength := (float64(.25) + .75*(1-ani))
center := Polar(a, twoThirds*partHeight)
aa := a + ani*geom.Pi
far := center.Add(Polar(aa, oneThird*partHeight))
farWidth := float64(.5) * (1 - ani)
right := far.Add(Polar(aa-.5*geom.Pi, farWidth))
left := far.Add(Polar(aa+.5*geom.Pi, farWidth))
close := center.Add(Polar(aa+geom.Pi, closeLength*twoThirds*partHeight))
partMesh := generateMeshFromPolygon(geom.PolF(right, left, close), .2)
rotateMeshAround(partMesh, fauxgl.V(-center.X, -center.Y, 0), ani*geom.Pi)
mesh.Add(partMesh)
}
if s.Current == 0 {
a.Biunit = mesh.BiUnitCube()
} else {
mesh.Transform(a.Biunit)
}
return mesh
} }
type FrameState struct { type FrameState struct {
@ -137,7 +94,7 @@ type FrameState struct {
func (s FrameState) Animation() float64 { return float64(s.Current) / float64(s.TotalFrames) } func (s FrameState) Animation() float64 { return float64(s.Current) / float64(s.TotalFrames) }
func generateTrianglesForPolygon(polygon geom.PolygonF, thickness float64) []*fauxgl.Triangle { func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh {
vec := func(p geom.PointF, z float64) fauxgl.Vector { return fauxgl.V(p.X, p.Y, z) } vec := func(p geom.PointF, z float64) fauxgl.Vector { return fauxgl.V(p.X, p.Y, z) }
tri := fauxgl.NewTriangleForPoints tri := fauxgl.NewTriangleForPoints
face := func(q, r, s geom.PointF, n float64) *fauxgl.Triangle { face := func(q, r, s geom.PointF, n float64) *fauxgl.Triangle {
@ -158,11 +115,7 @@ func generateTrianglesForPolygon(polygon geom.PolygonF, thickness float64) []*fa
q, r, s, t := vec(p, back), vec(next, back), vec(next, front), vec(p, front) q, r, s, t := vec(p, back), vec(next, back), vec(next, front), vec(p, front)
triangles = append(triangles, tri(q, r, s), tri(q, s, t)) triangles = append(triangles, tri(q, r, s), tri(q, s, t))
} }
return triangles
}
func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh {
triangles := generateTrianglesForPolygon(polygon, thickness)
mesh := fauxgl.NewTriangleMesh(triangles) mesh := fauxgl.NewTriangleMesh(triangles)
return mesh return mesh
} }
@ -177,54 +130,6 @@ func iterate(n int, threads int) <-chan int {
return iterator return iterator
} }
type MeshAnimation struct {
MeshTransformer
mesh *fauxgl.Mesh
}
func newMeshAnimation(mesh *fauxgl.Mesh, transformer MeshTransformer) *MeshAnimation {
mesh.BiUnitCube()
return &MeshAnimation{transformer, mesh}
}
func (a *MeshAnimation) animate(s FrameState) *fauxgl.Mesh {
mesh := a.mesh.Copy()
a.MeshTransformer.transform(mesh, s)
return mesh
}
type MeshAnimator interface {
animate(FrameState) *fauxgl.Mesh
}
type MeshRotateAnimation struct{}
func (MeshRotateAnimation) transform(mesh *fauxgl.Mesh, s FrameState) {
mesh.Transform(fauxgl.Rotate(up, 2*geom.Pi*s.Animation()))
}
type MeshTransformer interface {
transform(*fauxgl.Mesh, FrameState)
}
type MeshWobbleTransformation struct {
Wobble float64
}
func (a MeshWobbleTransformation) wobble(s FrameState) float64 {
animation := float64(s.Current) / float64(s.TotalFrames)
animation += .25
if animation >= 1 {
animation -= 1
}
return geom.Abs(animation*4-2) - 1
}
func (a MeshWobbleTransformation) transform(mesh *fauxgl.Mesh, s FrameState) {
mesh.Transform(fauxgl.Rotate(up, a.wobble(s)*a.Wobble*geom.Pi/180))
}
func parallel(n int, action func()) *sync.WaitGroup { func parallel(n int, action func()) *sync.WaitGroup {
wait := &sync.WaitGroup{} wait := &sync.WaitGroup{}
wait.Add(n) wait.Add(n)
@ -237,6 +142,33 @@ func parallel(n int, action func()) *sync.WaitGroup {
return wait return wait
} }
type MeshAnimationTransformer interface {
transform(*fauxgl.Mesh, FrameState)
}
type MeshRotateAnimation struct{}
func (MeshRotateAnimation) transform(mesh *fauxgl.Mesh, s FrameState) {
mesh.Transform(fauxgl.Rotate(up, 2*geom.Pi*s.Animation()))
}
type MeshWobbleTransformation struct {
Wobble float64
}
func (a MeshWobbleTransformation) animate(s FrameState) float64 {
animation := float64(s.Current) / float64(s.TotalFrames)
animation += .25
if animation >= 1 {
animation -= 1
}
return geom.Abs(animation*4-2) - 1
}
func (a MeshWobbleTransformation) transform(mesh *fauxgl.Mesh, s FrameState) {
mesh.Transform(fauxgl.Rotate(up, a.animate(s)*a.Wobble*geom.Pi/180))
}
func saveMeshSTL(path, name string, mesh *fauxgl.Mesh) error { func saveMeshSTL(path, name string, mesh *fauxgl.Mesh) error {
stl, err := os.Create(path) stl, err := os.Create(path)
if err != nil { if err != nil {

View File

@ -4,8 +4,6 @@ import (
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
) )
var AllMonsterTypes = []MonsterType{MonsterTypeStraight, MonsterTypeRandom, MonsterTypeChaser}
type ChasingMonster struct{} type ChasingMonster struct{}
func (m ChasingMonster) Type() MonsterType { return MonsterTypeChaser } func (m ChasingMonster) Type() MonsterType { return MonsterTypeChaser }

View File

@ -13,7 +13,7 @@ func CreateHeart() geom.PolygonF {
var polygon geom.PolygonF var polygon geom.PolygonF
const segments = 100 const segments = 100
for segment := 0; segment < 100; segment++ { for segment := 0; segment < 100; segment++ {
t := 2 * geom.Pi * Percentage(segment, segments) t := 2 * geom.Pi * float64(segment) / segments
st := geom.Sin(t) st := geom.Sin(t)
polygon.Points = append(polygon.Points, geom.PtF( polygon.Points = append(polygon.Points, geom.PtF(
16*st*st*st, 16*st*st*st,
@ -23,15 +23,15 @@ func CreateHeart() geom.PolygonF {
} }
func CreateHexagon() geom.PolygonF { func CreateHexagon() geom.PolygonF {
const sides = 6
return CreateRegularPolygon(6)
}
func CreateRegularPolygon(sides int) geom.PolygonF {
var polygon geom.PolygonF var polygon geom.PolygonF
for side := 0; side < sides; side++ { pt := func(rotation float64) geom.PointF {
a := .5*geom.Pi + 2*geom.Pi*rotation
return geom.PtF(geom.Cos(a), geom.Sin(a))
}
const sides = 6
for side := 0; side < 6; side++ {
polygon.Points = append(polygon.Points, polygon.Points = append(polygon.Points,
PolarUnity(float64(side)/float64(sides)), pt(float64(side)/float64(sides)),
) )
} }
return polygon return polygon
@ -39,10 +39,14 @@ func CreateRegularPolygon(sides int) geom.PolygonF {
func CreateStar(sides int) geom.PolygonF { func CreateStar(sides int) geom.PolygonF {
var polygon geom.PolygonF var polygon geom.PolygonF
pt := func(rotation float64) geom.PointF {
a := .5*geom.Pi + 2*geom.Pi*rotation
return geom.PtF(geom.Cos(a), geom.Sin(a))
}
for side := 0; side < sides; side++ { for side := 0; side < sides; side++ {
polygon.Points = append(polygon.Points, polygon.Points = append(polygon.Points,
PolarUnity(float64(side)/float64(sides)), pt(float64(side)/float64(sides)),
PolarUnity((float64(side)+0.5)/float64(sides)).Mul(.5), pt((float64(side)+0.5)/float64(sides)).Mul(.5),
) )
} }
return polygon return polygon

View File

@ -1,88 +0,0 @@
package tins2021
import (
"crypto/sha256"
"encoding/base64"
"fmt"
)
const scoreFileName = "score.json"
type Highscores []Score
func (h Highscores) AddScore(score, difficulty int) (Highscores, bool) {
highscores := len(h)
var rank = highscores
for ; rank > 0; rank-- {
if score <= h[rank-1].Score {
break
}
}
highscore := NewScore(score, difficulty)
if rank == highscores && highscores < 10 {
return append(h, highscore), true
}
if rank < 10 {
h = append(h[:rank], append([]Score{highscore}, h[rank:highscores]...)...)
if len(h) > 10 {
h = h[:10]
}
return h, true
}
return h, false
}
type Score struct {
Score int
Difficulty int
Hash string
}
func NewScore(score, difficulty int) Score {
s := Score{Score: score, Difficulty: difficulty}
s.Hash = s.hash()
return s
}
func (s *Score) hash() string {
hashText := fmt.Sprintf("tins2021_qbitter, %d, %d", s.Score, s.Difficulty)
hash := sha256.Sum256([]byte(hashText))
return base64.StdEncoding.EncodeToString(hash[:])
}
func (s *Score) Validate() bool {
hash := s.hash()
if hash == s.Hash {
return true
}
s.Score = 0
s.Difficulty = 0
return false
}
type ScoreState struct {
Current Score
CurrentLives int
Highscores Highscores
}
func LoadScores() (ScoreState, error) {
var state ScoreState
if err := LoadUserFileJSON(scoreFileName, &state); err != nil {
return ScoreState{}, err
}
state.Current.Validate()
for i := 0; i < len(state.Highscores); {
if !state.Highscores[i].Validate() {
state.Highscores = append(state.Highscores[:i], state.Highscores[i+1:]...)
} else {
i++
}
}
return state, nil
}
func SaveScores(s *ScoreState) error {
return SaveUserFileJSON(scoreFileName, s)
}

View File

@ -1,88 +0,0 @@
package tins2021
import (
"testing"
"github.com/stretchr/testify/assert"
)
func newFullHighscore() Highscores {
return Highscores{
NewScore(100, 100),
NewScore(90, 90),
NewScore(80, 80),
NewScore(70, 70),
NewScore(60, 60),
NewScore(50, 50),
NewScore(40, 40),
NewScore(30, 30),
NewScore(20, 20),
NewScore(10, 10),
}
}
func newHalfEmptyHighscore() Highscores {
return Highscores{
NewScore(100, 100),
NewScore(90, 90),
NewScore(80, 80),
NewScore(70, 70),
NewScore(60, 60),
}
}
func TestAddScoreBelowBottom(t *testing.T) {
h := newFullHighscore()
updated, high := h.AddScore(1, 1)
assert.False(t, high)
assert.Len(t, updated, 10)
for _, s := range h {
assert.Greater(t, s.Score, 1)
}
}
func TestAddScoreBottom(t *testing.T) {
h := newFullHighscore()
updated, high := h.AddScore(11, 11)
assert.True(t, high)
assert.Len(t, updated, 10)
assert.Equal(t, 11, updated[9].Score)
}
func TestAddScoreBottomNotFull(t *testing.T) {
h := Highscores{
NewScore(100, 100),
NewScore(90, 90),
NewScore(80, 80),
NewScore(70, 70),
}
updated, high := h.AddScore(50, 50)
assert.True(t, high)
assert.Len(t, updated, 5)
assert.Equal(t, 50, updated[4].Score)
}
func TestAddScoreMiddle(t *testing.T) {
h := newFullHighscore()
updated, high := h.AddScore(51, 51)
assert.True(t, high)
assert.Len(t, updated, 10)
assert.Equal(t, 51, updated[5].Score)
}
func TestAddScoreMiddleNotFull(t *testing.T) {
h := newHalfEmptyHighscore()
updated, high := h.AddScore(71, 71)
assert.True(t, high)
assert.Len(t, updated, 6)
assert.Equal(t, 71, updated[3].Score)
}
func TestAddScoreTop(t *testing.T) {
h := newFullHighscore()
updated, high := h.AddScore(101, 101)
assert.True(t, high)
assert.Len(t, updated, 10)
assert.Equal(t, 101, updated[0].Score)
}

View File

@ -1 +0,0 @@
go build -tags static -ldflags "-s -w" -o "%GOPATH%/bin/qbitter.exe" opslag.de/schobers/tins2021/cmd/tins2021

View File

@ -4,13 +4,35 @@ import (
"os" "os"
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
) )
const settingsFileName = "settings.json" type Settings struct {
Controls ControlsSettings
Window WindowSettings
}
type AudioSettings struct { func SettingsPath() (string, error) {
SoundVolume float64 return UserFile("settings.json")
MusicVolume float64 }
func (s *Settings) Init() error {
path, err := SettingsPath()
if err != nil {
return err
}
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil
}
return zntg.DecodeJSON(path, s)
}
func (s *Settings) Store() error {
path, err := SettingsPath()
if err != nil {
return err
}
return zntg.EncodeJSON(path, s)
} }
type ControlsSettings struct { type ControlsSettings struct {
@ -19,25 +41,6 @@ type ControlsSettings struct {
MoveDownLeft string MoveDownLeft string
MoveUpLeft string MoveUpLeft string
MoveUpRight string MoveUpRight string
Virtual bool
}
type Settings struct {
Audio AudioSettings
Controls ControlsSettings
Window WindowSettings
}
func (s *Settings) Init() error {
err := LoadUserFileJSON(settingsFileName, s)
if os.IsNotExist(err) {
return nil
}
return err
}
func (s *Settings) Store() error {
return SaveUserFileJSON(settingsFileName, s)
} }
type WindowSettings struct { type WindowSettings struct {