Compare commits

...

13 Commits

25 changed files with 1449 additions and 328 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.

10
TODO.md
View File

@ -8,7 +8,9 @@
- [ ] 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.
- [ ] Add audio settings (music & sound volume).
- [X] Hearts must be saved as well for resume.
- [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,24 +59,40 @@ 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)
err := a.context.Audio.LoadSample(
if err := a.context.Audio.LoadSample(
"level_completed.mp3",
"level_game_over.mp3",
"level_new_high_score.mp3",
"menu_interaction.mp3",
"monster_jump.mp3",
"player_collect_heart.mp3",
"player_collect_star.mp3",
"player_hurt.mp3",
); 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",
)
if err != nil {
return err
); err != nil {
log.Printf("failed to load samples; %v\n", err)
}
return nil

View File

@ -13,11 +13,14 @@ type appContext struct {
Settings *tins2021.Settings
Score *tins2021.ScoreState
Virtual *VirtualControls
Debug bool
StarTexture tins2021.AnimatedTexture
HeartTexture tins2021.AnimatedTexture
MonsterTextures map[tins2021.MonsterType]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
Audio *AudioPlayer
MenuMusic *Music
@ -28,17 +31,30 @@ type appContext struct {
func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.ScoreState, setView func(ui.Control)) *appContext {
textures := textureGenerator{}
app := &appContext{
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),
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
@ -56,6 +72,7 @@ func (app *appContext) Play(ctx ui.Context) {
app.ResetCurrentScore()
app.show(ctx, newLevelControl(app, ctx, level))
app.SetVirtualKeys(true, true, true)
}
func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
@ -68,57 +85,10 @@ 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) 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))
}
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.show(ctx, newCredits(app, ctx))
}
func (app *appContext) ShowSettings(ctx ui.Context) {
app.show(ctx, newSettings(app, ctx))
}
func (app *appContext) ShowHighscores(ctx ui.Context) {
app.show(ctx, newHighscores(app, ctx))
}
func (app *appContext) ShowInfo(ctx ui.Context) {
app.show(ctx, newInfo(app, ctx))
}
func (app *appContext) ShowMainMenu(ctx ui.Context) {
app.show(ctx, newMainMenu(app, ctx))
}
func (app *appContext) playNextGameMusic(ctx ui.Context) {
if app.GameMusic != nil {
return
@ -146,6 +116,93 @@ func (app *appContext) playNextGameMusic(ctx ui.Context) {
})
}
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)
if _, ok := control.(*levelController); ok {
app.switchToPlayMusic(ctx)
} else {
app.switchToMenuMusic(ctx)
}
}
func (app *appContext) ShowCredits(ctx ui.Context) {
app.show(ctx, newCredits(app, ctx))
app.SetVirtualKeys(true, true, false)
}
func (app *appContext) ShowSettings(ctx ui.Context) {
app.show(ctx, 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) {
app.show(ctx, newInfo(app, ctx))
app.SetVirtualKeys(true, true, false)
}
func (app *appContext) ShowMainMenu(ctx ui.Context) {
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
@ -159,7 +216,7 @@ func (app *appContext) switchToMenuMusic(ctx ui.Context) {
if app.MenuMusic != nil {
return
}
app.MenuMusic, _ = app.Audio.PlayMusic("song_menu.mp3", func(m *Music) {})
app.MenuMusic, _ = app.Audio.PlayMusic("song_menu.mp3", func(m *Music) { m.AutoRepeat = true })
gameMusic := app.GameMusic
app.GameMusic = nil
if gameMusic != nil {

View File

@ -9,6 +9,8 @@ import (
"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"
)
@ -33,21 +35,29 @@ func NewAudioPlayer(resources ui.Resources, prefix string) *AudioPlayer {
prefix: prefix,
SampleRate: rate,
Samples: map[string]Sample{},
SampleVolume: 1,
MusicVolume: 1,
SampleVolume: 0,
MusicVolume: 0,
}
}
func (p *AudioPlayer) LoadSample(name ...string) error {
for _, name := range name {
if _, err := p.openSample(name); err != nil {
return err
}
}
return nil
return p.LoadSampleVolume(0, name...)
}
func (p *AudioPlayer) openSample(name string) (Sample, error) {
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
@ -60,17 +70,23 @@ func (p *AudioPlayer) openSample(name string) (Sample, error) {
buffer := beep.NewBuffer(format)
buffer.Append(stream)
sample = Sample{Buffer: buffer, SampleRate: format.SampleRate}
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)
sample, err := p.openSample(name, 0)
if err != nil {
return err
}
speaker.Play(p.resample(sample.Stream(), sample.SampleRate))
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
}
@ -91,8 +107,8 @@ func (p *AudioPlayer) PlayMusic(name string, init func(*Music)) (*Music, error)
Volume: &effects.Volume{
Streamer: p.resample(closer, format.SampleRate),
Base: 2,
Volume: 1,
Silent: false,
Volume: p.MusicVolume,
Silent: p.MusicVolume <= minVolume,
},
}
if init != nil {
@ -143,6 +159,7 @@ func (m Music) Finished(player *AudioPlayer) {
type Sample struct {
*beep.Buffer
Volume float64
SampleRate beep.SampleRate
}

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

@ -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
@ -99,6 +109,10 @@ 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.SetCurrentScore(r.Level)
@ -125,6 +139,8 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
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")
@ -134,25 +150,39 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
}
}
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 {
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:
r.app.Audio.PlaySample("player_hurt.mp3")
monsterHit(hit)
case r.Level.Lives > lives:
r.app.Audio.PlaySample("player_collect_heart.mp3")
}
r.app.Audio.PlaySample("player_move.mp3")
checkGameOver()
if r.Level.StarsCollected == r.Level.Stars {
r.app.Audio.PlaySample("level_completed.mp3")
}
checkLevelCompleted()
}
}
@ -161,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
@ -168,10 +199,10 @@ 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()
r.app.Audio.PlaySample("player_hurt.mp3")
checkGameOver()
continue
}
@ -190,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)
@ -222,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)
}
@ -237,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)),
@ -247,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()
@ -272,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))
@ -319,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
@ -362,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)
@ -380,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

