Compare commits

...

30 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
99d9d09c2f Merge branch 'master' of https://opslag.de/schobers/tins2021 2021-08-11 08:03:12 +02:00
4f1760ad57 Add resume play.
Add highscores.
2021-08-11 08:03:02 +02:00
c628ae4b09 Changed appearance of settings a little. 2021-08-11 06:23:20 +02:00
c52f2682e0 Changed release directory in release script. 2021-08-10 22:46:32 +02:00
000348339d Added support for changing controls.
Changed info screen slightly.
2021-08-10 22:36:42 +02:00
165d1fcd26 Fixed extract command (and changed it to --extract). 2021-08-10 19:41:30 +02:00
f9da21b61c Added console build tag (which doesn't hide the console window). 2021-08-10 19:40:59 +02:00
7053e7b9f2 Reverted back to go:embed.
Parallelized generation of textures.
Refactored animation rendering.
2021-08-10 19:33:30 +02:00
64ff5ac78e Increased difficulty a lot.
Added jumping animations for monsters.
2021-08-09 18:30:23 +02:00
90b79c7e49 Fixed wobble animation (animation was not stable).
- Scaling textures to integer width/height.
2021-08-09 17:26:28 +02:00
6ce3d84417 Extended TODO. 2021-08-09 16:12:20 +02:00
7541fa0085 Updated wording a bit. 2021-08-09 15:30:06 +02:00
44f6217bc7 Added TODO. 2021-08-09 15:23:45 +02:00
47 changed files with 2843 additions and 554 deletions

1
.gitignore vendored
View File

@ -1,4 +1,3 @@
.vscode/launch.json
scripts/build
cmd/tins2021/rice-box.go

View File

@ -2,24 +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)
* [go.rice](###rice)
- [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
@ -114,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
@ -125,15 +128,17 @@ 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-resources` flag. The resources will be extract in the current working directory. Note that the game will use the resources on disk first if they are available.
You can extract all resources embedded in the executable by running it from the command line with the `--extract` flag. The resources will be extract in the current working directory. Note that the game will use the resources on disk first if they are available.
## Sources
Can be found at https://opslag.de/schobers/tins2021 (Git repository).
@ -186,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.
@ -388,28 +407,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
### rice
Copyright (c) 2013, Geert-Johan Riemer
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON 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.

16
TODO.md Normal file
View File

@ -0,0 +1,16 @@
- [X] Increase difficulty.
- [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).
- [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?
- [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

54
animatedtexture.go Normal file
View File

@ -0,0 +1,54 @@
package tins2021
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
type AnimatedTexture struct {
texture NamedTexture
animation ui.Texture
frames []geom.RectangleF32
}
func newAnimatedTexture(texture NamedTexture, animation ui.Texture, n int) AnimatedTexture {
frames := make([]geom.RectangleF32, 0, n)
height := float32(animation.Height())
width := float32(animation.Width())
for i := 0; i < n; i++ {
left := width * float32(i) / float32(n)
right := width * float32(i+1) / float32(n)
frames = append(frames, geom.RectF32(left, 0, right, height))
}
return AnimatedTexture{texture: texture, animation: animation, frames: frames}
}
func NewAnimatedTexture(texture NamedTexture, n int) AnimatedTexture {
return newAnimatedTexture(texture, texture.Texture(), n)
}
func FitAnimatedTexture(texture NamedTexture, scale float32, n int) AnimatedTexture {
height := float32(texture.Texture().Height())
scale = ScaleRound(height, scale)
return newAnimatedTexture(texture, texture.Scaled(scale), n)
}
func (t AnimatedTexture) Draw(renderer ui.Renderer, pos geom.PointF32, frame int) {
renderer.DrawTexturePointOptions(t.animation, pos, ui.DrawOptions{Source: &t.frames[frame]})
}
func (t AnimatedTexture) FrameSize(i int) geom.PointF32 { return t.frames[i].Size() }
func (t AnimatedTexture) Frames() int { return len(t.frames) }
func (t AnimatedTexture) Scale(scale float32) AnimatedTexture {
return FitAnimatedTexture(t.texture, scale, t.Frames())
}
func FindScaleRound(length, desired float32) float32 {
return ScaleRound(length, desired/length)
}
func ScaleRound(length, scale float32) float32 {
return geom.Round32(length*scale) / length
}

View File

@ -5,51 +5,8 @@ import (
"time"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
type AnimatedTexture struct {
Texture ui.Texture
Frames []geom.RectangleF32
}
func NewAnimatedTexture(texture ui.Texture, n int) AnimatedTexture {
frames := make([]geom.RectangleF32, 0, n)
height := float32(texture.Height())
width := float32(texture.Width())
for i := 0; i < n; i++ {
left := width * float32(i) / float32(n)
right := width * float32(i+1) / float32(n)
frames = append(frames, geom.RectF32(left, 0, right, height))
}
return AnimatedTexture{Texture: texture, Frames: frames}
}
func NewSquareAnimatedTexture(texture ui.Texture) AnimatedTexture {
var frames []geom.RectangleF32
height := float32(texture.Height())
width := float32(texture.Width())
for left := float32(0); left < width; left += height {
frames = append(frames, geom.RectF32(left, 0, left+height, height))
}
return AnimatedTexture{Texture: texture, Frames: frames}
}
func (t AnimatedTexture) Scale(scale float32) AnimatedTexture {
frames := make([]geom.RectangleF32, 0, len(t.Frames))
for _, frame := range t.Frames {
frames = append(frames, geom.RectangleF32{Min: frame.Min.Mul(scale), Max: frame.Max.Mul(scale)})
}
return AnimatedTexture{
Texture: t.Texture,
Frames: frames,
}
}
func (t AnimatedTexture) Draw(renderer ui.Renderer, pos geom.PointF32, frame int) {
renderer.DrawTexturePointOptions(t.Texture, pos, ui.DrawOptions{Source: &t.Frames[frame]})
}
type Animation struct {
LastUpdate time.Time
Frame int

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"
@ -10,6 +13,7 @@ type app struct {
ui.Proxy
settings *tins2021.Settings
score *tins2021.ScoreState
context *appContext
}
@ -35,18 +39,18 @@ const fpsOverlayName = `fps`
func (a *app) Init(ctx ui.Context) error {
if err := a.loadFonts(ctx,
fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12},
fontDescriptor{"default", "fonts/escheresk.ttf", 48},
fontDescriptor{"small", "fonts/escheresk.ttf", 16},
fontDescriptor{"score", "fonts/FiraMono-Regular.ttf", 24},
fontDescriptor{"title", "fonts/escher.ttf", 80},
fontDescriptor{"debug", "resources/fonts/FiraMono-Regular.ttf", 12},
fontDescriptor{"default", "resources/fonts/escheresk.ttf", 48},
fontDescriptor{"small", "resources/fonts/escheresk.ttf", 16},
fontDescriptor{"score", "resources/fonts/FiraMono-Regular.ttf", 24},
fontDescriptor{"title", "resources/fonts/escher.ttf", 80},
); err != nil {
return err
}
textureLoader := tins2021.NewResourceLoader()
textures := ctx.Textures()
if err := textureLoader.LoadFromFile(ctx.Resources(), "textures.txt", func(name, content string) error {
if err := textureLoader.LoadFromFile(ctx.Resources(), "resources/textures.txt", func(name, content string) error {
_, err := textures.CreateTexturePath(name, content, true)
return err
}); err != nil {
@ -55,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, 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,7 +1,8 @@
package main
import (
"image"
"fmt"
"math/rand"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui"
@ -10,46 +11,68 @@ import (
type appContext struct {
setView func(ui.Control)
Debug bool
Settings *tins2021.Settings
Score *tins2021.ScoreState
Virtual *VirtualControls
Debug bool
MonsterTextureNames map[tins2021.MonsterType]string
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
GameMusic *Music
GameMusicSong int
}
func newAppContext(ctx ui.Context, setView func(ui.Control)) *appContext {
newAnimatedTexture(ctx, "star", "images/star.png", func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateStar(5), tins2021.Yellow, tins2021.NewRotateAnimation(defaultAnimationFrames))
})
newAnimatedTexture(ctx, "heart", "images/heart.png", func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, tins2021.NewRotateAnimation(defaultAnimationFrames))
})
func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.ScoreState, setView func(ui.Control)) *appContext {
textures := textureGenerator{}
app := &appContext{
setView: setView,
MonsterTextureNames: map[tins2021.MonsterType]string{
tins2021.MonsterTypeStraight: "straight-walking-monster",
tins2021.MonsterTypeRandom: "random-walking-monster",
tins2021.MonsterTypeChaser: "chasing-monster",
},
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{},
}
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeStraight], "images/monster-straight.png", func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Green, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
})
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeRandom], "images/monster-random.png", func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Blue, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
})
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeChaser], "images/monster-chaser.png", func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Purple, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
})
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
}
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.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) {
@ -62,21 +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) show(control ui.Control) {
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(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.setView(newCredits(app, ctx))
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.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,14 +36,16 @@ 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",
" - https://liballeg.org", "",
"SDL: another cross-platform development library",
" - https://libsdl.org", "",
"go-sdl2: an SDL abstraction for Go",
"go-sdl2: a SDL abstraction for Go",
" - https://github.com/veandco/go-sdl2", "",
"Fira Mono: monotype font",
"Escheresk: M.C. Escher themed font",
@ -58,8 +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", "",
"rice: a library for embedding files in Go",
" - https://github.com/GeertJohan/go.rice", "",
"beep: a sound library",
" - https://github.com/faiface/beep", "",
"oto: a low level sound library",
" - https://https://github.com/hajimehoshi/oto", "",
"",
"# THANKS",
"",
@ -81,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:
@ -148,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
@ -184,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

@ -1,12 +1,11 @@
package main
import (
"embed"
"io"
"io/fs"
"os"
"path/filepath"
rice "github.com/GeertJohan/go.rice"
)
func copyFile(path string, file fs.File) error {
@ -21,19 +20,19 @@ func copyFile(path string, file fs.File) error {
return err
}
func copyBoxToDisk() error {
box := rice.MustFindBox(`resources`)
return box.Walk("", func(path string, info os.FileInfo, err error) error {
func copyBoxToDisk(resources embed.FS) error {
return fs.WalkDir(resources, ".", func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if info.IsDir() {
if entry.IsDir() {
return nil
}
src, err := box.Open(path)
src, err := resources.Open(path)
if err != nil {
return err
}
return copyFile(filepath.Join(box.Name(), path), src)
defer src.Close()
return copyFile(path, src)
})
}

View File

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

View File

@ -6,15 +6,19 @@ import (
"opslag.de/schobers/zntg/ui"
)
const infoLegendIconSize = .36
const infoLegendIconMargin = 4.
const infoLegendSeparatorMargin = 24
const infoText = "Qbitter is a game loosly based on the work \"LW305 Kringloop\" of M.C. Escher where a gnome runs down a stairs and morphs into its 2D abstract shape. The game also lends ideas from Q*Bert, a game from the eighties that is based on the works of M.C. Escher. \n\nIn the game you (represented as a gnome) have to collect stars while trying to avoid enemies (hexagons). Every level has increasing difficulty."
var infoLegendIconRectangle = geom.RectRelF32(0, 0, infoLegendIconSize*tins2021.TextureSize, infoLegendIconSize*tins2021.TextureSize)
type info struct {
ui.StackPanel
app *appContext
legend *ui.StackPanel
app *appContext
}
const infoText = "Qbitter is a game loosly based on a work \"LW305 Kringloop\" of M.C. Escher where a gnome runs down a stairs and morphs into its 2D abstract shape. The game also lends ideas from Q*Bert, a game from the eighties that was itself based on the works of M.C. Escher. In the game you (represented as a gnome) have to collect stars while trying to avoid enemies (hexagons). Every level has increasing difficulty."
type infoLegend struct {
ui.ControlBase
@ -23,44 +27,43 @@ type infoLegend struct {
}
func (l *infoLegend) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 {
return geom.PtF32(geom.NaN32(), .4*tins2021.TextureSize)
textureHeight := float32(l.Icon.Height())
font := ctx.Fonts().Font("score")
fontHeight := float32(font.Height())
return geom.PtF32(textureHeight+infoLegendSeparatorMargin+font.WidthOf(l.Description), geom.Max32(textureHeight+2*infoLegendIconMargin, fontHeight))
}
const infoLegenIconSize = .36
var infoLegendIconRectangle = geom.RectRelF32(0, 0, infoLegenIconSize*tins2021.TextureSize, infoLegenIconSize*tins2021.TextureSize)
func (l *infoLegend) Render(ctx ui.Context) {
bounds := l.Bounds()
separator := .3 * bounds.Dx()
textureHeight := float32(l.Icon.Height())
renderer := ctx.Renderer()
renderer.DrawTexturePointOptions(l.Icon, bounds.Min.Add2D(separator-textureHeight, 0), ui.DrawOptions{Source: &infoLegendIconRectangle})
renderer.DrawTexturePointOptions(l.Icon, bounds.Min, ui.DrawOptions{Source: &infoLegendIconRectangle})
font := ctx.Fonts().Font("score")
fontHeight := float32(font.Height())
renderer.Text(font, bounds.Min.Add2D(separator+12, .5*(textureHeight-fontHeight)), ctx.Style().Palette.Text, l.Description)
renderer.Text(font, bounds.Min.Add2D(textureHeight+infoLegendSeparatorMargin, .5*(textureHeight-fontHeight)+infoLegendIconMargin), ctx.Style().Palette.Text, l.Description)
}
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", infoLegenIconSize),
Icon: ctx.Textures().ScaledByName("star", infoLegendIconSize),
Description: "Collect them to complete the level.",
})
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("heart", infoLegenIconSize),
Icon: ctx.Textures().ScaledByName("heart", infoLegendIconSize),
Description: "Gives (back) a life.",
})
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("straight-walking-monster", infoLegenIconSize),
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", infoLegenIconSize),
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeRandom), infoLegendIconSize),
Description: "Monster that walks randomly.",
})
p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("chasing-monster", infoLegenIconSize),
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeChaser), infoLegendIconSize),
Description: "Monster that walks towards you.",
})
})
@ -71,7 +74,7 @@ func newInfo(app *appContext, ctx ui.Context) ui.Control {
label("", "score"), // spacing
paragraphOpts(infoText, "score", labelOptions{TextAlignment: ui.AlignCenter}),
label("", "score"), // spacing
legend,
Center(legend),
},
},
Orientation: ui.OrientationVertical,

View File

@ -3,6 +3,7 @@ package main
import (
"fmt"
"image/color"
"log"
"math/rand"
"strconv"
"time"
@ -20,25 +21,71 @@ type levelController struct {
Level *tins2021.Level
Cubes cubeTexture
Cube tins2021.NamedTexture
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
Controls map[ui.Key]tins2021.Direction
Highscore bool
}
func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController {
control := &levelController{app: app}
textures := ctx.Textures()
control.Cubes = newCubeTexture(textures, tins2021.Orange)
control.Cube = tins2021.MustCreateNamedTextureImage(textures, "cube", tins2021.GenerateCube(tins2021.Orange))
control.Inverted = tins2021.MustCreateNamedTextureImage(textures, "cube_inverted", tins2021.GenerateHole(tins2021.Orange))
small, err := tins2021.NewBitmapFont(ctx.Renderer(), ctx.Fonts().Font("small"), tins2021.NumericCharacters...)
if err != nil {
panic(err)
}
control.SmallFont = small
switch app.Settings.Controls.Type {
case controlsTypeArrows:
control.Controls = map[ui.Key]tins2021.Direction{
ui.KeyUp: tins2021.DirectionUpLeft,
ui.KeyLeft: tins2021.DirectionDownLeft,
ui.KeyDown: tins2021.DirectionDownRight,
ui.KeyRight: tins2021.DirectionUpRight,
}
case controlsTypeCustom:
find := func(s string, defaultKey ui.Key) ui.Key {
for key, setting := range supportedCustomKeys {
if s == setting {
return key
}
}
return defaultKey
}
control.Controls = map[ui.Key]tins2021.Direction{
find(app.Settings.Controls.MoveUpLeft, ui.KeyW): tins2021.DirectionUpLeft,
find(app.Settings.Controls.MoveDownLeft, ui.KeyA): tins2021.DirectionDownLeft,
find(app.Settings.Controls.MoveDownRight, ui.KeyS): tins2021.DirectionDownRight,
find(app.Settings.Controls.MoveUpRight, ui.KeyD): tins2021.DirectionUpRight,
}
default:
control.Controls = map[ui.Key]tins2021.Direction{
ui.KeyW: tins2021.DirectionUpLeft,
ui.KeyA: tins2021.DirectionDownLeft,
ui.KeyS: tins2021.DirectionDownRight,
ui.KeyD: tins2021.DirectionUpRight,
}
}
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)
@ -49,11 +96,27 @@ func IsModifierPressed(modifiers ui.KeyModifier, pressed ui.KeyModifier) bool {
return modifiers&pressed == pressed
}
func (r *levelController) updateHighscore() bool {
highscores, highscore := r.app.Score.Highscores.AddScore(r.Level.Score, r.Level.Difficulty)
if highscore {
r.app.Score.Highscores = highscores
}
r.app.ResetCurrentScore()
return highscore
}
func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
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)
}
r.app.ShowMainMenu(ctx)
}
}
@ -67,23 +130,59 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
case *ui.KeyDownEvent:
switch e.Key {
case ui.KeyEnter:
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:
switch e.Key {
case ui.KeyW:
r.Level.MovePlayer(tins2021.DirectionUpLeft)
case ui.KeyD:
r.Level.MovePlayer(tins2021.DirectionUpRight)
case ui.KeyS:
r.Level.MovePlayer(tins2021.DirectionDownRight)
case ui.KeyA:
r.Level.MovePlayer(tins2021.DirectionDownLeft)
dir, ok := r.Controls[e.Key]
if ok {
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()
}
}
@ -92,19 +191,22 @@ 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 < 15 {
if animation.Frame < 40 { // after 40 frames the player hit is checked
continue
}
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()
checkGameOver()
continue
}
if animation.Frame < 20 {
if animation.Frame < 50 { // after 50 frames the animation has finished
continue
}
r.Level.MoveMonster(target, pos)
@ -119,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)
@ -127,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
}
}
@ -146,17 +249,17 @@ func (r *levelController) Play(level *tins2021.Level) {
r.Level = level
r.Animations = map[string]*tins2021.Animations{
"star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true),
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
"star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true),
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
"monster": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
}
r.IdleMonsters = tins2021.NewAnimations(500*time.Millisecond, 100, false, false)
r.MovingMonsters = tins2021.NewAnimations(50*time.Millisecond, 20, 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)
}
for _, monster := range r.app.MonsterTextureNames {
r.Animations[monster] = tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true)
}
}
const defaultAnimationFrames = 20
@ -167,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)),
@ -177,38 +281,43 @@ 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()
textures := ctx.Textures()
cubes := r.Cubes.Scaled(textures, scale)
cube := r.Cube.Scaled(scale)
inverted := r.Inverted.Scaled(scale)
cubeWidth := float32(cubes.Normal.Width())
cubeHeight := float32(cubes.Normal.Height())
cubeWidth := float32(cube.Width())
cubeHeight := float32(cube.Height())
player := ctx.Textures().ScaledByName("dwarf", scale*.6)
star := tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName("star", scale*.4), defaultAnimationFrames)
heart := tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName("heart", scale*.4), defaultAnimationFrames)
star := r.app.StarTexture.Scale(scale * .4)
heart := r.app.HeartTexture.Scale(scale * .4)
monsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{}
for typ, name := range r.app.MonsterTextureNames {
monsterTextures[typ] = tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName(name, scale*.4), defaultAnimationFrames)
for typ, animation := range r.app.MonsterTextures {
monsterTextures[typ] = animation.Scale(scale * .4)
}
propOffset := geom.PtF32(-.5*float32(star.Texture.Height()), -.8*float32(star.Texture.Height()))
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))
distances := r.Level.Tiles.Distances(r.Level.Player)
positionOfTile := func(position geom.Point, tile *tins2021.Tile) (topLeft, centerOfPlatform geom.PointF32) {
positionOfTile := func(position geom.Point) (topLeft, centerOfPlatform geom.PointF32) {
topLeft = toScreen(position)
if tile.Inversed {
if r.Level.Tiles[position].Inversed {
return topLeft, topLeft.Add2D(.5*float32(cubeWidth), .6*float32(cubeHeight))
}
return topLeft, topLeft.Add2D(.5*float32(cubeWidth), .2*float32(cubeHeight))
@ -221,10 +330,10 @@ func (r levelController) Render(ctx ui.Context) {
if tile == nil {
continue
}
screenPos, platformPos := positionOfTile(pos, tile)
tileTexture := cubes.Normal.Texture
screenPos, platformPos := positionOfTile(pos)
tileTexture := cube
if tile.Inversed {
tileTexture = cubes.Inversed.Texture
tileTexture = inverted
}
renderer.DrawTexturePoint(tileTexture, screenPos)
if r.app.Debug {
@ -248,19 +357,51 @@ 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
}
_, platformPos := positionOfTile(pos, tile)
name := r.app.MonsterTextureNames[monsterType.Type()]
monsterTextures[monsterType.Type()].Draw(renderer, platformPos.Add(propOffset), r.Animations[name].Frame(pos))
texture := dyingMonsterTextures[monster]
_, platformPos := positionOfTile(pos)
texture.Draw(renderer, platformPos.Add(propOffset), frame)
}
for _, pos := range died {
delete(r.DyingMonsters.Values, pos)
delete(r.DyingMonsterTypes, pos)
}
textColor := ctx.Style().Palette.Text
scoreFont := ctx.Fonts().Font("score")
fontOffsetY := .5 * (float32(star.Texture.Height()) - scoreFont.Height())
fontOffsetY := .5 * (float32(propHeight) - scoreFont.Height())
// stars & hearts
scoreTopLeft := scoreView.Min
@ -282,24 +423,34 @@ 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)
if r.Highscore {
highscoreFont := ctx.Fonts().Font("default")
offsetY += titleFont.Height() + scoreFont.Height()
renderer.TextAlign(highscoreFont, geom.PtF32(centerX, offsetY), ctx.Style().Palette.Primary, "NEW HIGHSCORE", ui.AlignCenter)
}
offsetY += titleFont.Height() + scoreFont.Height()
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,10 +39,23 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
menu.Add("Play", func(ctx ui.Context) {
app.Play(ctx)
})
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("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 {

View File

@ -1 +1 @@
dwarf: images/dwarf.png
dwarf: resources/textures/gnome.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

899
cmd/tins2021/settings.go Normal file
View File

@ -0,0 +1,899 @@
package main
import (
"fmt"
"image"
"image/color"
"github.com/golang/freetype/truetype"
"github.com/llgcode/draw2d"
"github.com/llgcode/draw2d/draw2dimg"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
)
const (
controlsTypeWASD = "wasd"
controlsTypeArrows = "arrows"
controlsTypeCustom = "custom"
)
const keyboardKeyCornerRadius = .1 * keyboardKeyWidth
const keyboardKeyHeight = .2 * keyboardLayoutTextureWidth
const keyboardKeySkew = .15
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 keyHeight_5 = .5 * keyboardKeyHeight
skewed := func(x, y float64) (float64, float64) {
return center.X + x - keyboardKeySkew*y, center.Y + y
}
drawKeyOutline(ctx, center, geom.PtF(keyboardKeyWidth, keyboardKeyHeight), keyboardKeySkew, color)
setDraw2DFont(ctx, font)
ctx.SetFontSize(keyHeight_5)
text := fmt.Sprintf("%c", key)
textCenter := draw2DCenterString(ctx, text)
textX, textY := skewed(textCenter.X, textCenter.Y)
ctx.FillStringAt(text, textX, textY)
}
func drawKeyOutline(ctx *draw2dimg.GraphicContext, center, size geom.PointF, skew float64, color color.Color) {
const cornerRadius = keyboardKeyCornerRadius
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
ctx.LineTo(skewed(x+cornerRadius*geom.Cos(aa), y-cornerRadius*geom.Sin(aa)))
}
}
ctx.SetLineWidth(3)
ctx.SetStrokeColor(color)
ctx.SetFillColor(color)
ctx.MoveTo(skewed(-keyWidth_5+cornerRadius, keyHeight_5))
ctx.LineTo(skewed(keyWidth_5-cornerRadius, keyHeight_5))
corner(keyWidth_5-cornerRadius, keyHeight_5-cornerRadius, .75)
ctx.LineTo(skewed(keyWidth_5, -keyHeight_5+cornerRadius))
corner(keyWidth_5-cornerRadius, -keyHeight_5+cornerRadius, 0)
ctx.LineTo(skewed(-keyWidth_5+cornerRadius, -keyHeight_5))
corner(-keyWidth_5+cornerRadius, -keyHeight_5+cornerRadius, .25)
ctx.LineTo(skewed(-keyWidth_5, keyHeight_5-cornerRadius))
corner(-keyWidth_5+cornerRadius, keyHeight_5-cornerRadius, .5)
ctx.Close()
ctx.Stroke()
}
func generateArrowKeys(resources ui.Resources) image.Image {
return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.53, .25), Key: '↑'},
keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: '←'},
keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: '↓'},
keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: '→'},
)
}
func generateArrowKeysHighlight(resources ui.Resources, highlight [4]bool) image.Image {
spaceOrRune := func(r rune, space bool) rune {
if space {
return ' '
}
return r
}
return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.53, .25), Key: spaceOrRune('↑', !highlight[0]), Highlight: highlight[0]},
keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: spaceOrRune('←', !highlight[1]), Highlight: highlight[1]},
keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: spaceOrRune('↓', !highlight[2]), Highlight: highlight[2]},
keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: spaceOrRune('→', !highlight[3]), Highlight: highlight[3]},
)
}
func generateCustomKeys(resources ui.Resources, keys [4]rune) image.Image {
return generateKeys(resources,
keyboardLayoutKey{Position: geom.PtF(.45, .25), Key: keys[0]},
keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: keys[1]},
keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: keys[2]},
keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: keys[3]},
)
}
func generateKeys(resources ui.Resources, keys ...keyboardLayoutKey) image.Image {
im := image.NewRGBA(image.Rect(0, 0, keyboardLayoutTextureWidth, keyboardLayoutTextureHeight))
ctx := draw2dimg.NewGraphicContext(im)
font, err := parseScoreFont(resources)
if err != nil {
panic(err)
}
for _, key := range keys {
var color color.Color = color.White
if key.Highlight {
color = zntg.MustHexColor(tins2021.Orange)
}
center := geom.PtF(key.Position.X*keyboardLayoutTextureWidth, key.Position.Y*keyboardLayoutTextureHeight)
drawKey(ctx, font, center, key.Key, color)
}
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'},
keyboardLayoutKey{Position: geom.PtF(.2, .75), Key: 'A'},
keyboardLayoutKey{Position: geom.PtF(.5, .75), Key: 'S'},
keyboardLayoutKey{Position: geom.PtF(.8, .75), Key: 'D'},
)
}
type keyboardLayoutKey struct {
Position geom.PointF
Key rune
Highlight bool
}
type keyboardLayoutSettings struct {
ui.ControlBase
app *appContext
handler SettingHandler
Active bool
ActiveLayout int
SelectedLayout int
SelectingCustom int
}
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)
ctx.Textures().CreateTextureGo("layout-select-2", generateArrowKeysHighlight(ctx.Resources(), [4]bool{false, true, false, false}), true)
ctx.Textures().CreateTextureGo("layout-select-3", generateArrowKeysHighlight(ctx.Resources(), [4]bool{false, false, true, false}), true)
ctx.Textures().CreateTextureGo("layout-select-4", generateArrowKeysHighlight(ctx.Resources(), [4]bool{false, false, false, true}), true)
var layout int
switch app.Settings.Controls.Type {
case controlsTypeArrows:
layout = 1
case controlsTypeCustom:
layout = 2
}
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",
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

@ -0,0 +1,44 @@
package main
import (
"image"
"opslag.de/schobers/tins2021"
)
type textureGenerator struct{}
func (textureGenerator) Star() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateStar(5), tins2021.Yellow, defaultAnimationFrames, tins2021.MeshRotateAnimation{})
}
func (textureGenerator) Heart() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, defaultAnimationFrames, tins2021.MeshRotateAnimation{})
}
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) 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) 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

@ -1,48 +1,28 @@
package main
import (
"fmt"
"image"
"image/png"
"log"
"os"
"path/filepath"
"time"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui"
)
type namedTexture struct {
ui.Texture
Name string
func chrono(action func()) time.Duration {
start := time.Now()
action()
return time.Now().Sub(start)
}
func newNamedTexture(textures *ui.Textures, name string, im image.Image) namedTexture {
texture, err := textures.CreateTextureGo(name, im, true)
if err != nil {
panic(err)
}
return namedTexture{texture, name}
}
func (t namedTexture) Scaled(textures *ui.Textures, scale float32) namedTexture {
return namedTexture{textures.ScaledByName(t.Name, scale), t.Name}
}
type cubeTexture struct {
Normal, Inversed namedTexture
}
func newCubeTexture(textures *ui.Textures, color string) cubeTexture {
return cubeTexture{
Normal: newNamedTexture(textures, "cube_"+color, tins2021.GenerateCube(color)),
Inversed: newNamedTexture(textures, "cube_"+color+"_inversed", tins2021.GenerateHole(color)),
}
}
func (t cubeTexture) Scaled(textures *ui.Textures, scale float32) cubeTexture {
return cubeTexture{
Normal: t.Normal.Scaled(textures, scale),
Inversed: t.Inversed.Scaled(textures, scale),
}
func chronoErr(action func() error) (time.Duration, error) {
start := time.Now()
err := action()
return time.Now().Sub(start), err
}
func loadTextureImage(ctx ui.Context, resource string) image.Image {
@ -58,26 +38,29 @@ func loadTextureImage(ctx ui.Context, resource string) image.Image {
return im
}
func saveTextureImage(resource string, im image.Image) {
func newAnimatedTexture(ctx ui.Context, name string, frames int, render func() image.Image) tins2021.AnimatedTexture {
resourceName := fmt.Sprintf("resources/textures/%s.png", name)
raw := loadTextureImage(ctx, resourceName)
if raw == nil {
renderTime := chrono(func() {
raw = render()
})
if err := saveTextureImage(resourceName, raw); err != nil {
log.Printf("failed to write animated texture \"%s\" to disk (%s); error: %v\n", name, resourceName, err)
} else {
log.Printf("generated animated texture \"%s\" in %v (and saved to disk \"%s\")\n", name, renderTime, resourceName)
}
}
texture := tins2021.MustCreateNamedTextureImage(ctx.Textures(), name, raw)
return tins2021.NewAnimatedTexture(texture, frames)
}
func saveTextureImage(resource string, im image.Image) error {
os.MkdirAll(filepath.Dir(resource), 0744)
out, err := os.Create(resource)
if err != nil {
return
return err
}
defer out.Close()
png.Encode(out, im)
return png.Encode(out, im)
}
func newAnimatedTexture(ctx ui.Context, name, resource string, render animatedTextureRenderFn) tins2021.AnimatedTexture {
raw := loadTextureImage(ctx, resource)
if raw == nil {
raw = render()
saveTextureImage(resource, raw)
}
texture, err := ctx.Textures().CreateTextureGo(name, raw, true)
if err != nil {
panic(err)
}
return tins2021.NewSquareAnimatedTexture(texture)
}
type animatedTextureRenderFn func() image.Image

View File

@ -1,23 +1,20 @@
package main
import (
"embed"
"flag"
"image/color"
"log"
rice "github.com/GeertJohan/go.rice"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/addons/riceres"
"opslag.de/schobers/zntg/addons/embedres"
"opslag.de/schobers/zntg/ui"
)
// #cgo windows LDFLAGS: -Wl,-subsystem,windows
import "C"
//go:generate go get -u github.com/GeertJohan/go.rice/rice
//go:generate rice embed
//go:embed resources
var resources embed.FS
func main() {
err := run()
@ -27,20 +24,25 @@ func main() {
}
func openResources() ui.Resources {
box := rice.MustFindBox(`resources`)
embedded := riceres.New(box)
return ui.NewFallbackResources(ui.NewPathResources(nil, box.Name()), embedded)
embedded := embedres.New(resources)
return ui.NewFallbackResources(ui.NewPathResources(nil, `resources`), embedded)
}
func run() error {
var background string
var extract bool
flag.BoolVar(&extract, "extract-resources", false, "extracts all resources to the current working directory")
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()
return copyBoxToDisk(resources)
}
res := openResources()
if background != "" {
return GenerateBackground(res, background)
}
ptPtr := func(x, y int) *geom.Point {
p := geom.Pt(x, y)
@ -54,6 +56,11 @@ func run() error {
}
defer settings.Store()
score, err := tins2021.LoadScores()
if err != nil {
log.Printf("unable to load score; error: %v", err)
}
var location *geom.PointF32
if settings.Window.Location != nil {
location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)}
@ -78,7 +85,10 @@ func run() error {
app := &app{
settings: settings,
score: &score,
}
defer tins2021.SaveScores(app.score)
style := ui.DefaultStyle()
style.Palette = &ui.Palette{
Background: zntg.MustHexColor(`#494949`),

View File

@ -0,0 +1,4 @@
package main
// #cgo windows,!console LDFLAGS: -Wl,-subsystem,windows
import "C"

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
}

36
io.go
View File

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

View File

@ -92,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) {
@ -170,7 +180,7 @@ func (l *Level) Randomize(difficulty int, stars int) {
l.Tiles[pos].Star = true
stars--
}
hearts := 1 + (100-difficulty)/50 // [3..1] (only difficulty has 3 hearts)
hearts := rand.Intn(2 + (100-difficulty)/50) // [3..1] (only difficulty 0 has 3 hearts)
for hearts > 0 {
pos := pick(difficulty)
if l.Tiles[pos].Occupied() {
@ -179,9 +189,9 @@ func (l *Level) Randomize(difficulty int, stars int) {
l.Tiles[pos].Heart = true
hearts--
}
monsters := 2 + (8 * difficulty / 100)
minRandomMonster := (100-difficulty)*80/100 + 10 // [90..10]
minChaserMonster := (100-difficulty)*50/100 + 50 // [100..50]
monsters := 5 + (25 * difficulty / 100)
minRandomMonster := (100-difficulty)*60/100 + 10 // [70..10]
minChaserMonster := (100-difficulty)*70/100 + 25 // [95..25]
for monsters > 0 {
pos := pick(100 - difficulty)
curr := l.Monsters[pos]

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

265
meshanimation.go Normal file
View File

@ -0,0 +1,265 @@
package tins2021
import (
"fmt"
"image"
"os"
"runtime"
"sync"
"github.com/fogleman/fauxgl"
"github.com/nfnt/resize"
"golang.org/x/image/draw"
"opslag.de/schobers/geom"
"opslag.de/schobers/geom/ints"
"opslag.de/schobers/zntg/ui"
)
const (
fovy = 40 // vertical field of view in degrees
near = 1 // near clipping plane
far = 10 // far clipping plane
)
var (
eye = fauxgl.V(0, 0, 4) // camera position
center = fauxgl.V(0, 0, 0) // view center position
up = fauxgl.V(0, 1, 0) // up vector
light = fauxgl.V(.5, 1, .75).Normalize() // light direction
)
func Animate(hexColor string, frames int, animator MeshAnimator) image.Image {
const scale = 4
const s = 1.1
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(1, func() {
context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale)
color := fauxgl.HexColor(hexColor)
for i := range framesC {
context.ClearDepthBuffer()
context.ClearColorBufferWith(fauxgl.Transparent)
shader := fauxgl.NewPhongShader(matrix, light, eye)
shader.ObjectColor = color
shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`))
context.Shader = shader
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)
}
})
for f := 0; f < frames; f++ {
framesC <- f
}
close(framesC)
wait.Wait()
return animation
}
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, transformer MeshTransformer) image.Image {
path, err := resources.FetchResource(name)
if err != nil {
panic(err)
}
mesh, err := fauxgl.LoadSTL(path)
if err != nil {
panic(err)
}
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 {
Current int
TotalFrames int
}
func (s FrameState) Animation() float64 { return float64(s.Current) / float64(s.TotalFrames) }
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 {
return tri(vec(q, n*thickness), vec(r, n*thickness), vec(s, n*thickness))
}
var triangles []*fauxgl.Triangle
// generate front & back
for _, t := range polygon.Triangulate() {
triangles = append(triangles,
face(t.Points[0], t.Points[1], t.Points[2], 1), // front
face(t.Points[2], t.Points[1], t.Points[0], -1), // back
)
}
// generate side
back, front := -thickness, thickness
for i, p := range polygon.Points {
next := polygon.Points[(i+1)%len(polygon.Points)]
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
}
func iterate(n int, threads int) <-chan int {
iterator := make(chan int, threads)
go func() {
for i := 0; i < n; i++ {
iterator <- i
}
}()
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)
for i := 0; i < n; i++ {
go func() {
action()
wait.Done()
}()
}
return wait
}
func saveMeshSTL(path, name string, mesh *fauxgl.Mesh) error {
stl, err := os.Create(path)
if err != nil {
return err
}
defer stl.Close()
fmt.Fprintf(stl, "solid %s\n", name)
for _, triangle := range mesh.Triangles {
normal := triangle.Normal()
fmt.Fprintf(stl, " facet normal %f, %f, %f\n", normal.X, normal.Y, normal.Z)
fmt.Fprintf(stl, " outer loop\n")
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V1.Position.X, triangle.V1.Position.Y, triangle.V1.Position.Z)
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V2.Position.X, triangle.V2.Position.Y, triangle.V2.Position.Z)
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V3.Position.X, triangle.V3.Position.Y, triangle.V3.Position.Z)
fmt.Fprintf(stl, " endloop\n")
fmt.Fprintf(stl, " endfacet\n")
}
fmt.Fprintf(stl, "endsolid %s\n", name)
return nil
}
func SaveSTLFromPolygon(path, name string, polygon geom.PolygonF, thickness float64) {
mesh := generateMeshFromPolygon(polygon, thickness)
saveMeshSTL(path, name, mesh)
}

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 }

39
namedtexture.go Normal file
View File

@ -0,0 +1,39 @@
package tins2021
import (
"image"
"opslag.de/schobers/zntg/ui"
)
type NamedTexture struct {
textures *ui.Textures
name string
}
func NewNamedTexture(textures *ui.Textures, name string) NamedTexture {
return NamedTexture{
textures: textures,
name: name,
}
}
func CreateNamedTextureImage(textures *ui.Textures, name string, im image.Image) (NamedTexture, error) {
_, err := textures.CreateTextureGo(name, im, true)
if err != nil {
return NamedTexture{}, err
}
return NewNamedTexture(textures, name), nil
}
func MustCreateNamedTextureImage(textures *ui.Textures, name string, im image.Image) NamedTexture {
texture, err := CreateNamedTextureImage(textures, name, im)
if err != nil {
panic(err)
}
return texture
}
func (t NamedTexture) Scaled(scale float32) ui.Texture { return t.textures.ScaledByName(t.name, scale) }
func (t NamedTexture) Texture() ui.Texture { return t.textures.Texture(t.name) }

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

@ -1,211 +0,0 @@
package tins2021
import (
"fmt"
"image"
"os"
"github.com/fogleman/fauxgl"
"github.com/nfnt/resize"
"golang.org/x/image/draw"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
const (
fovy = 40 // vertical field of view in degrees
near = 1 // near clipping plane
far = 10 // far clipping plane
)
var (
eye = fauxgl.V(0, 0, 4) // camera position
center = fauxgl.V(0, 0, 0) // view center position
up = fauxgl.V(0, 1, 0) // up vector
light = fauxgl.V(.5, 1, .75).Normalize() // light direction
)
func AnimatePolygon(polygon geom.PolygonF, hexColor string, renderer AnimationRenderer) image.Image {
mesh := generateMeshFromPolygon(polygon, .2)
renderer.setup(mesh)
return renderMeshAnimation(hexColor, renderer.frames(), renderer.render)
}
func AnimateSTL(resources ui.PhysicalResources, name, hexColor string, renderer AnimationRenderer) image.Image {
path, err := resources.FetchResource(name)
if err != nil {
panic(err)
}
mesh, err := fauxgl.LoadSTL(path)
if err != nil {
panic(err)
}
renderer.setup(mesh)
return renderMeshAnimation(hexColor, renderer.frames(), renderer.render)
}
type animationRendererBase struct {
Frames int
Mesh *fauxgl.Mesh
}
func (r animationRendererBase) frames() int { return r.Frames }
func (r *animationRendererBase) setup(mesh *fauxgl.Mesh) {
r.Mesh = mesh
mesh.BiUnitCube()
}
var _ AnimationRenderer = &RotateAnimationRenderer{}
var _ AnimationRenderer = &WobbleAnimationRenderer{}
type AnimationRenderer interface {
frames() int
setup(*fauxgl.Mesh)
render(*fauxgl.Context, int, float64)
}
func generateMeshFromPolygon(polygon geom.PolygonF, thickness float64) *fauxgl.Mesh {
vec := func(p geom.PointF, z float64) fauxgl.Vector { return fauxgl.V(p.X, p.Y, z) }
tri := fauxgl.NewTriangleForPoints
face := func(q, r, s geom.PointF, n float64) *fauxgl.Triangle {
return tri(vec(q, n*thickness), vec(r, n*thickness), vec(s, n*thickness))
}
var triangles []*fauxgl.Triangle
// generate front & back
for _, t := range polygon.Triangulate() {
triangles = append(triangles,
face(t.Points[0], t.Points[1], t.Points[2], 1), // front
face(t.Points[2], t.Points[1], t.Points[0], -1), // back
)
}
// generate side
back, front := -thickness, thickness
for i, p := range polygon.Points {
next := polygon.Points[(i+1)%len(polygon.Points)]
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))
}
mesh := fauxgl.NewTriangleMesh(triangles)
return mesh
}
func renderMeshAnimation(hexColor string, frames int, render func(*fauxgl.Context, int, float64)) image.Image {
const scale = 4
context := fauxgl.NewContext(TextureSize*scale, TextureSize*scale)
// matrix := fauxgl.LookAt(eye, center, up).Perspective(fovy, 1, near, far)
const s = 1.1
// rot3 := func(m fauxgl.Matrix) fauxgl.Matrix {
// return fauxgl.Matrix{
// X00: m.X20, X01: m.X10, X02: m.X00, X03: m.X03,
// X10: m.X21, X11: m.X11, X12: m.X01, X13: m.X13,
// X20: m.X22, X21: m.X12, X22: m.X02, X23: m.X23,
// X30: m.X30, X31: m.X31, X32: m.X32, X33: m.X33,
// }
// }
// sqrt_6_1 := 1 / geom.Sqrt(6)
// iso := fauxgl.Matrix{
// X00: sqrt_6_1 * geom.Sqrt(3), X01: 0, X02: -sqrt_6_1 * geom.Sqrt(3), X03: 0,
// X10: sqrt_6_1, X11: 2 * sqrt_6_1, X12: sqrt_6_1, X13: 0,
// X20: sqrt_6_1 * geom.Sqrt(2), X21: -sqrt_6_1 * geom.Sqrt(2), X22: sqrt_6_1 * geom.Sqrt(2), X23: 0,
// X30: 0, X31: 0, X32: 0, X33: 1}
matrix := fauxgl.Orthographic(-s, s, -s, s, near, far).Mul(fauxgl.LookAt(eye, center, up))
color := fauxgl.HexColor(hexColor)
animation := image.NewNRGBA(image.Rect(0, 0, TextureSize*frames, TextureSize))
for i := 0; i < frames; i++ {
context.ClearDepthBuffer()
context.ClearColorBufferWith(fauxgl.Transparent)
shader := fauxgl.NewPhongShader(matrix, light, eye)
shader.ObjectColor = color
shader.AmbientColor = fauxgl.MakeColor(mustHexColor(`#7F7F7F`))
context.Shader = shader
render(context, i, float64(i)/float64(frames))
frame := resize.Resize(TextureSize, TextureSize, context.Image(), resize.Bilinear)
draw.Copy(animation, image.Pt(i*TextureSize, 0), frame, frame.Bounds(), draw.Src, nil)
}
return animation
}
type RotateAnimationRenderer struct {
animationRendererBase
Rotation float64
}
func NewRotateAnimation(frames int) AnimationRenderer {
return &RotateAnimationRenderer{
animationRendererBase: animationRendererBase{Frames: frames},
Rotation: 2 * geom.Pi / float64(frames),
}
}
func (a RotateAnimationRenderer) render(context *fauxgl.Context, _ int, _ float64) {
context.DrawMesh(a.Mesh)
a.Mesh.Transform(fauxgl.Rotate(up, a.Rotation))
}
func saveMeshSTL(path, name string, mesh *fauxgl.Mesh) error {
stl, err := os.Create(path)
if err != nil {
return err
}
defer stl.Close()
fmt.Fprintf(stl, "solid %s\n", name)
for _, triangle := range mesh.Triangles {
normal := triangle.Normal()
fmt.Fprintf(stl, " facet normal %f, %f, %f\n", normal.X, normal.Y, normal.Z)
fmt.Fprintf(stl, " outer loop\n")
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V1.Position.X, triangle.V1.Position.Y, triangle.V1.Position.Z)
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V2.Position.X, triangle.V2.Position.Y, triangle.V2.Position.Z)
fmt.Fprintf(stl, " vertex %f, %f, %f\n", triangle.V3.Position.X, triangle.V3.Position.Y, triangle.V3.Position.Z)
fmt.Fprintf(stl, " endloop\n")
fmt.Fprintf(stl, " endfacet\n")
}
fmt.Fprintf(stl, "endsolid %s\n", name)
return nil
}
func SaveSTLFromPolygon(path, name string, polygon geom.PolygonF, thickness float64) {
mesh := generateMeshFromPolygon(polygon, thickness)
saveMeshSTL(path, name, mesh)
}
type WobbleAnimationRenderer struct {
animationRendererBase
Wobble float64
}
func NewWobbleAnimation(frames int, wobble float64) AnimationRenderer {
return &WobbleAnimationRenderer{
animationRendererBase: animationRendererBase{Frames: frames},
Wobble: wobble,
}
}
func (a WobbleAnimationRenderer) animate(frame float64) float64 {
frame += .25
if frame >= 1 {
frame -= 1
}
// return geom.Cos(float64(frame) * 2 * geom.Pi / float64(a.Frames))
return geom.Abs(frame*4-2) - 1
}
func (a WobbleAnimationRenderer) render(context *fauxgl.Context, frame int, animation float64) {
context.DrawMesh(a.Mesh)
curr := a.animate(animation)
next := a.animate(float64(frame+1) / float64(a.Frames))
a.Mesh.Transform(fauxgl.Rotate(up, (next-curr)*a.Wobble*geom.Pi/180))
}

88
score.go Normal file
View File

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

88
score_test.go Normal file
View File

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

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

@ -15,7 +15,8 @@ rm -rf build/linux*
rm -rf build/macosx*
rm -rf build/windows*
mkdir -p build/release
qbitter_release_dir=release/qbitter_${version_safe}
mkdir -p build/${qbitter_release_dir}
go generate ../cmd/tins2021
@ -23,38 +24,38 @@ mkdir -p build/linux
go build -tags static -ldflags "-s -w" -o build/linux/qbitter ../cmd/tins2021
cp ../README.md build/linux
cd build/linux
zip -9 -q ../release/qbitter_${version_safe}_linux_amd64.zip *
echo "Created Linux release: build/release/qbitter_${version_safe}_linux_amd64.zip"
zip -9 -q ../${qbitter_release_dir}/qbitter_${version_safe}_linux_amd64.zip *
echo "Created Linux release: build/${qbitter_release_dir}/qbitter_${version_safe}_linux_amd64.zip"
cd ../..
mkdir -p build/linux-allegro
go build -tags static,allegro -ldflags "-s -w" -o build/linux-allegro/qbitter ../cmd/tins2021
cp ../README.md build/linux-allegro
cd build/linux-allegro
zip -9 -q ../release/qbitter_allegro_${version_safe}_linux_amd64.zip *
echo "Created Linux (Allegro) release: build/release/qbitter_allegro_${version_safe}_linux_amd64.zip"
zip -9 -q ../${qbitter_release_dir}/qbitter_allegro_${version_safe}_linux_amd64.zip *
echo "Created Linux (Allegro) release: build/${qbitter_release_dir}/qbitter_allegro_${version_safe}_linux_amd64.zip"
cd ../..
mkdir -p build/macosx
CGO_ENABLED=1 CC=o64-clang CXX=o64-clang++ GOOS=darwin GOARCH=amd64 go build -tags static -ldflags "-s -w" -o build/macosx/qbitter ../cmd/tins2021
cp ../README.md build/macosx
cd build/macosx
zip -9 -q ../release/qbitter_${version_safe}_macosx_amd64.zip *
echo "Created Mac OS X release: build/release/qbitter_${version_safe}_macosx_amd64.zip"
zip -9 -q ../${qbitter_release_dir}/qbitter_${version_safe}_macosx_amd64.zip *
echo "Created Mac OS X release: build/${qbitter_release_dir}/qbitter_${version_safe}_macosx_amd64.zip"
cd ../..
mkdir -p build/windows
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 go build -tags static -ldflags "-s -w" -o build/windows/qbitter.exe ../cmd/tins2021
cp ../README.md build/windows
cd build/windows
zip -9 -q ../release/qbitter_${version_safe}_windows_amd64.zip *
echo "Created Windows release: build/release/qbitter_${version_safe}_windows_amd64.zip"
zip -9 -q ../${qbitter_release_dir}/qbitter_${version_safe}_windows_amd64.zip *
echo "Created Windows release: build/${qbitter_release_dir}/qbitter_${version_safe}_windows_amd64.zip"
cd ../..
mkdir -p build/windows-allegro
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 go build -tags static,allegro,mingw64_7_3 -ldflags "-s -w" -o build/windows-allegro/qbitter.exe ../cmd/tins2021
cp ../README.md build/windows-allegro
cd build/windows-allegro
zip -9 -q ../release/qbitter_allegro_${version_safe}_windows_amd64.zip *
echo "Created Windows (Allegro) release: build/release/qbitter_allegro_${version_safe}_windows_amd64.zip"
zip -9 -q ../${qbitter_release_dir}/qbitter_allegro_${version_safe}_windows_amd64.zip *
echo "Created Windows (Allegro) release: build/${qbitter_release_dir}/qbitter_allegro_${version_safe}_windows_amd64.zip"
cd ../..

View File

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