Compare commits

...

17 Commits

Author SHA1 Message Date
1a393c0065 Fixed virtual buttons. 2021-08-20 13:13:43 +02:00
19adf64c54 Added virtual controls.
Added missing mouse pointers where applicable.
2021-08-20 10:21:41 +02:00
aca3a5af78 Fixed bug where audio samples were actually louder instead of softer. 2021-08-19 21:38:22 +02:00
5935a7fea8 Fixed bug where menu song wasn't repeated. 2021-08-19 21:37:29 +02:00
65db973699 Added audio settings. 2021-08-17 18:22:22 +02:00
99e87cef6e Fixed bug where adding scores would truncate the last score even when there is less than ten highscores (the current maximum). 2021-08-15 09:59:36 +02:00
88e6fc4181 Put more emphasis on Allegro in the README.
Added Allegro license to the README.
2021-08-15 08:51:16 +02:00
24db632470 Fixed monster z-fighting. 2021-08-14 09:46:38 +02:00
17008871ce Balance audio a bit. 2021-08-14 09:38:30 +02:00
aab65a984a Added exploding monsters. 2021-08-14 09:22:40 +02:00
3198659d11 Updated credits. 2021-08-13 19:46:58 +02:00
4adfdbe006 Made monsters a bit less synced. 2021-08-13 13:09:31 +02:00
0d49482036 Made loading of audio samples more graceful (game will play without them). 2021-08-13 10:20:03 +02:00
3c99e5881b Saving lives left for resume as well. 2021-08-12 22:35:33 +02:00
e3527eb580 Added music & game sounds.
User can now use mouse to select controls.
2021-08-12 22:28:31 +02:00
cbd08cdc12 Added install.bat. 2021-08-11 19:56:50 +02:00
c47f9383c3 Added background generation. 2021-08-11 19:55:52 +02:00
29 changed files with 1910 additions and 323 deletions

View File

