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 .vscode/launch.json
scripts/build scripts/build
cmd/tins2021/rice-box.go

View File

@ -2,24 +2,25 @@
## Content ## Content
- [Introduction](##Introduction) - [Introduction](#introduction)
- [Additional Rules](##Additional-Rules) - [Additional Rules](#additional-rules)
* [Implementation](###Implementation) * [Implementation](#implementation)
* [Definition](###Definition) * [Definition](#definition)
- [Building](##Building) - [Building](#building)
- [Sources](##Sources) * [Using Allegro](#using-allegro)
- [Licenses (third party)](##Licenses) - [Sources](#sources)
* [Go-SDL2](###Go-SDL2) - [Licenses (third party)](#licenses)
* [SDL 2.0](###SDL-2.0) * [Go-SDL2](#go-sdl2)
* [Fira Mono font](###Fira-Mono-font) * [SDL 2.0](#sdl-2-0)
* [Escher font](###Escher-font) * [Allegro 5](#allegro-5)
* [Escheresk font](###Escheresk-font) * [Fira Mono font](#fira-mono-font)
* [fauxgl](###fauxgl) * [Escher font](#escher-font)
* [draw2d](###draw2d) * [Escheresk font](#escheresk-font)
* [go-colurful](###go-colurful) * [fauxgl](#fauxgl)
* [resize](###resize) * [draw2d](#draw2d)
* [testify](###testify) * [go-colurful](#go-colurful)
* [go.rice](###rice) * [resize](#resize)
* [testify](#testify)
## Introduction ## Introduction
@ -114,6 +115,8 @@ Prerequisites:
- GCC or a compatible compiler. - GCC or a compatible compiler.
- [Git](https://git-scm.com/download). - [Git](https://git-scm.com/download).
**Note:** the SDL 2.0 dependency can be replaced with [Allegro 5](#Using-Allegro) as well.
With all prequisites installed you can run: With all prequisites installed you can run:
``` ```
go get -u opslag.de/schobers/tins2021/cmd/tins2021 go get -u opslag.de/schobers/tins2021/cmd/tins2021
@ -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 go install -tags static -ldflags "-s -w" opslag.de/schobers/tins2021/cmd/tins2021
``` ```
### Using Allegro
If you want to use the Allegro backend you can add the build tag `allegro` to the `go install` command. E.g.: If you want to use the Allegro backend you can add the build tag `allegro` to the `go install` command. E.g.:
``` ```
go get -u opslag.de/schobers/allg5 go get -u opslag.de/schobers/allg5
go install -tags static,allegro -ldflags "-s -w" opslag.de/schobers/tins2021/cmd/tins2021 go install -tags static,allegro -ldflags "-s -w" opslag.de/schobers/tins2021/cmd/tins2021
``` ```
In this case the SDL 2.0 prerequisite is replaced by 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 ## 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 ## Sources
Can be found at https://opslag.de/schobers/tins2021 (Git repository). 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 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
### Allegro 5
Copyright © 2008-2010 the Allegro 5 Development Team
This software is provided as-is, without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
This notice may not be removed or altered from any source distribution.
### Fira Mono font ### Fira Mono font
Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A. Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.
@ -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, 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 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE. 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" "time"
"opslag.de/schobers/geom" "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 { type Animation struct {
LastUpdate time.Time LastUpdate time.Time
Frame int Frame int

View File

@ -1,6 +1,9 @@
package main package main
import ( import (
"log"
"opslag.de/schobers/geom"
"opslag.de/schobers/tins2021" "opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/play" "opslag.de/schobers/zntg/play"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
@ -10,6 +13,7 @@ type app struct {
ui.Proxy ui.Proxy
settings *tins2021.Settings settings *tins2021.Settings
score *tins2021.ScoreState
context *appContext context *appContext
} }
@ -35,18 +39,18 @@ const fpsOverlayName = `fps`
func (a *app) Init(ctx ui.Context) error { func (a *app) Init(ctx ui.Context) error {
if err := a.loadFonts(ctx, if err := a.loadFonts(ctx,
fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12}, fontDescriptor{"debug", "resources/fonts/FiraMono-Regular.ttf", 12},
fontDescriptor{"default", "fonts/escheresk.ttf", 48}, fontDescriptor{"default", "resources/fonts/escheresk.ttf", 48},
fontDescriptor{"small", "fonts/escheresk.ttf", 16}, fontDescriptor{"small", "resources/fonts/escheresk.ttf", 16},
fontDescriptor{"score", "fonts/FiraMono-Regular.ttf", 24}, fontDescriptor{"score", "resources/fonts/FiraMono-Regular.ttf", 24},
fontDescriptor{"title", "fonts/escher.ttf", 80}, fontDescriptor{"title", "resources/fonts/escher.ttf", 80},
); err != nil { ); err != nil {
return err return err
} }
textureLoader := tins2021.NewResourceLoader() textureLoader := tins2021.NewResourceLoader()
textures := ctx.Textures() 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) _, err := textures.CreateTexturePath(name, content, true)
return err return err
}); err != nil { }); err != nil {
@ -55,11 +59,42 @@ func (a *app) Init(ctx ui.Context) error {
ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{Align: ui.AlignRight}, false) ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{Align: ui.AlignRight}, false)
a.context = newAppContext(ctx, func(control ui.Control) { virtual := NewVirtualControls(nil)
a.Content = control 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) a.context.ShowMainMenu(ctx)
if err := a.context.Audio.LoadSample(
"level_completed.mp3",
"level_game_over.mp3",
"level_new_high_score.mp3",
"player_collect_heart.mp3",
"player_collect_star.mp3",
"player_hurt.mp3",
); err != nil {
log.Printf("failed to load samples; %v\n", err)
}
if err := a.context.Audio.LoadSampleVolume(-.5, "menu_interaction.mp3"); err != nil {
log.Printf("failed to load samples; %v\n", err)
}
if err := a.context.Audio.LoadSampleVolume(-1,
"monster_jump.mp3",
"player_move.mp3",
); err != nil {
log.Printf("failed to load samples; %v\n", err)
}
return nil return nil
} }

View File

@ -1,7 +1,8 @@
package main package main
import ( import (
"image" "fmt"
"math/rand"
"opslag.de/schobers/tins2021" "opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
@ -10,46 +11,68 @@ import (
type appContext struct { type appContext struct {
setView func(ui.Control) setView func(ui.Control)
Settings *tins2021.Settings
Score *tins2021.ScoreState
Virtual *VirtualControls
Debug bool Debug bool
StarTexture tins2021.AnimatedTexture
HeartTexture tins2021.AnimatedTexture
MonsterTextureNames map[tins2021.MonsterType]string 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 { func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.ScoreState, setView func(ui.Control)) *appContext {
newAnimatedTexture(ctx, "star", "images/star.png", func() image.Image { textures := textureGenerator{}
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))
})
app := &appContext{ app := &appContext{
setView: setView, setView: setView,
MonsterTextureNames: map[tins2021.MonsterType]string{ Settings: settings,
tins2021.MonsterTypeStraight: "straight-walking-monster", Audio: NewAudioPlayer(ctx.Resources(), "resources/sounds/"),
tins2021.MonsterTypeRandom: "random-walking-monster", Score: score,
tins2021.MonsterTypeChaser: "chasing-monster", 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)) app.Audio.SampleVolume = settings.Audio.SoundVolume
}) app.Audio.MusicVolume = settings.Audio.MusicVolume
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeRandom], "images/monster-random.png", func() image.Image {
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Blue, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30)) monsterNames := map[tins2021.MonsterType]string{
}) tins2021.MonsterTypeStraight: "straight-walking",
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeChaser], "images/monster-chaser.png", func() image.Image { tins2021.MonsterTypeRandom: "random-walking",
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Purple, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30)) tins2021.MonsterTypeChaser: "chasing",
}) }
for typ, name := range monsterNames {
textureName := fmt.Sprintf("%s-monster", name)
app.MonsterTextureNames[typ] = textureName
app.MonsterTextures[typ] = newAnimatedTexture(ctx, textureName, defaultAnimationFrames, textures.Monster(typ))
app.DyingMonsterTextures[typ] = newAnimatedTexture(ctx, fmt.Sprintf("%s-dying-monster", name), defaultAnimationFrames, textures.DyingMonster(typ))
}
return app return app
} }
const numberOfStars = 5 const numberOfStars = 5
func (app *appContext) MenuInteraction() {
app.Audio.PlaySample("menu_interaction.mp3")
}
func (app *appContext) Play(ctx ui.Context) { func (app *appContext) Play(ctx ui.Context) {
level := tins2021.NewLevel() level := tins2021.NewLevel()
level.Randomize(0, numberOfStars) level.Randomize(0, numberOfStars)
app.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) { 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.Score = score
level.Lives = lives level.Lives = lives
app.SetVirtualKeys(true, true, true)
controller.Play(level) 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) app.setView(control)
if _, ok := control.(*levelController); ok {
app.switchToPlayMusic(ctx)
} else {
app.switchToMenuMusic(ctx)
}
} }
func (app *appContext) ShowCredits(ctx ui.Context) { func (app *appContext) ShowCredits(ctx ui.Context) {
app.setView(newCredits(app, ctx)) app.show(ctx, newCredits(app, ctx))
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) { 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) { 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)", "zntg: an abstraction for rendering (UIs)",
" - https://opslag.de/schobers/zntg", "", " - https://opslag.de/schobers/zntg", "",
"allg5: an Allegro abstraction for Go", "allg5: an Allegro abstraction for Go",
" - https://opslag.de/schobers/allg5", " - https://opslag.de/schobers/allg5", "",
"ut: a utility library",
" - https://opslag.de/schobers/ut", "",
"", "",
"**... and also using**", "**... and also using**",
"Allegro: cross-platform development library", "Allegro: cross-platform development library",
" - https://liballeg.org", "", " - https://liballeg.org", "",
"SDL: another cross-platform development library", "SDL: another cross-platform development library",
" - https://libsdl.org", "", " - https://libsdl.org", "",
"go-sdl2: an SDL abstraction for Go", "go-sdl2: a SDL abstraction for Go",
" - https://github.com/veandco/go-sdl2", "", " - https://github.com/veandco/go-sdl2", "",
"Fira Mono: monotype font", "Fira Mono: monotype font",
"Escheresk: M.C. Escher themed font", "Escheresk: M.C. Escher themed font",
@ -58,8 +60,10 @@ func newCredits(app *appContext, ctx ui.Context) *credits {
" - https://github.com/nfnt/resize", "", " - https://github.com/nfnt/resize", "",
"testify: a testing library for Go", "testify: a testing library for Go",
" - https://github.com/stretchr/testify", "", " - https://github.com/stretchr/testify", "",
"rice: a library for embedding files in Go", "beep: a sound library",
" - https://github.com/GeertJohan/go.rice", "", " - https://github.com/faiface/beep", "",
"oto: a low level sound library",
" - https://https://github.com/hajimehoshi/oto", "",
"", "",
"# THANKS", "# THANKS",
"", "",
@ -81,7 +85,9 @@ func (c *credits) Handle(ctx ui.Context, e ui.Event) bool {
s := c.content[c.hovering] s := c.content[c.hovering]
if strings.HasPrefix(s, " - https://") { if strings.HasPrefix(s, " - https://") {
url := s[3:] url := s[3:]
c.openBrowser(url) if err := c.openBrowser(url); err != nil {
log.Println(err)
}
} }
} }
case *ui.KeyDownEvent: case *ui.KeyDownEvent:
@ -148,26 +154,23 @@ func (c *credits) fonts(ctx ui.Context) []ui.Font {
return fonts return fonts
} }
func (c *credits) openBrowser(url string) { func (c *credits) openBrowser(url string) error {
var err error
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
err = exec.Command("xdg-open", url).Start() return exec.Command("xdg-open", url).Start()
case "windows": case "windows":
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start() return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
case "darwin": case "darwin":
err = exec.Command("open", url).Start() return exec.Command("open", url).Start()
default: default:
err = fmt.Errorf("unsupported platform") return fmt.Errorf("unsupported platform")
}
if err != nil {
log.Println(err)
} }
} }
func (c *credits) Render(ctx ui.Context) { func (c *credits) Render(ctx ui.Context) {
renderer := ctx.Renderer() renderer := ctx.Renderer()
width := c.Bounds().Dx() bounds := c.Bounds()
width := bounds.Dx()
defaultColor := ctx.Style().Palette.Text defaultColor := ctx.Style().Palette.Text
c.enumerateContent(ctx, func(s string, i int, top, height float32, font ui.Font) { c.enumerateContent(ctx, func(s string, i int, top, height float32, font ui.Font) {
color := defaultColor color := defaultColor
@ -184,6 +187,6 @@ func (c *credits) Render(ctx ui.Context) {
} }
textWidth := font.WidthOf(s) 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 package main
import ( import (
"embed"
"io" "io"
"io/fs" "io/fs"
"os" "os"
"path/filepath" "path/filepath"
rice "github.com/GeertJohan/go.rice"
) )
func copyFile(path string, file fs.File) error { func copyFile(path string, file fs.File) error {
@ -21,19 +20,19 @@ func copyFile(path string, file fs.File) error {
return err return err
} }
func copyBoxToDisk() error { func copyBoxToDisk(resources embed.FS) error {
box := rice.MustFindBox(`resources`) return fs.WalkDir(resources, ".", func(path string, entry fs.DirEntry, err error) error {
return box.Walk("", func(path string, info os.FileInfo, err error) error {
if err != nil { if err != nil {
return err return err
} }
if info.IsDir() { if entry.IsDir() {
return nil return nil
} }
src, err := box.Open(path) src, err := resources.Open(path)
if err != nil { if err != nil {
return err 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" "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 { type info struct {
ui.StackPanel ui.StackPanel
app *appContext app *appContext
legend *ui.StackPanel
} }
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 { type infoLegend struct {
ui.ControlBase ui.ControlBase
@ -23,44 +27,43 @@ type infoLegend struct {
} }
func (l *infoLegend) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 { 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) { func (l *infoLegend) Render(ctx ui.Context) {
bounds := l.Bounds() bounds := l.Bounds()
separator := .3 * bounds.Dx()
textureHeight := float32(l.Icon.Height()) textureHeight := float32(l.Icon.Height())
renderer := ctx.Renderer() 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") font := ctx.Fonts().Font("score")
fontHeight := float32(font.Height()) 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 { func newInfo(app *appContext, ctx ui.Context) ui.Control {
monsterName := func(typ tins2021.MonsterType) string { return app.MonsterTextureNames[typ] }
legend := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) { legend := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("star", infoLegenIconSize), Icon: ctx.Textures().ScaledByName("star", infoLegendIconSize),
Description: "Collect them to complete the level.", Description: "Collect them to complete the level.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("heart", infoLegenIconSize), Icon: ctx.Textures().ScaledByName("heart", infoLegendIconSize),
Description: "Gives (back) a life.", Description: "Gives (back) a life.",
}) })
p.AddChild(&infoLegend{ 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.", Description: "Monster that walks over a fixed diagonal.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("random-walking-monster", infoLegenIconSize), Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeRandom), infoLegendIconSize),
Description: "Monster that walks randomly.", Description: "Monster that walks randomly.",
}) })
p.AddChild(&infoLegend{ p.AddChild(&infoLegend{
Icon: ctx.Textures().ScaledByName("chasing-monster", infoLegenIconSize), Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeChaser), infoLegendIconSize),
Description: "Monster that walks towards you.", Description: "Monster that walks towards you.",
}) })
}) })
@ -71,7 +74,7 @@ func newInfo(app *appContext, ctx ui.Context) ui.Control {
label("", "score"), // spacing label("", "score"), // spacing
paragraphOpts(infoText, "score", labelOptions{TextAlignment: ui.AlignCenter}), paragraphOpts(infoText, "score", labelOptions{TextAlignment: ui.AlignCenter}),
label("", "score"), // spacing label("", "score"), // spacing
legend, Center(legend),
}, },
}, },
Orientation: ui.OrientationVertical, Orientation: ui.OrientationVertical,

View File

@ -3,6 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"image/color" "image/color"
"log"
"math/rand" "math/rand"
"strconv" "strconv"
"time" "time"
@ -20,25 +21,71 @@ type levelController struct {
Level *tins2021.Level Level *tins2021.Level
Cubes cubeTexture Cube tins2021.NamedTexture
Inverted tins2021.NamedTexture
Animations map[string]*tins2021.Animations Animations map[string]*tins2021.Animations
IdleMonsters *tins2021.Animations IdleMonsters *tins2021.Animations
MovingMonsters *tins2021.Animations MovingMonsters *tins2021.Animations
DyingMonsters *tins2021.Animations
DyingMonsterTypes map[geom.Point]tins2021.MonsterType
SmallFont *tins2021.BitmapFont SmallFont *tins2021.BitmapFont
Controls map[ui.Key]tins2021.Direction
Highscore bool
} }
func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController { func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController {
control := &levelController{app: app} control := &levelController{app: app}
textures := ctx.Textures() 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...) small, err := tins2021.NewBitmapFont(ctx.Renderer(), ctx.Fonts().Font("small"), tins2021.NumericCharacters...)
if err != nil { if err != nil {
panic(err) panic(err)
} }
control.SmallFont = small 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) control.Play(level)
@ -49,11 +96,27 @@ func IsModifierPressed(modifiers ui.KeyModifier, pressed ui.KeyModifier) bool {
return modifiers&pressed == pressed return modifiers&pressed == pressed
} }
func (r *levelController) updateHighscore() bool {
highscores, highscore := r.app.Score.Highscores.AddScore(r.Level.Score, r.Level.Difficulty)
if highscore {
r.app.Score.Highscores = highscores
}
r.app.ResetCurrentScore()
return highscore
}
func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool { func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
switch e := e.(type) { switch e := e.(type) {
case *ui.KeyDownEvent: case *ui.KeyDownEvent:
switch e.Key { switch e.Key {
case ui.KeyEnter:
if r.Level.GameOver {
r.app.ShowHighscores(ctx)
}
case ui.KeyEscape: case ui.KeyEscape:
if r.Level.StarsCollected == r.Level.Stars {
r.app.SetCurrentScore(r.Level)
}
r.app.ShowMainMenu(ctx) r.app.ShowMainMenu(ctx)
} }
} }
@ -67,23 +130,59 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
case *ui.KeyDownEvent: case *ui.KeyDownEvent:
switch e.Key { switch e.Key {
case ui.KeyEnter: case ui.KeyEnter:
r.app.SetCurrentScore(r.Level)
r.app.PlayNext(ctx, r) r.app.PlayNext(ctx, r)
} }
} }
return false return false
} }
checkGameOver := func() {
if r.Level.GameOver {
r.app.SetVirtualKeys(true, true, false)
r.Highscore = r.updateHighscore()
if r.Highscore {
r.app.Audio.PlaySample("level_new_high_score.mp3")
} else {
r.app.Audio.PlaySample("level_game_over.mp3")
}
}
}
checkLevelCompleted := func() {
if r.Level.StarsCollected == r.Level.Stars {
r.app.SetVirtualKeys(true, true, false)
r.app.Audio.PlaySample("level_completed.mp3")
}
}
monsterHit := func(hit *tins2021.MonsterHit) {
r.app.Audio.PlaySample("player_hurt.mp3")
if hit == nil {
log.Printf("player was hit by monster but we don't know exactly where?\n")
}
r.DyingMonsters.Frame(hit.Position)
r.DyingMonsterTypes[hit.Position] = hit.Type
}
switch e := e.(type) { switch e := e.(type) {
case *ui.KeyDownEvent: case *ui.KeyDownEvent:
switch e.Key { dir, ok := r.Controls[e.Key]
case ui.KeyW: if ok {
r.Level.MovePlayer(tins2021.DirectionUpLeft) stars, lives := r.Level.StarsCollected, r.Level.Lives
case ui.KeyD: _, hit := r.Level.MovePlayer(dir)
r.Level.MovePlayer(tins2021.DirectionUpRight) switch {
case ui.KeyS: case r.Level.StarsCollected > stars:
r.Level.MovePlayer(tins2021.DirectionDownRight) r.app.Audio.PlaySample("player_collect_star.mp3")
case ui.KeyA: case r.Level.Lives < lives:
r.Level.MovePlayer(tins2021.DirectionDownLeft) 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.IdleMonsters.Update()
r.MovingMonsters.Update() r.MovingMonsters.Update()
r.DyingMonsters.Update()
var jumped []geom.Point var jumped []geom.Point
for pos, animation := range r.MovingMonsters.Values { for pos, animation := range r.MovingMonsters.Values {
if animation.Frame < 15 { if animation.Frame < 40 { // after 40 frames the player hit is checked
continue continue
} }
target := r.Level.MonsterTargets[pos] target := r.Level.MonsterTargets[pos]
if target == r.Level.Player { // player is hit if target == r.Level.Player { // player is hit
monsterHit(&tins2021.MonsterHit{Position: target, Type: r.Level.Monsters[pos].Type()})
r.Level.DestroyMonster(pos) r.Level.DestroyMonster(pos)
jumped = append(jumped, pos) jumped = append(jumped, pos)
r.Level.DecrementLive() r.Level.DecrementLive()
checkGameOver()
continue continue
} }
if animation.Frame < 20 { if animation.Frame < 50 { // after 50 frames the animation has finished
continue continue
} }
r.Level.MoveMonster(target, pos) r.Level.MoveMonster(target, pos)
@ -119,7 +221,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
var jumping []geom.Point var jumping []geom.Point
for pos, animation := range r.IdleMonsters.Values { for pos, animation := range r.IdleMonsters.Values {
for animation.Frame > 0 { for animation.Frame > 0 {
if rand.Intn(10) != 0 { if rand.Intn(100) < 37 { // P = .37
monster, ok := r.Level.Monsters[pos] monster, ok := r.Level.Monsters[pos]
if ok && monster != nil { if ok && monster != nil {
target, ok := monster.FindTarget(r.Level, pos) target, ok := monster.FindTarget(r.Level, pos)
@ -127,6 +229,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
r.Level.MonsterTargets[pos] = target r.Level.MonsterTargets[pos] = target
r.MovingMonsters.Frame(pos) r.MovingMonsters.Frame(pos)
jumping = append(jumping, pos) jumping = append(jumping, pos)
r.app.Audio.PlaySample("monster_jump.mp3")
break break
} }
} }
@ -148,15 +251,15 @@ func (r *levelController) Play(level *tins2021.Level) {
r.Animations = map[string]*tins2021.Animations{ r.Animations = map[string]*tins2021.Animations{
"star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true), "star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true),
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true), "heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
"monster": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
} }
r.IdleMonsters = tins2021.NewAnimations(500*time.Millisecond, 100, false, false) r.IdleMonsters = tins2021.NewAnimations(40*time.Millisecond, 100, false, false)
r.MovingMonsters = tins2021.NewAnimations(50*time.Millisecond, 20, false, false) r.MovingMonsters = tins2021.NewAnimations(16*time.Millisecond, 50, false, false)
r.DyingMonsters = tins2021.NewAnimations(16*time.Millisecond, 20, false, false)
r.DyingMonsterTypes = map[geom.Point]tins2021.MonsterType{}
for monster := range level.Monsters { for monster := range level.Monsters {
r.IdleMonsters.Frame(monster) r.IdleMonsters.Frame(monster)
} }
for _, monster := range r.app.MonsterTextureNames {
r.Animations[monster] = tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true)
}
} }
const defaultAnimationFrames = 20 const defaultAnimationFrames = 20
@ -167,7 +270,8 @@ func (r levelController) Render(ctx ui.Context) {
centerTopSquare := geom.PtF32(.5, .5*geom.Sin32(twelfth)) centerTopSquare := geom.PtF32(.5, .5*geom.Sin32(twelfth))
delta := geom.PtF32(geom.Cos32(twelfth), .5+centerTopSquare.Y) delta := geom.PtF32(geom.Cos32(twelfth), .5+centerTopSquare.Y)
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) levelView := geom.PtF32(float32(r.Level.Bounds.Dx()+2)*delta.X, float32(r.Level.Bounds.Dy()+2)*delta.Y)
textureWidth := geom.Min32( textureWidth := geom.Min32(
geom.Floor32(tins2021.TextureSize*view.X*.75/(levelView.X*tins2021.TextureSize)), geom.Floor32(tins2021.TextureSize*view.X*.75/(levelView.X*tins2021.TextureSize)),
@ -177,38 +281,43 @@ func (r levelController) Render(ctx ui.Context) {
delta = delta.Mul(textureWidth) delta = delta.Mul(textureWidth)
centerTopSquare = centerTopSquare.Mul(textureWidth) centerTopSquare = centerTopSquare.Mul(textureWidth)
scoreView := geom.RectF32(levelView.X*textureWidth, offsetY+delta.Y, view.X, view.Y-delta.Y-offsetY) 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.X = geom.Round32(delta.X)
delta.Y = geom.Round32(delta.Y) delta.Y = geom.Round32(delta.Y)
toScreen := func(p geom.Point) geom.PointF32 { toScreen := func(p geom.Point) geom.PointF32 {
if p.Y%2 == 0 { if p.Y%2 == 0 {
return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, offsetY) 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() renderer := ctx.Renderer()
textures := ctx.Textures() cube := r.Cube.Scaled(scale)
cubes := r.Cubes.Scaled(textures, scale) inverted := r.Inverted.Scaled(scale)
cubeWidth := float32(cubes.Normal.Width()) cubeWidth := float32(cube.Width())
cubeHeight := float32(cubes.Normal.Height()) cubeHeight := float32(cube.Height())
player := ctx.Textures().ScaledByName("dwarf", scale*.6) player := ctx.Textures().ScaledByName("dwarf", scale*.6)
star := tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName("star", scale*.4), defaultAnimationFrames) star := r.app.StarTexture.Scale(scale * .4)
heart := tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName("heart", scale*.4), defaultAnimationFrames) heart := r.app.HeartTexture.Scale(scale * .4)
monsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{} monsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{}
for typ, name := range r.app.MonsterTextureNames { for typ, animation := range r.app.MonsterTextures {
monsterTextures[typ] = tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName(name, scale*.4), defaultAnimationFrames) 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) 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) 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), .6*float32(cubeHeight))
} }
return topLeft, topLeft.Add2D(.5*float32(cubeWidth), .2*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 { if tile == nil {
continue continue
} }
screenPos, platformPos := positionOfTile(pos, tile) screenPos, platformPos := positionOfTile(pos)
tileTexture := cubes.Normal.Texture tileTexture := cube
if tile.Inversed { if tile.Inversed {
tileTexture = cubes.Inversed.Texture tileTexture = inverted
} }
renderer.DrawTexturePoint(tileTexture, screenPos) renderer.DrawTexturePoint(tileTexture, screenPos)
if r.app.Debug { if r.app.Debug {
@ -248,19 +357,51 @@ func (r levelController) Render(ctx ui.Context) {
renderer.DrawTexturePoint(player, playerPosition.Add(centerTopSquare)) renderer.DrawTexturePoint(player, playerPosition.Add(centerTopSquare))
} }
for pos, monsterType := range r.Level.Monsters { 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] tile := r.Level.Tiles[pos]
if tile == nil { if tile == nil {
continue continue
} }
_, platformPos := positionOfTile(pos, tile) texture := monsterTextures[monsterType.Type()]
name := r.app.MonsterTextureNames[monsterType.Type()] _, platformPos := positionOfTile(pos)
monsterTextures[monsterType.Type()].Draw(renderer, platformPos.Add(propOffset), r.Animations[name].Frame(pos)) if target, ok := r.Level.MonsterTargets[pos]; ok {
_, targetPlatformPos := positionOfTile(target)
dt := float32(r.MovingMonsters.Frame(pos)) / 50.
delta := targetPlatformPos.Sub(platformPos)
curve := geom.PtF32(0, .6*geom.Sin32(dt*geom.Pi)*textureWidth)
interpolatedPos := platformPos.Add(delta.Mul(dt)).Sub(curve)
texture.Draw(renderer, interpolatedPos.Add(propOffset), r.Animations["monster"].Frame(pos))
} else {
texture.Draw(renderer, platformPos.Add(propOffset), r.Animations["monster"].Frame(pos))
}
}
}
var died []geom.Point
for pos, monster := range r.DyingMonsterTypes {
frame := r.DyingMonsters.Frame(pos)
if frame == 20 {
died = append(died, pos)
continue
}
texture := dyingMonsterTextures[monster]
_, platformPos := positionOfTile(pos)
texture.Draw(renderer, platformPos.Add(propOffset), frame)
}
for _, pos := range died {
delete(r.DyingMonsters.Values, pos)
delete(r.DyingMonsterTypes, pos)
} }
textColor := ctx.Style().Palette.Text textColor := ctx.Style().Palette.Text
scoreFont := ctx.Fonts().Font("score") scoreFont := ctx.Fonts().Font("score")
fontOffsetY := .5 * (float32(star.Texture.Height()) - scoreFont.Height()) fontOffsetY := .5 * (float32(propHeight) - scoreFont.Height())
// stars & hearts // stars & hearts
scoreTopLeft := scoreView.Min scoreTopLeft := scoreView.Min
@ -282,24 +423,34 @@ func (r levelController) Render(ctx ui.Context) {
scoreTopLeft.Y -= scoreFont.Height() scoreTopLeft.Y -= scoreFont.Height()
renderer.Text(scoreFont, scoreTopLeft, textColor, "Score:") renderer.Text(scoreFont, scoreTopLeft, textColor, "Score:")
bounds := r.Bounds() centerX := bounds.Min.X + .5*bounds.Dx()
centerX := .5 * bounds.Dx()
titleFont := ctx.Fonts().Font("title") titleFont := ctx.Fonts().Font("title")
screenSize := ctx.Renderer().Size().ToF32()
screen := geom.RectF32(0, 0, screenSize.X, screenSize.Y)
if r.Level.GameOver { if r.Level.GameOver {
renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`)) renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`))
offsetY := .5*bounds.Dy() - titleFont.Height() offsetY := .5*bounds.Dy() - titleFont.Height()
renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "GAME OVER", ui.AlignCenter) renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "GAME OVER", ui.AlignCenter)
if r.Highscore {
highscoreFont := ctx.Fonts().Font("default")
offsetY += titleFont.Height() + scoreFont.Height()
renderer.TextAlign(highscoreFont, geom.PtF32(centerX, offsetY), ctx.Style().Palette.Primary, "NEW HIGHSCORE", ui.AlignCenter)
}
offsetY += titleFont.Height() + scoreFont.Height() offsetY += titleFont.Height() + scoreFont.Height()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Final score: %d", r.Level.Score), ui.AlignCenter) renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Final score: %d", r.Level.Score), ui.AlignCenter)
offsetY += 2 * scoreFont.Height()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [enter] to show highscores.", ui.AlignCenter)
offsetY += 2 * scoreFont.Height() offsetY += 2 * scoreFont.Height()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [escape] to quit.", ui.AlignCenter) renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [escape] to quit.", ui.AlignCenter)
} else if r.Level.StarsCollected == r.Level.Stars { } else if r.Level.StarsCollected == r.Level.Stars {
renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`)) renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`))
offsetY := .5*bounds.Dy() - titleFont.Height() 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() offsetY += titleFont.Height() + scoreFont.Height()
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Score: %d", r.Level.Score), ui.AlignCenter) renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Score: %d", r.Level.Score), ui.AlignCenter)

View File

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

View File

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

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 package main
import ( import (
"fmt"
"image" "image"
"image/png" "image/png"
"log"
"os" "os"
"path/filepath"
"time"
"opslag.de/schobers/tins2021" "opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
) )
type namedTexture struct { func chrono(action func()) time.Duration {
ui.Texture start := time.Now()
action()
Name string return time.Now().Sub(start)
} }
func newNamedTexture(textures *ui.Textures, name string, im image.Image) namedTexture { func chronoErr(action func() error) (time.Duration, error) {
texture, err := textures.CreateTextureGo(name, im, true) start := time.Now()
if err != nil { err := action()
panic(err) return time.Now().Sub(start), 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 loadTextureImage(ctx ui.Context, resource string) image.Image { func loadTextureImage(ctx ui.Context, resource string) image.Image {
@ -58,26 +38,29 @@ func loadTextureImage(ctx ui.Context, resource string) image.Image {
return im 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) out, err := os.Create(resource)
if err != nil { if err != nil {
return return err
} }
defer out.Close() 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 package main
import ( import (
"embed"
"flag" "flag"
"image/color" "image/color"
"log" "log"
rice "github.com/GeertJohan/go.rice"
"opslag.de/schobers/geom" "opslag.de/schobers/geom"
"opslag.de/schobers/tins2021" "opslag.de/schobers/tins2021"
"opslag.de/schobers/zntg" "opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/addons/riceres" "opslag.de/schobers/zntg/addons/embedres"
"opslag.de/schobers/zntg/ui" "opslag.de/schobers/zntg/ui"
) )
// #cgo windows LDFLAGS: -Wl,-subsystem,windows //go:embed resources
import "C" var resources embed.FS
//go:generate go get -u github.com/GeertJohan/go.rice/rice
//go:generate rice embed
func main() { func main() {
err := run() err := run()
@ -27,20 +24,25 @@ func main() {
} }
func openResources() ui.Resources { func openResources() ui.Resources {
box := rice.MustFindBox(`resources`) embedded := embedres.New(resources)
embedded := riceres.New(box) return ui.NewFallbackResources(ui.NewPathResources(nil, `resources`), embedded)
return ui.NewFallbackResources(ui.NewPathResources(nil, box.Name()), embedded)
} }
func run() error { func run() error {
var background string
var extract bool 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() flag.Parse()
if extract { if extract {
return copyBoxToDisk() return copyBoxToDisk(resources)
} }
res := openResources() res := openResources()
if background != "" {
return GenerateBackground(res, background)
}
ptPtr := func(x, y int) *geom.Point { ptPtr := func(x, y int) *geom.Point {
p := geom.Pt(x, y) p := geom.Pt(x, y)
@ -54,6 +56,11 @@ func run() error {
} }
defer settings.Store() defer settings.Store()
score, err := tins2021.LoadScores()
if err != nil {
log.Printf("unable to load score; error: %v", err)
}
var location *geom.PointF32 var location *geom.PointF32
if settings.Window.Location != nil { if settings.Window.Location != nil {
location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)} location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)}
@ -78,7 +85,10 @@ func run() error {
app := &app{ app := &app{
settings: settings, settings: settings,
score: &score,
} }
defer tins2021.SaveScores(app.score)
style := ui.DefaultStyle() style := ui.DefaultStyle()
style.Palette = &ui.Palette{ style.Palette = &ui.Palette{
Background: zntg.MustHexColor(`#494949`), Background: zntg.MustHexColor(`#494949`),

View File

@ -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 package tins2021
import ( import (
"encoding/json"
"os"
"opslag.de/schobers/zntg" "opslag.de/schobers/zntg"
) )
const appName = "tins2021_qbitter" const appName = "tins2021_qbitter"
func UserDir() (string, error) { return zntg.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) 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) towards, allowed := l.CanPlayerMove(dir)
if !allowed { if !allowed {
return false return false, nil
} }
l.Player = towards l.Player = towards
tile := l.Tiles[towards] tile := l.Tiles[towards]
@ -109,13 +114,18 @@ func (l *Level) MovePlayer(dir Direction) bool {
tile.Star = false tile.Star = false
l.Score += 25 l.Score += 25
} }
var hit *MonsterHit
if l.Monsters[towards] != nil { if l.Monsters[towards] != nil {
hit = &MonsterHit{
Position: towards,
Type: l.Monsters[towards].Type(),
}
l.DecrementLive() l.DecrementLive()
l.DestroyMonster(towards) l.DestroyMonster(towards)
l.Score -= 5 l.Score -= 5
} }
l.Score -= 1 // for every move l.Score -= 1 // for every move
return true return true, hit
} }
func (l *Level) Randomize(difficulty int, stars int) { 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 l.Tiles[pos].Star = true
stars-- 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 { for hearts > 0 {
pos := pick(difficulty) pos := pick(difficulty)
if l.Tiles[pos].Occupied() { if l.Tiles[pos].Occupied() {
@ -179,9 +189,9 @@ func (l *Level) Randomize(difficulty int, stars int) {
l.Tiles[pos].Heart = true l.Tiles[pos].Heart = true
hearts-- hearts--
} }
monsters := 2 + (8 * difficulty / 100) monsters := 5 + (25 * difficulty / 100)
minRandomMonster := (100-difficulty)*80/100 + 10 // [90..10] minRandomMonster := (100-difficulty)*60/100 + 10 // [70..10]
minChaserMonster := (100-difficulty)*50/100 + 50 // [100..50] minChaserMonster := (100-difficulty)*70/100 + 25 // [95..25]
for monsters > 0 { for monsters > 0 {
pos := pick(100 - difficulty) pos := pick(100 - difficulty)
curr := l.Monsters[pos] 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" "opslag.de/schobers/geom"
) )
var AllMonsterTypes = []MonsterType{MonsterTypeStraight, MonsterTypeRandom, MonsterTypeChaser}
type ChasingMonster struct{} type ChasingMonster struct{}
func (m ChasingMonster) Type() MonsterType { return MonsterTypeChaser } func (m ChasingMonster) Type() MonsterType { return MonsterTypeChaser }

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 var polygon geom.PolygonF
const segments = 100 const segments = 100
for segment := 0; segment < 100; segment++ { for segment := 0; segment < 100; segment++ {
t := 2 * geom.Pi * float64(segment) / segments t := 2 * geom.Pi * Percentage(segment, segments)
st := geom.Sin(t) st := geom.Sin(t)
polygon.Points = append(polygon.Points, geom.PtF( polygon.Points = append(polygon.Points, geom.PtF(
16*st*st*st, 16*st*st*st,
@ -23,15 +23,15 @@ func CreateHeart() geom.PolygonF {
} }
func CreateHexagon() geom.PolygonF { func CreateHexagon() geom.PolygonF {
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 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, polygon.Points = append(polygon.Points,
pt(float64(side)/float64(sides)), PolarUnity(float64(side)/float64(sides)),
) )
} }
return polygon return polygon
@ -39,14 +39,10 @@ func CreateHexagon() geom.PolygonF {
func CreateStar(sides int) geom.PolygonF { func CreateStar(sides int) geom.PolygonF {
var polygon geom.PolygonF var polygon geom.PolygonF
pt := func(rotation float64) geom.PointF {
a := .5*geom.Pi + 2*geom.Pi*rotation
return geom.PtF(geom.Cos(a), geom.Sin(a))
}
for side := 0; side < sides; side++ { for side := 0; side < sides; side++ {
polygon.Points = append(polygon.Points, polygon.Points = append(polygon.Points,
pt(float64(side)/float64(sides)), PolarUnity(float64(side)/float64(sides)),
pt((float64(side)+0.5)/float64(sides)).Mul(.5), PolarUnity((float64(side)+0.5)/float64(sides)).Mul(.5),
) )
} }
return polygon 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/macosx*
rm -rf build/windows* 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 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 go build -tags static -ldflags "-s -w" -o build/linux/qbitter ../cmd/tins2021
cp ../README.md build/linux cp ../README.md build/linux
cd build/linux cd build/linux
zip -9 -q ../release/qbitter_${version_safe}_linux_amd64.zip * zip -9 -q ../${qbitter_release_dir}/qbitter_${version_safe}_linux_amd64.zip *
echo "Created Linux release: build/release/qbitter_${version_safe}_linux_amd64.zip" echo "Created Linux release: build/${qbitter_release_dir}/qbitter_${version_safe}_linux_amd64.zip"
cd ../.. cd ../..
mkdir -p build/linux-allegro mkdir -p build/linux-allegro
go build -tags static,allegro -ldflags "-s -w" -o build/linux-allegro/qbitter ../cmd/tins2021 go build -tags static,allegro -ldflags "-s -w" -o build/linux-allegro/qbitter ../cmd/tins2021
cp ../README.md build/linux-allegro cp ../README.md build/linux-allegro
cd build/linux-allegro cd build/linux-allegro
zip -9 -q ../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/release/qbitter_allegro_${version_safe}_linux_amd64.zip" echo "Created Linux (Allegro) release: build/${qbitter_release_dir}/qbitter_allegro_${version_safe}_linux_amd64.zip"
cd ../.. cd ../..
mkdir -p build/macosx 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 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 cp ../README.md build/macosx
cd build/macosx cd build/macosx
zip -9 -q ../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/release/qbitter_${version_safe}_macosx_amd64.zip" echo "Created Mac OS X release: build/${qbitter_release_dir}/qbitter_${version_safe}_macosx_amd64.zip"
cd ../.. cd ../..
mkdir -p build/windows 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 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 cp ../README.md build/windows
cd build/windows cd build/windows
zip -9 -q ../release/qbitter_${version_safe}_windows_amd64.zip * zip -9 -q ../${qbitter_release_dir}/qbitter_${version_safe}_windows_amd64.zip *
echo "Created Windows release: build/release/qbitter_${version_safe}_windows_amd64.zip" echo "Created Windows release: build/${qbitter_release_dir}/qbitter_${version_safe}_windows_amd64.zip"
cd ../.. cd ../..
mkdir -p build/windows-allegro 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 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 cp ../README.md build/windows-allegro
cd build/windows-allegro cd build/windows-allegro
zip -9 -q ../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/release/qbitter_allegro_${version_safe}_windows_amd64.zip" echo "Created Windows (Allegro) release: build/${qbitter_release_dir}/qbitter_allegro_${version_safe}_windows_amd64.zip"
cd ../.. cd ../..

View File

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