@ -44,7 +44,7 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
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() })

View File

@ -66,6 +66,7 @@ func (m *Menu) Handle(ctx ui.Context, e ui.Event) bool {
for i, button := range m.buttons {
if button.IsOver() {
m.updateActiveButton(ctx, i)
ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer)
break
}
}

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

@ -6,6 +6,7 @@ import (
"image/color"
"github.com/golang/freetype/truetype"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021"
@ -13,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
@ -20,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
@ -50,14 +82,6 @@ func drawKey(ctx *draw2dimg.GraphicContext, font *truetype.Font, center geom.Poi
corner(-keyWidth_5+cornerRadius, keyHeight_5-cornerRadius, .5)
ctx.Close()
ctx.Stroke()
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 generateArrowKeys(resources ui.Resources) image.Image {
@ -115,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'},
@ -130,24 +216,20 @@ type keyboardLayoutKey struct {
Highlight bool
}
type settings struct {
ui.StackPanel
type keyboardLayoutSettings struct {
ui.ControlBase
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)
@ -163,42 +245,19 @@ 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
}
var supportedCustomKeys = map[ui.Key]string{
ui.KeyA: "A",
ui.KeyB: "B",
ui.KeyC: "C",
ui.KeyD: "D",
ui.KeyE: "E",
ui.KeyF: "F",
ui.KeyG: "G",
ui.KeyH: "H",
ui.KeyI: "I",
ui.KeyJ: "J",
ui.KeyK: "K",
ui.KeyL: "L",
ui.KeyM: "M",
ui.KeyN: "N",
ui.KeyO: "O",
ui.KeyP: "P",
ui.KeyQ: "Q",
ui.KeyR: "R",
ui.KeyS: "S",
ui.KeyT: "T",
ui.KeyU: "U",
ui.KeyV: "V",
ui.KeyW: "W",
ui.KeyX: "X",
ui.KeyY: "Y",
ui.KeyZ: "Z",
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 *settings) Handle(ctx ui.Context, e ui.Event) bool {
if s.StackPanel.Handle(ctx, e) {
func (s *keyboardLayoutSettings) Handle(ctx ui.Context, e ui.Event) bool {
if s.ControlBase.Handle(ctx, e) {
return true
}
switch e := e.(type) {
@ -241,19 +300,28 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
s.app.MenuInteraction()
return true
case ui.KeyLeft:
s.setActiveLayout(s.ActiveLayout - 1)
if s.Active {
s.setActiveLayout(s.ActiveLayout - 1)
}
case ui.KeyRight:
s.setActiveLayout(s.ActiveLayout + 1)
if s.Active {
s.setActiveLayout(s.ActiveLayout + 1)
}
case ui.KeyEnter:
s.selectLayout()
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 {
@ -268,34 +336,23 @@ func (s *settings) Handle(ctx ui.Context, e ui.Event) bool {
return false
}
func (s *settings) 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) IsActive() bool { return s.Active }
func (s *settings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int {
func (s *keyboardLayoutSettings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int {
bounds := s.Bounds()
center := bounds.Center()
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
top := center.Y - 2*font.Height()
bottom := center.Y + scale*keyboardLayoutTextureWidth
if mouse.In(geom.RectF32(left, top, right, bottom)) {
return i
}
@ -303,23 +360,37 @@ func (s *settings) isOverLayout(ctx ui.Context, mouse geom.PointF32) int {
return -1
}
func (s *settings) setActiveLayout(layout int) {
layout = (layout + 3) % 3
change := layout != s.ActiveLayout
s.ActiveLayout = (layout + 3) % 3
if change {
s.app.MenuInteraction()
}
}
func (s *settings) Render(ctx ui.Context) {
func (s *keyboardLayoutSettings) PostRender(ctx ui.Context) {
bounds := s.Bounds()
center := bounds.Center()
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{
@ -332,6 +403,8 @@ func (s *settings) Render(ctx ui.Context) {
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
@ -345,23 +418,12 @@ func (s *settings) Render(ctx ui.Context) {
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})
}
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))
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 *settings) renderCustomLayout(ctx ui.Context) {
func (s *keyboardLayoutSettings) renderCustomLayout(ctx ui.Context) {
runeOrQuestionMark := func(s string) rune {
if len(s) == 0 {
return '?'
@ -377,6 +439,461 @@ func (s *settings) renderCustomLayout(ctx ui.Context) {
ctx.Textures().CreateTextureGo("layout-custom", generateCustomKeys(ctx.Resources(), customKeys), true)
}
func skewedKeyboardCoordinates(x, y float64) (float64, float64) {
return x - keyboardKeySkew*y, y
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",
ui.KeyC: "C",
ui.KeyD: "D",
ui.KeyE: "E",
ui.KeyF: "F",
ui.KeyG: "G",
ui.KeyH: "H",
ui.KeyI: "I",
ui.KeyJ: "J",
ui.KeyK: "K",
ui.KeyL: "L",
ui.KeyM: "M",
ui.KeyN: "N",
ui.KeyO: "O",
ui.KeyP: "P",
ui.KeyQ: "Q",
ui.KeyR: "R",
ui.KeyS: "S",
ui.KeyT: "T",
ui.KeyU: "U",
ui.KeyV: "V",
ui.KeyW: "W",
ui.KeyX: "X",
ui.KeyY: "Y",
ui.KeyZ: "Z",
}
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:
switch e.Key {
case ui.KeyLeft:
if c.Active {
c.changeVolume(-1)
}
case ui.KeyRight:
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:
c.changeVolume(-1)
case 11:
c.changeVolume(1)
default:
c.setVolume(float64(over + minVolume))
}
}
}
return false
}
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")
top := bounds.Min.Y + 1.5*font.Height()
bottom := top + font.Height()
if p.Y < top || p.Y >= bottom {
return -1
}
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
activeColor := ctx.Style().Palette.Primary
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
}
return nil
}
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 (c *volumeControl) SetActive(_ ui.Context, active bool) { c.Active = active }
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

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

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)

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