@ -2,23 +2,25 @@
## Content
- [Introduction](##Introduction)
- [Additional Rules](##Additional-Rules)
* [Implementation](###Implementation)
* [Definition](###Definition)
- [Building](##Building)
- [Sources](##Sources)
- [Licenses (third party)](##Licenses)
* [Go-SDL2](###Go-SDL2)
* [SDL 2.0](###SDL-2.0)
* [Fira Mono font](###Fira-Mono-font)
* [Escher font](###Escher-font)
* [Escheresk font](###Escheresk-font)
* [fauxgl](###fauxgl)
* [draw2d](###draw2d)
* [go-colurful](###go-colurful)
* [resize](###resize)
* [testify](###testify)
- [Introduction](#introduction)
- [Additional Rules](#additional-rules)
* [Implementation](#implementation)
* [Definition](#definition)
- [Building](#building)
* [Using Allegro](#using-allegro)
- [Sources](#sources)
- [Licenses (third party)](#licenses)
* [Go-SDL2](#go-sdl2)
* [SDL 2.0](#sdl-2-0)
* [Allegro 5](#allegro-5)
* [Fira Mono font](#fira-mono-font)
* [Escher font](#escher-font)
* [Escheresk font](#escheresk-font)
* [fauxgl](#fauxgl)
* [draw2d](#draw2d)
* [go-colurful](#go-colurful)
* [resize](#resize)
* [testify](#testify)
## Introduction
@ -113,6 +115,8 @@ Prerequisites:
- GCC or a compatible compiler.
- [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:
```
go get -u opslag.de/schobers/tins2021/cmd/tins2021
@ -124,12 +128,14 @@ go generate 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.:
```
go get -u opslag.de/schobers/allg5
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 the Allegro 5.2 (development libraries must be available for your C compiler).
In this case the SDL 2.0 prerequisite is replaced by Allegro 5.2 (development libraries must be available for your C compiler).
## 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.
@ -185,6 +191,20 @@ 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
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
Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.

12
TODO.md
View File

@ -1,12 +1,16 @@
- [X] Increase difficulty.
- [ ] Add music & sounds.
- [X] Add music & sounds.
- [X] Keep score/difficulty level (resume & restart).
- [X] ~~Explain controls on info page~~ add settings for controls.
- [X] Fix usage of go/embed (and remove rice again).
- [X] Add monster animations (~~jumping on tile &~~ towards new tile).
- [ ] Scale icons (heart & star on right side) when playing.
- [X] ~~Scale icons (heart & star on right side) when playing.~~
- [ ] Change layout when playing in portrait mode.
- [X] Fix wobble animation.
- [ ] Add more unit tests?
- [ ] Fix z-fighting of monsters.
- [ ] Add exploding animation of monsters.
- [X] Fix z-fighting of monsters.
- [X] 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,6 +1,9 @@
package main
import (
"log"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/play"
"opslag.de/schobers/zntg/ui"
@ -56,11 +59,42 @@ func (a *app) Init(ctx ui.Context) error {
ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{Align: ui.AlignRight}, false)
a.context = newAppContext(ctx, a.settings, a.score, func(control ui.Control) {
a.Content = control
virtual := NewVirtualControls(nil)
virtual.RegisterKey("confirm", ui.KeyEnter, tins2021.MustCreateNamedTextureImage(textures, "return-key", drawReturnKey()))
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)
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
}

View File

@ -1,6 +1,9 @@
package main
import (
"fmt"
"math/rand"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui"
)
@ -10,27 +13,48 @@ type appContext struct {
Settings *tins2021.Settings
Score *tins2021.ScoreState
Virtual *VirtualControls
Debug bool
StarTexture tins2021.AnimatedTexture
HeartTexture tins2021.AnimatedTexture
StarTexture tins2021.AnimatedTexture
HeartTexture tins2021.AnimatedTexture
MonsterTextureNames map[tins2021.MonsterType]string
MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
DyingMonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
Audio *AudioPlayer
MenuMusic *Music
GameMusic *Music
GameMusicSong int
}
func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.ScoreState, setView func(ui.Control)) *appContext {
textures := textureGenerator{}
app := &appContext{
setView: setView,
Settings: settings,
Score: score,
StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star),
HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart),
MonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{
tins2021.MonsterTypeStraight: newAnimatedTexture(ctx, "straight-walking-monster", defaultAnimationFrames, textures.StraightWalkingMonster),
tins2021.MonsterTypeRandom: newAnimatedTexture(ctx, "random-walking-monster", defaultAnimationFrames, textures.RandomWalkingMonster),
tins2021.MonsterTypeChaser: newAnimatedTexture(ctx, "chasing-monster", defaultAnimationFrames, textures.ChasingMonster),
},
setView: setView,
Settings: settings,
Audio: NewAudioPlayer(ctx.Resources(), "resources/sounds/"),
Score: score,
StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star),
HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart),
MonsterTextureNames: map[tins2021.MonsterType]string{},
MonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{},
DyingMonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{},
}
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
@ -38,12 +62,17 @@ func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.
const numberOfStars = 5
func (app *appContext) MenuInteraction() {
app.Audio.PlaySample("menu_interaction.mp3")
}
func (app *appContext) Play(ctx ui.Context) {
level := tins2021.NewLevel()
level.Randomize(0, numberOfStars)
app.Score.Current = tins2021.Score{}
app.show(newLevelControl(app, ctx, level))
app.ResetCurrentScore()
app.show(ctx, newLevelControl(app, ctx, level))
app.SetVirtualKeys(true, true, true)
}
func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
@ -56,37 +85,141 @@ func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
level.Score = score
level.Lives = lives
app.SetVirtualKeys(true, true, true)
controller.Play(level)
}
func (app *appContext) playNextGameMusic(ctx ui.Context) {
if app.GameMusic != nil {
return
}
const songs = 4
pick := func() int {
for {
s := rand.Intn(songs) + 1
if s == app.GameMusicSong {
continue
}
app.GameMusicSong = s
return s
}
}
song := fmt.Sprintf("song_game_%d.mp3", pick())
app.GameMusic, _ = app.Audio.PlayMusic(song, func(m *Music) {
m.OnFinished = func() {
if app.GameMusic == nil {
return
}
app.GameMusic = nil
app.playNextGameMusic(ctx)
}
})
}
func (app *appContext) 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(newLevelControl(app, ctx, level))
app.show(ctx, newLevelControl(app, ctx, level))
app.SetVirtualKeys(true, true, true)
}
func (app *appContext) show(control ui.Control) {
func (app *appContext) ResetCurrentScore() {
app.Score.Current = tins2021.Score{}
app.Score.CurrentLives = 0
}
func (app *appContext) SetCurrentScore(level *tins2021.Level) {
app.Score.Current = tins2021.NewScore(level.Score, level.Difficulty+1)
app.Score.CurrentLives = level.Lives
}
func (app *appContext) show(ctx ui.Context, control ui.Control) {
app.setView(control)
if _, ok := control.(*levelController); ok {
app.switchToPlayMusic(ctx)
} else {
app.switchToMenuMusic(ctx)
}
}
func (app *appContext) ShowCredits(ctx ui.Context) {
app.setView(newCredits(app, ctx))
app.show(ctx, newCredits(app, ctx))
app.SetVirtualKeys(true, true, false)
}
func (app *appContext) ShowSettings(ctx ui.Context) {
app.setView(newSettings(app, ctx))
app.show(ctx, newSettings(app, ctx))
app.SetVirtualKeys(true, true, false)
}
func (app *appContext) ShowHighscores(ctx ui.Context) {
app.setView(newHighscores(app, ctx))
app.show(ctx, newHighscores(app, ctx))
app.SetVirtualKeys(true, true, false)
}
func (app *appContext) ShowInfo(ctx ui.Context) {
app.setView(newInfo(app, ctx))
app.show(ctx, newInfo(app, ctx))
app.SetVirtualKeys(true, true, false)
}
func (app *appContext) ShowMainMenu(ctx ui.Context) {
app.show(newMainMenu(app, ctx))
app.show(ctx, 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()
}
}

198
cmd/tins2021/audio.go Normal file
View File

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

@ -0,0 +1,64 @@
package main
import (
"image"
"image/color"
"image/png"
"math/rand"
"os"
"github.com/llgcode/draw2d/draw2dimg"
"github.com/nfnt/resize"
"golang.org/x/image/draw"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui"
)
func GenerateBackground(resources ui.Resources, path string) error {
const cubeTextureWidth = 100
cube := resize.Resize(cubeTextureWidth, 0, tins2021.GenerateCube(tins2021.Blue), resize.Bilinear)
inverted := resize.Resize(cubeTextureWidth, 0, tins2021.GenerateHole(tins2021.Blue), resize.Bilinear)
const twelfth = (1. / 6) * geom.Pi
centerTopSquare := geom.PtF(.5, .5*geom.Sin(twelfth))
delta := geom.PtF(geom.Cos(twelfth), .5+centerTopSquare.Y).Mul(cubeTextureWidth).Mul(.5).ToInt().Mul(2)
bounds := image.Rect(2560, 1440, 0, 0)
im := image.NewRGBA(bounds)
var odd bool
for y := -cubeTextureWidth; y < bounds.Max.Y; y += delta.Y {
left := -cubeTextureWidth
if odd {
left += delta.X / 2
}
for x := left; x < bounds.Max.X; x += delta.X {
currentCube := cube
n := rand.Intn(2)
if n == 0 {
currentCube = inverted
}
draw.Copy(im, image.Pt(x, y), currentCube, image.Rect(0, 0, cubeTextureWidth, cubeTextureWidth), draw.Over, nil)
}
odd = !odd
}
ctx := draw2dimg.NewGraphicContext(im)
font, err := parseTitleFont(resources)
if err != nil {
return err
}
setDraw2DFont(ctx, font)
ctx.SetFontSize(224)
const text = "QBITTER"
center := draw2DCenterString(ctx, text)
ctx.SetFillColor(color.White)
ctx.FillStringAt(text, .5*float64(bounds.Dx())+center.X, .5*float64(bounds.Dy())+center.Y)
out, err := os.Create(path)
if err != nil {
return err
}
return png.Encode(out, im)
}

View File

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

View File

@ -0,0 +1,47 @@
package main
import (
"io/ioutil"
"github.com/golang/freetype/truetype"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
func draw2DCenterString(ctx *draw2dimg.GraphicContext, s string) geom.PointF {
left, top, right, bottom := ctx.GetStringBounds(s)
return geom.PtF(-.5*(right-left), .5*(bottom-top))
}
type draw2DFontCache struct{ *truetype.Font }
func (f draw2DFontCache) Load(draw2d.FontData) (*truetype.Font, error) { return f.Font, nil }
func (draw2DFontCache) Store(draw2d.FontData, *truetype.Font) {}
func parseTrueTypeFont(resources ui.Resources, name string) (*truetype.Font, error) {
ttf, err := resources.OpenResource(name)
if err != nil {
return nil, err
}
defer ttf.Close()
data, err := ioutil.ReadAll(ttf)
if err != nil {
return nil, err
}
return truetype.Parse(data)
}
func parseScoreFont(resources ui.Resources) (*truetype.Font, error) {
return parseTrueTypeFont(resources, "resources/fonts/FiraMono-Regular.ttf")
}
func parseTitleFont(resources ui.Resources) (*truetype.Font, error) {
return parseTrueTypeFont(resources, "resources/fonts/escher.ttf")
}
func setDraw2DFont(ctx *draw2dimg.GraphicContext, font *truetype.Font) {
ctx.FontCache = draw2DFontCache{font}
// ctx.SetFont(font) // is ignored anyway
}

View File

@ -44,6 +44,7 @@ func (l *infoLegend) Render(ctx ui.Context) {
}
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) {
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("star", infoLegendIconSize),
@ -54,15 +55,15 @@ func newInfo(app *appContext, ctx ui.Context) ui.Control {
Description: "Gives (back) a life.",
})
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("straight-walking-monster", infoLegendIconSize),
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeStraight), infoLegendIconSize),
Description: "Monster that walks over a fixed diagonal.",
})
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("random-walking-monster", infoLegendIconSize),
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeRandom), infoLegendIconSize),
Description: "Monster that walks randomly.",
})
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("chasing-monster", infoLegendIconSize),
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeChaser), infoLegendIconSize),
Description: "Monster that walks towards you.",
})
})

View File

@ -3,6 +3,7 @@ package main
import (
"fmt"
"image/color"
"log"
"math/rand"
"strconv"
"time"
@ -24,8 +25,10 @@ type levelController struct {
Inverted tins2021.NamedTexture
Animations map[string]*tins2021.Animations
IdleMonsters *tins2021.Animations
MovingMonsters *tins2021.Animations
IdleMonsters *tins2021.Animations
MovingMonsters *tins2021.Animations
DyingMonsters *tins2021.Animations
DyingMonsterTypes map[geom.Point]tins2021.MonsterType
SmallFont *tins2021.BitmapFont
@ -77,6 +80,13 @@ 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)
return control
@ -91,7 +101,7 @@ func (r *levelController) updateHighscore() bool {
if highscore {
r.app.Score.Highscores = highscores
}
r.app.Score.Current = tins2021.Score{} // reset score
r.app.ResetCurrentScore()
return highscore
}
@ -99,9 +109,13 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
switch e := e.(type) {
case *ui.KeyDownEvent:
switch e.Key {
case ui.KeyEnter:
if r.Level.GameOver {
r.app.ShowHighscores(ctx)
}
case ui.KeyEscape:
if r.Level.StarsCollected == r.Level.Stars {
r.app.Score.Current = tins2021.NewScore(r.Level.Score, r.Level.Difficulty+1)
r.app.SetCurrentScore(r.Level)
}
r.app.ShowMainMenu(ctx)
}
@ -116,21 +130,59 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
case *ui.KeyDownEvent:
switch e.Key {
case ui.KeyEnter:
r.app.Score.Current = tins2021.NewScore(r.Level.Score, r.Level.Difficulty+1)
r.app.SetCurrentScore(r.Level)
r.app.PlayNext(ctx, r)
}
}
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) {
case *ui.KeyDownEvent:
dir, ok := r.Controls[e.Key]
if ok {
r.Level.MovePlayer(dir)
if r.Level.GameOver {
r.Highscore = r.updateHighscore()
stars, lives := r.Level.StarsCollected, r.Level.Lives
_, 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()
}
}
@ -139,6 +191,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
}
r.IdleMonsters.Update()
r.MovingMonsters.Update()
r.DyingMonsters.Update()
var jumped []geom.Point
for pos, animation := range r.MovingMonsters.Values {
if animation.Frame < 40 { // after 40 frames the player hit is checked
@ -146,12 +199,11 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
}
target := r.Level.MonsterTargets[pos]
if target == r.Level.Player { // player is hit
monsterHit(&tins2021.MonsterHit{Position: target, Type: r.Level.Monsters[pos].Type()})
r.Level.DestroyMonster(pos)
jumped = append(jumped, pos)
r.Level.DecrementLive()
if r.Level.GameOver {
r.Highscore = r.updateHighscore()
}
checkGameOver()
continue
}
if animation.Frame < 50 { // after 50 frames the animation has finished
@ -169,7 +221,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
var jumping []geom.Point
for pos, animation := range r.IdleMonsters.Values {
for animation.Frame > 0 {
if rand.Intn(10) != 0 {
if rand.Intn(100) < 37 { // P = .37
monster, ok := r.Level.Monsters[pos]
if ok && monster != nil {
target, ok := monster.FindTarget(r.Level, pos)
@ -177,6 +229,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
r.Level.MonsterTargets[pos] = target
r.MovingMonsters.Frame(pos)
jumping = append(jumping, pos)
r.app.Audio.PlaySample("monster_jump.mp3")
break
}
}
@ -200,8 +253,10 @@ func (r *levelController) Play(level *tins2021.Level) {
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
"monster": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
}
r.IdleMonsters = tins2021.NewAnimations(200*time.Millisecond, 100, false, false)
r.IdleMonsters = tins2021.NewAnimations(40*time.Millisecond, 100, 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 {
r.IdleMonsters.Frame(monster)
}
@ -215,7 +270,8 @@ func (r levelController) Render(ctx ui.Context) {
centerTopSquare := geom.PtF32(.5, .5*geom.Sin32(twelfth))
delta := geom.PtF32(geom.Cos32(twelfth), .5+centerTopSquare.Y)
view := r.Bounds().Size()
bounds := r.Bounds()
view := bounds.Size()
levelView := geom.PtF32(float32(r.Level.Bounds.Dx()+2)*delta.X, float32(r.Level.Bounds.Dy()+2)*delta.Y)
textureWidth := geom.Min32(
geom.Floor32(tins2021.TextureSize*view.X*.75/(levelView.X*tins2021.TextureSize)),
@ -225,15 +281,15 @@ func (r levelController) Render(ctx ui.Context) {
delta = delta.Mul(textureWidth)
centerTopSquare = centerTopSquare.Mul(textureWidth)
scoreView := geom.RectF32(levelView.X*textureWidth, offsetY+delta.Y, view.X, view.Y-delta.Y-offsetY)
scoreView := geom.RectF32(levelView.X*textureWidth, offsetY+delta.Y, view.X, view.Y-delta.Y-offsetY).Add(bounds.Min)
delta.X = geom.Round32(delta.X)
delta.Y = geom.Round32(delta.Y)
toScreen := func(p geom.Point) geom.PointF32 {
if p.Y%2 == 0 {
return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, offsetY)
return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, offsetY).Add(bounds.Min)
}
return p.ToF32().Mul2D(delta.XY()).Add2D(0, offsetY)
return p.ToF32().Mul2D(delta.XY()).Add2D(0, offsetY).Add(bounds.Min)
}
renderer := ctx.Renderer()
@ -250,6 +306,10 @@ func (r levelController) Render(ctx ui.Context) {
for typ, animation := range r.app.MonsterTextures {
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
propOffset := geom.PtF32(-.5*float32(propHeight), -.8*float32(propHeight))
@ -297,23 +357,46 @@ func (r levelController) Render(ctx ui.Context) {
renderer.DrawTexturePoint(player, playerPosition.Add(centerTopSquare))
}
for pos, monsterType := range r.Level.Monsters {
tile := r.Level.Tiles[pos]
if tile == nil {
for y := r.Level.Bounds.Min.Y; y < r.Level.Bounds.Max.Y; y++ {
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]
if tile == nil {
continue
}
texture := monsterTextures[monsterType.Type()]
_, platformPos := positionOfTile(pos)
if target, ok := r.Level.MonsterTargets[pos]; ok {
_, targetPlatformPos := positionOfTile(target)
dt := float32(r.MovingMonsters.Frame(pos)) / 50.
delta := targetPlatformPos.Sub(platformPos)
curve := geom.PtF32(0, .6*geom.Sin32(dt*geom.Pi)*textureWidth)
interpolatedPos := platformPos.Add(delta.Mul(dt)).Sub(curve)
texture.Draw(renderer, interpolatedPos.Add(propOffset), r.Animations["monster"].Frame(pos))
} else {
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 := monsterTextures[monsterType.Type()]
texture := dyingMonsterTextures[monster]
_, platformPos := positionOfTile(pos)
if target, ok := r.Level.MonsterTargets[pos]; ok {
_, targetPlatformPos := positionOfTile(target)
dt := float32(r.MovingMonsters.Frame(pos)) / 50.
delta := targetPlatformPos.Sub(platformPos)
curve := geom.PtF32(0, .6*geom.Sin32(dt*geom.Pi)*textureWidth)
interpolatedPos := platformPos.Add(delta.Mul(dt)).Sub(curve)
texture.Draw(renderer, interpolatedPos.Add(propOffset), r.Animations["monster"].Frame(pos))
} else {
texture.Draw(renderer, platformPos.Add(propOffset), r.Animations["monster"].Frame(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
@ -340,12 +423,13 @@ func (r levelController) Render(ctx ui.Context) {
scoreTopLeft.Y -= scoreFont.Height()
renderer.Text(scoreFont, scoreTopLeft, textColor, "Score:")
bounds := r.Bounds()
centerX := .5 * bounds.Dx()
centerX := bounds.Min.X + .5*bounds.Dx()
titleFont := ctx.Fonts().Font("title")
screenSize := ctx.Renderer().Size().ToF32()
screen := geom.RectF32(0, 0, screenSize.X, screenSize.Y)
if r.Level.GameOver {
renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`))
renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`))
offsetY := .5*bounds.Dy() - titleFont.Height()
renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "GAME OVER", ui.AlignCenter)
@ -358,12 +442,15 @@ func (r levelController) Render(ctx ui.Context) {
offsetY += titleFont.Height() + scoreFont.Height()
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()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [escape] to quit.", ui.AlignCenter)
} else if r.Level.StarsCollected == r.Level.Stars {
renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`))
renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`))
offsetY := .5*bounds.Dy() - titleFont.Height()
renderer.TextAlign(titleFont, geom.PtF32(.5*bounds.Dx(), offsetY), textColor, "COMPLETED", ui.AlignCenter)
renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "COMPLETED", ui.AlignCenter)
offsetY += titleFont.Height() + scoreFont.Height()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Score: %d", r.Level.Score), ui.AlignCenter)

View File

@ -29,6 +29,8 @@ func (c *center) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.P
type mainMenu struct {
ui.StackPanel
music *Music
}
func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
@ -37,15 +39,23 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
menu.Add("Play", func(ctx ui.Context) {
app.Play(ctx)
})
if app.Score.Current.Difficulty > 0 {
resume := app.Score.Current.Difficulty > 0
if resume {
menu.Add("Resume", func(ctx ui.Context) { app.PlayResume(ctx) })
}
menu.Add("Highscores", func(ctx ui.Context) { app.ShowHighscores(ctx) })
menu.Add("Controls", func(ctx ui.Context) { app.ShowSettings(ctx) })
menu.Add("Settings", func(ctx ui.Context) { app.ShowSettings(ctx) })
menu.Add("Credits", func(ctx ui.Context) { app.ShowCredits(ctx) })
menu.Add("Quit", func(ctx ui.Context) { ctx.Quit() })
menu.Activate(1) // play
if resume {
menu.Activate(2) // resume
} else {
menu.Activate(1) // play
}
menu.ActiveChanged.AddHandlerEmpty((func(ui.Context) {
app.MenuInteraction()
}))
ctx.Animate()
return Center(&mainMenu{

View File

@ -11,6 +11,8 @@ type Menu struct {
active int
buttons []*MenuButton
ActiveChanged ui.Events
}
func NewMenu() *Menu {
@ -25,7 +27,7 @@ func (m *Menu) Activate(i int) {
if len(m.buttons) == 0 || i < 0 {
return
}
m.updateActiveButton(i % len(m.buttons))
m.updateActiveButton(nil, i%len(m.buttons))
}
func (m *Menu) Add(text string, click func(ui.Context)) {
@ -54,29 +56,34 @@ func (m *Menu) Handle(ctx ui.Context, e ui.Event) bool {
onEscape(ctx)
}
case ui.KeyDown:
m.updateActiveButton((m.active + 1) % len(m.buttons))
m.updateActiveButton(ctx, (m.active+1)%len(m.buttons))
case ui.KeyUp:
m.updateActiveButton((m.active + len(m.buttons) - 1) % len(m.buttons))
m.updateActiveButton(ctx, (m.active+len(m.buttons)-1)%len(m.buttons))
case ui.KeyEnter:
m.buttons[m.active].InvokeClick(ctx)
}
case *ui.MouseMoveEvent:
for i, button := range m.buttons {
if button.IsOver() {
m.updateActiveButton(i)
m.updateActiveButton(ctx, i)
ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer)
break
}
}
m.updateActiveButton(m.active)
m.updateActiveButton(ctx, m.active)
}
return false
}
func (m *Menu) updateActiveButton(active int) {
func (m *Menu) updateActiveButton(ctx ui.Context, active int) {
change := m.active != active
m.active = active
for i, btn := range m.buttons {
btn.Over = i == m.active
}
if change && ctx != nil {
m.ActiveChanged.Notify(ctx, nil)
}
}
type MenuButton struct {

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

@ -4,7 +4,6 @@ import (
"fmt"
"image"
"image/color"
"io/ioutil"
"github.com/golang/freetype/truetype"
"github.com/llgcode/draw2d"
@ -15,6 +14,12 @@ import (
"opslag.de/schobers/zntg/ui"
)
const (
controlsTypeWASD = "wasd"
controlsTypeArrows = "arrows"
controlsTypeCustom = "custom"
)
const keyboardKeyCornerRadius = .1 * keyboardKeyWidth
const keyboardKeyHeight = .2 * keyboardLayoutTextureWidth
const keyboardKeySkew = .15
@ -22,15 +27,40 @@ const keyboardKeyWidth = .25 * keyboardLayoutTextureWidth
const keyboardLayoutTextureHeight = 256
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) {
const cornerRadius = keyboardKeyCornerRadius
const keyHeight_5 = .5 * keyboardKeyHeight
const keyWidth_5 = .5 * keyboardKeyWidth
skewed := func(x, y float64) (float64, float64) {
x, y = skewedKeyboardCoordinates(x, y)
return center.X + x, center.Y + y
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
var keyHeight_5 = .5 * size.Y
var keyWidth_5 = .5 * size.X
skewed := func(x, y float64) (float64, float64) {
return center.X + x - skew*y, center.Y + y
}
corner := func(x, y, start float64) {
for a := start; a <= start+.25; a += .025 {
aa := a * 2 * geom.Pi
@ -52,22 +82,8 @@ func drawKey(ctx *draw2dimg.GraphicContext, font *truetype.Font, center geom.Poi
corner(-keyWidth_5+cornerRadius, keyHeight_5-cornerRadius, .5)
ctx.Close()
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 {
return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.53, .25), Key: '↑'},
@ -106,7 +122,7 @@ func generateKeys(resources ui.Resources, keys ...keyboardLayoutKey) image.Image
im := image.NewRGBA(image.Rect(0, 0, keyboardLayoutTextureWidth, keyboardLayoutTextureHeight))
ctx := draw2dimg.NewGraphicContext(im)
font, err := parseFont(resources)
font, err := parseScoreFont(resources)
if err != nil {
panic(err)
}
@ -123,6 +139,68 @@ func generateKeys(resources ui.Resources, keys ...keyboardLayoutKey) image.Image
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 {
return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.45, .25), Key: 'W'},
@ -138,37 +216,20 @@ type keyboardLayoutKey struct {
Highlight bool
}
func parseFont(resources ui.Resources) (*truetype.Font, error) {
ttf, err := resources.OpenResource("resources/fonts/FiraMono-Regular.ttf")
if err != nil {
return nil, err
}
defer ttf.Close()
data, err := ioutil.ReadAll(ttf)
if err != nil {
return nil, err
}
return truetype.Parse(data)
}
type keyboardLayoutSettings struct {
ui.ControlBase
type settings struct {
ui.StackPanel
app *appContext
app *appContext
handler SettingHandler
Active bool
ActiveLayout int
SelectedLayout int
SelectingCustom int
}
const (
controlsTypeWASD = "wasd"
controlsTypeArrows = "arrows"
controlsTypeCustom = "custom"
)
func newSettings(app *appContext, ctx ui.Context) *settings {
func newKeyboardLayoutSettings(app *appContext, ctx ui.Context, handler SettingHandler) *keyboardLayoutSettings {
ctx.Textures().CreateTextureGo("layout-wasd", generateWASDKeys(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)
@ -184,11 +245,366 @@ func newSettings(app *appContext, ctx ui.Context) *settings {
layout = 2
}
settings := &settings{app: app, ActiveLayout: layout, SelectedLayout: layout}
settings := &keyboardLayoutSettings{app: app, handler: handler, ActiveLayout: layout, SelectedLayout: layout}
settings.renderCustomLayout(ctx)
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{
ui.KeyA: "A",
ui.KeyB: "B",
@ -218,130 +634,266 @@ var supportedCustomKeys = map[ui.Key]string{
ui.KeyZ: "Z",
}
func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
if s.StackPanel.Handle(ctx, e) {
type toggleControl struct {
ui.StackPanel
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
}
switch e := e.(type) {
case *ui.KeyDownEvent:
if s.SelectingCustom > 0 {
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:
s.ActiveLayout = (s.ActiveLayout + 2) % 3
if c.Active {
c.changeVolume(-1)
}
case ui.KeyRight:
s.ActiveLayout = (s.ActiveLayout + 1) % 3
case ui.KeyEnter:
switch s.ActiveLayout {
if c.Active {
c.changeVolume(1)
}
}
case *ui.MouseMoveEvent:
over := c.isOver(ctx, e.Pos())
switch over {
case -1:
c.overLeftKnob = false
c.overBar = -1
c.overRightKnob = false
case 0:
if !c.overLeftKnob {
c.handler.Interacted()
}
c.overLeftKnob = true
c.overBar = -1
c.overRightKnob = false
case 11:
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:
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
c.changeVolume(-1)
case 11:
c.changeVolume(1)
default:
c.setVolume(float64(over + minVolume))
}
}
}
return false
}
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) IsActive() bool { return c.Active }
func (c *volumeControl) isOver(ctx ui.Context, p geom.PointF32) int {
bounds := c.Bounds()
p = p.Sub(c.Offset())
font := ctx.Fonts().Font("default")
layouts := []string{
"WASD",
"ARROWS",
"CUSTOM",
top := bounds.Min.Y + 1.5*font.Height()
bottom := top + font.Height()
if p.Y < top || p.Y >= bottom {
return -1
}
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
highlightColor := ctx.Style().Palette.Primary
activeColor := ctx.Style().Palette.Primary
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
bounds := c.Bounds()
center := bounds.Center()
renderer := ctx.Renderer()
top := bounds.Min.Y
fontColor := normalColor
if c.Active {
fontColor = activeColor
}
renderer.TextAlign(font, geom.PtF32(center.X, top), fontColor, c.Name, ui.AlignCenter)
tint := func(active bool) color.Color {
if active {
return activeColor
}
if s.SelectedLayout == i {
layoutColor = highlightColor
}
renderer.TextAlign(font, geom.PtF32(layoutCenter, center.Y-2*font.Height()), textColor, layout, ui.AlignCenter)
renderer.DrawTexturePointOptions(ctx.Textures().ScaledByName(layoutTextures[i], scale), geom.PtF32(layoutLeft, center.Y), ui.DrawOptions{Tint: layoutColor})
return nil
}
if s.SelectingCustom > 0 {
renderer.FillRectangle(bounds, zntg.MustHexColor(`#000000DF`))
selectTexture := fmt.Sprintf("layout-select-%d", s.SelectingCustom)
layoutLeft := .36 * width
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))
leftKnob, leftKnobRegion := c.texture.Scaled("leftKnob", scale)
leftKnobWidth := leftKnobRegion.Dx()
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 (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 (c *volumeControl) SetActive(_ ui.Context, active bool) { c.Active = active }
func skewedKeyboardCoordinates(x, y float64) (float64, float64) {
return x - keyboardKeySkew*y, y
func (c *volumeControl) setVolume(volume float64) {
volume = geom.Min(maxVolume, geom.Max(minVolume, volume))
if c.Volume == volume {
return
}
c.Volume = volume
c.VolumeChanged(c.Volume)
c.handler.Interacted()
}

View File

@ -16,12 +16,29 @@ func (textureGenerator) Heart() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, defaultAnimationFrames, tins2021.MeshRotateAnimation{})
}
func (textureGenerator) monster(color string) image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), color, defaultAnimationFrames, tins2021.MeshWobbleTransformation{Wobble: 30})
func (g textureGenerator) MonsterTypeColor(typ tins2021.MonsterType) string {
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) ChasingMonster() image.Image { return g.monster(tins2021.Purple) }
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})
}
}
func (g textureGenerator) RandomWalkingMonster() image.Image { return g.monster(tins2021.Blue) }
func (g textureGenerator) StraightWalkingMonster() image.Image { return g.monster(tins2021.Green) }
func (g textureGenerator) DyingMonster(typ tins2021.MonsterType) func() image.Image {
color := g.MonsterTypeColor(typ)
return func() image.Image {
return tins2021.Animate(color, defaultAnimationFrames, &tins2021.ExplodingHexagonAnimation{})
}
}

View File

@ -0,0 +1,42 @@
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,14 +29,20 @@ func openResources() ui.Resources {
}
func run() error {
var background string
var extract bool
flag.StringVar(&background, "background", "", "generates a background")
flag.BoolVar(&extract, "extract", false, "extracts all resources to the current working directory")
flag.Parse()
if extract {
return copyBoxToDisk(resources)
}
res := openResources()
if background != "" {
return GenerateBackground(res, background)
}
ptPtr := func(x, y int) *geom.Point {
p := geom.Pt(x, y)

View File

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

View File

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

15
math.go Normal file
View File

@ -0,0 +1,15 @@
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,19 +28,17 @@ var (
light = fauxgl.V(.5, 1, .75).Normalize() // light direction
)
func animateMesh(mesh *fauxgl.Mesh, hexColor string, frames int, transform MeshAnimationTransformer) image.Image {
func Animate(hexColor string, frames int, animator MeshAnimator) image.Image {
const scale = 4
const s = 1.1
mesh.BiUnitCube()
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))
threads := ints.Max(1, runtime.NumCPU())
framesC := make(chan int, threads)
wait := parallel(threads, func() {
wait := parallel(1, func() {
context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale)
color := fauxgl.HexColor(hexColor)
@ -53,9 +51,8 @@ func animateMesh(mesh *fauxgl.Mesh, hexColor string, frames int, transform MeshA
shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`))
context.Shader = shader
copy := mesh.Copy()
transform.transform(copy, FrameState{Current: i, TotalFrames: frames})
context.DrawMesh(copy)
mesh := animator.animate(FrameState{Current: i, TotalFrames: frames})
context.DrawMesh(mesh)
frame := resize.Resize(TextureSize, TextureSize, context.Image(), resize.Bilinear)
draw.Copy(animation, image.Pt(i*TextureSize, 0), frame, frame.Bounds(), draw.Src, nil)
@ -70,12 +67,12 @@ func animateMesh(mesh *fauxgl.Mesh, hexColor string, frames int, transform MeshA
return animation
}
func AnimatePolygon(polygon geom.PolygonF, hexColor string, frames int, transform MeshAnimationTransformer) image.Image {
mesh := generateMeshFromPolygon(polygon, .2)
return animateMesh(mesh, hexColor, frames, transform)
func AnimatePolygon(polygon geom.PolygonF, hexColor string, frames int, transformer MeshTransformer) image.Image {
animation := newMeshAnimation(generateMeshFromPolygon(polygon, .2), transformer)
return Animate(hexColor, frames, animation)
}
func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, frames int, transform MeshAnimationTransformer) image.Image {
func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, frames int, transformer MeshTransformer) image.Image {
path, err := resources.FetchResource(name)
if err != nil {
panic(err)
@ -84,7 +81,53 @@ func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, frames in
if err != nil {
panic(err)
}
return animateMesh(mesh, hexColor, frames, transform)
return Animate(hexColor, frames, newMeshAnimation(mesh, transformer))
}
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 {
@ -94,7 +137,7 @@ type FrameState struct {
func (s FrameState) Animation() float64 { return float64(s.Current) / float64(s.TotalFrames) }
func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh {
func generateTrianglesForPolygon(polygon geom.PolygonF, thickness float64) []*fauxgl.Triangle {
vec := func(p geom.PointF, z float64) fauxgl.Vector { return fauxgl.V(p.X, p.Y, z) }
tri := fauxgl.NewTriangleForPoints
face := func(q, r, s geom.PointF, n float64) *fauxgl.Triangle {
@ -115,7 +158,11 @@ func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.M
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))
}
return triangles
}
func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh {
triangles := generateTrianglesForPolygon(polygon, thickness)
mesh := fauxgl.NewTriangleMesh(triangles)
return mesh
}
@ -130,6 +177,54 @@ func iterate(n int, threads int) <-chan int {
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 {
wait := &sync.WaitGroup{}
wait.Add(n)
@ -142,33 +237,6 @@ func parallel(n int, action func()) *sync.WaitGroup {
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 {
stl, err := os.Create(path)
if err != nil {

View File

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

View File

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

View File

@ -23,7 +23,11 @@ func (h Highscores) AddScore(score, difficulty int) (Highscores, bool) {
return append(h, highscore), true
}
if rank < 10 {
return append(h[:rank], append([]Score{highscore}, h[rank:highscores-1]...)...), true
h = append(h[:rank], append([]Score{highscore}, h[rank:highscores]...)...)
if len(h) > 10 {
h = h[:10]
}
return h, true
}
return h, false
}
@ -57,8 +61,9 @@ func (s *Score) Validate() bool {
}
type ScoreState struct {
Current Score
Highscores Highscores
Current Score
CurrentLives int
Highscores Highscores
}
func LoadScores() (ScoreState, error) {

View File

@ -21,6 +21,16 @@ func newFullHighscore() Highscores {
}
}
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)
@ -61,6 +71,14 @@ func TestAddScoreMiddle(t *testing.T) {
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)

1
scripts/install.bat Normal file
View File

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

View File

@ -8,7 +8,22 @@ import (
const settingsFileName = "settings.json"
type AudioSettings struct {
SoundVolume float64
MusicVolume float64
}
type ControlsSettings struct {
Type string
MoveDownRight string
MoveDownLeft string
MoveUpLeft string
MoveUpRight string
Virtual bool
}
type Settings struct {
Audio AudioSettings
Controls ControlsSettings
Window WindowSettings
}
@ -25,14 +40,6 @@ func (s *Settings) Store() error {
return SaveUserFileJSON(settingsFileName, s)
}
type ControlsSettings struct {
Type string
MoveDownRight string
MoveDownLeft string
MoveUpLeft string
MoveUpRight string
}
type WindowSettings struct {
Location *geom.Point
Size *geom.Point