Compare commits
30 Commits
qbitter_1_
...
master
Author | SHA1 | Date | |
---|---|---|---|
1a393c0065 | |||
19adf64c54 | |||
aca3a5af78 | |||
5935a7fea8 | |||
65db973699 | |||
99e87cef6e | |||
88e6fc4181 | |||
24db632470 | |||
17008871ce | |||
aab65a984a | |||
3198659d11 | |||
4adfdbe006 | |||
0d49482036 | |||
3c99e5881b | |||
e3527eb580 | |||
cbd08cdc12 | |||
c47f9383c3 | |||
99d9d09c2f | |||
4f1760ad57 | |||
c628ae4b09 | |||
c52f2682e0 | |||
000348339d | |||
165d1fcd26 | |||
f9da21b61c | |||
7053e7b9f2 | |||
64ff5ac78e | |||
90b79c7e49 | |||
6ce3d84417 | |||
7541fa0085 | |||
44f6217bc7 |
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
.vscode/launch.json
|
||||
|
||||
scripts/build
|
||||
cmd/tins2021/rice-box.go
|
||||
|
84
README.md
@ -2,24 +2,25 @@
|
||||
|
||||
## Content
|
||||
|
||||
- [Introduction](##Introduction)
|
||||
- [Additional Rules](##Additional-Rules)
|
||||
* [Implementation](###Implementation)
|
||||
* [Definition](###Definition)
|
||||
- [Building](##Building)
|
||||
- [Sources](##Sources)
|
||||
- [Licenses (third party)](##Licenses)
|
||||
* [Go-SDL2](###Go-SDL2)
|
||||
* [SDL 2.0](###SDL-2.0)
|
||||
* [Fira Mono font](###Fira-Mono-font)
|
||||
* [Escher font](###Escher-font)
|
||||
* [Escheresk font](###Escheresk-font)
|
||||
* [fauxgl](###fauxgl)
|
||||
* [draw2d](###draw2d)
|
||||
* [go-colurful](###go-colurful)
|
||||
* [resize](###resize)
|
||||
* [testify](###testify)
|
||||
* [go.rice](###rice)
|
||||
- [Introduction](#introduction)
|
||||
- [Additional Rules](#additional-rules)
|
||||
* [Implementation](#implementation)
|
||||
* [Definition](#definition)
|
||||
- [Building](#building)
|
||||
* [Using Allegro](#using-allegro)
|
||||
- [Sources](#sources)
|
||||
- [Licenses (third party)](#licenses)
|
||||
* [Go-SDL2](#go-sdl2)
|
||||
* [SDL 2.0](#sdl-2-0)
|
||||
* [Allegro 5](#allegro-5)
|
||||
* [Fira Mono font](#fira-mono-font)
|
||||
* [Escher font](#escher-font)
|
||||
* [Escheresk font](#escheresk-font)
|
||||
* [fauxgl](#fauxgl)
|
||||
* [draw2d](#draw2d)
|
||||
* [go-colurful](#go-colurful)
|
||||
* [resize](#resize)
|
||||
* [testify](#testify)
|
||||
|
||||
## Introduction
|
||||
|
||||
@ -114,6 +115,8 @@ Prerequisites:
|
||||
- GCC or a compatible compiler.
|
||||
- [Git](https://git-scm.com/download).
|
||||
|
||||
**Note:** the SDL 2.0 dependency can be replaced with [Allegro 5](#Using-Allegro) as well.
|
||||
|
||||
With all prequisites installed you can run:
|
||||
```
|
||||
go get -u opslag.de/schobers/tins2021/cmd/tins2021
|
||||
@ -125,15 +128,17 @@ go generate opslag.de/schobers/tins2021/cmd/tins2021
|
||||
go install -tags static -ldflags "-s -w" opslag.de/schobers/tins2021/cmd/tins2021
|
||||
```
|
||||
|
||||
### Using Allegro
|
||||
|
||||
If you want to use the Allegro backend you can add the build tag `allegro` to the `go install` command. E.g.:
|
||||
```
|
||||
go get -u opslag.de/schobers/allg5
|
||||
go install -tags static,allegro -ldflags "-s -w" opslag.de/schobers/tins2021/cmd/tins2021
|
||||
```
|
||||
In this case the SDL 2.0 prerequisite is replaced by the Allegro 5.2 (development libraries must be available for your C compiler).
|
||||
In this case the SDL 2.0 prerequisite is replaced by Allegro 5.2 (development libraries must be available for your C compiler).
|
||||
|
||||
## Command line interface
|
||||
You can extract all resources embedded in the executable by running it from the command line with the `--extract-resources` flag. The resources will be extract in the current working directory. Note that the game will use the resources on disk first if they are available.
|
||||
You can extract all resources embedded in the executable by running it from the command line with the `--extract` flag. The resources will be extract in the current working directory. Note that the game will use the resources on disk first if they are available.
|
||||
|
||||
## Sources
|
||||
Can be found at https://opslag.de/schobers/tins2021 (Git repository).
|
||||
@ -186,6 +191,20 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
### Allegro 5
|
||||
|
||||
Copyright © 2008-2010 the Allegro 5 Development Team
|
||||
|
||||
This software is provided ‘as-is’, without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software.
|
||||
|
||||
Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions:
|
||||
|
||||
The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required.
|
||||
|
||||
Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software.
|
||||
|
||||
This notice may not be removed or altered from any source distribution.
|
||||
|
||||
### Fira Mono font
|
||||
|
||||
Copyright (c) 2012-2013, The Mozilla Corporation and Telefonica S.A.
|
||||
@ -388,28 +407,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
### rice
|
||||
|
||||
Copyright (c) 2013, Geert-Johan Riemer
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
1. Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
16
TODO.md
Normal file
@ -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
@ -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
|
||||
}
|
43
animation.go
@ -5,51 +5,8 @@ import (
|
||||
"time"
|
||||
|
||||
"opslag.de/schobers/geom"
|
||||
"opslag.de/schobers/zntg/ui"
|
||||
)
|
||||
|
||||
type AnimatedTexture struct {
|
||||
Texture ui.Texture
|
||||
Frames []geom.RectangleF32
|
||||
}
|
||||
|
||||
func NewAnimatedTexture(texture ui.Texture, n int) AnimatedTexture {
|
||||
frames := make([]geom.RectangleF32, 0, n)
|
||||
height := float32(texture.Height())
|
||||
width := float32(texture.Width())
|
||||
for i := 0; i < n; i++ {
|
||||
left := width * float32(i) / float32(n)
|
||||
right := width * float32(i+1) / float32(n)
|
||||
frames = append(frames, geom.RectF32(left, 0, right, height))
|
||||
}
|
||||
return AnimatedTexture{Texture: texture, Frames: frames}
|
||||
}
|
||||
|
||||
func NewSquareAnimatedTexture(texture ui.Texture) AnimatedTexture {
|
||||
var frames []geom.RectangleF32
|
||||
height := float32(texture.Height())
|
||||
width := float32(texture.Width())
|
||||
for left := float32(0); left < width; left += height {
|
||||
frames = append(frames, geom.RectF32(left, 0, left+height, height))
|
||||
}
|
||||
return AnimatedTexture{Texture: texture, Frames: frames}
|
||||
}
|
||||
|
||||
func (t AnimatedTexture) Scale(scale float32) AnimatedTexture {
|
||||
frames := make([]geom.RectangleF32, 0, len(t.Frames))
|
||||
for _, frame := range t.Frames {
|
||||
frames = append(frames, geom.RectangleF32{Min: frame.Min.Mul(scale), Max: frame.Max.Mul(scale)})
|
||||
}
|
||||
return AnimatedTexture{
|
||||
Texture: t.Texture,
|
||||
Frames: frames,
|
||||
}
|
||||
}
|
||||
|
||||
func (t AnimatedTexture) Draw(renderer ui.Renderer, pos geom.PointF32, frame int) {
|
||||
renderer.DrawTexturePointOptions(t.Texture, pos, ui.DrawOptions{Source: &t.Frames[frame]})
|
||||
}
|
||||
|
||||
type Animation struct {
|
||||
LastUpdate time.Time
|
||||
Frame int
|
||||
|
@ -1,6 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"opslag.de/schobers/geom"
|
||||
"opslag.de/schobers/tins2021"
|
||||
"opslag.de/schobers/zntg/play"
|
||||
"opslag.de/schobers/zntg/ui"
|
||||
@ -10,6 +13,7 @@ type app struct {
|
||||
ui.Proxy
|
||||
|
||||
settings *tins2021.Settings
|
||||
score *tins2021.ScoreState
|
||||
|
||||
context *appContext
|
||||
}
|
||||
@ -35,18 +39,18 @@ const fpsOverlayName = `fps`
|
||||
|
||||
func (a *app) Init(ctx ui.Context) error {
|
||||
if err := a.loadFonts(ctx,
|
||||
fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12},
|
||||
fontDescriptor{"default", "fonts/escheresk.ttf", 48},
|
||||
fontDescriptor{"small", "fonts/escheresk.ttf", 16},
|
||||
fontDescriptor{"score", "fonts/FiraMono-Regular.ttf", 24},
|
||||
fontDescriptor{"title", "fonts/escher.ttf", 80},
|
||||
fontDescriptor{"debug", "resources/fonts/FiraMono-Regular.ttf", 12},
|
||||
fontDescriptor{"default", "resources/fonts/escheresk.ttf", 48},
|
||||
fontDescriptor{"small", "resources/fonts/escheresk.ttf", 16},
|
||||
fontDescriptor{"score", "resources/fonts/FiraMono-Regular.ttf", 24},
|
||||
fontDescriptor{"title", "resources/fonts/escher.ttf", 80},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
textureLoader := tins2021.NewResourceLoader()
|
||||
textures := ctx.Textures()
|
||||
if err := textureLoader.LoadFromFile(ctx.Resources(), "textures.txt", func(name, content string) error {
|
||||
if err := textureLoader.LoadFromFile(ctx.Resources(), "resources/textures.txt", func(name, content string) error {
|
||||
_, err := textures.CreateTexturePath(name, content, true)
|
||||
return err
|
||||
}); err != nil {
|
||||
@ -55,11 +59,42 @@ func (a *app) Init(ctx ui.Context) error {
|
||||
|
||||
ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{Align: ui.AlignRight}, false)
|
||||
|
||||
a.context = newAppContext(ctx, func(control ui.Control) {
|
||||
a.Content = control
|
||||
virtual := NewVirtualControls(nil)
|
||||
virtual.RegisterKey("confirm", ui.KeyEnter, tins2021.MustCreateNamedTextureImage(textures, "return-key", drawReturnKey()))
|
||||
virtual.RegisterKey("arrow-top-left", ui.KeyUp, tins2021.MustCreateNamedTextureImage(textures, "arrow-top-left-key", drawArrowKey(.75*geom.Pi)))
|
||||
virtual.RegisterKey("arrow-top-right", ui.KeyRight, tins2021.MustCreateNamedTextureImage(textures, "arrow-top-right-key", drawArrowKey(.25*geom.Pi)))
|
||||
virtual.RegisterKey("arrow-bottom-left", ui.KeyLeft, tins2021.MustCreateNamedTextureImage(textures, "arrow-bottom-left-key", drawArrowKey(1.25*geom.Pi)))
|
||||
virtual.RegisterKey("arrow-bottom-right", ui.KeyDown, tins2021.MustCreateNamedTextureImage(textures, "arrow-bottom-right-key", drawArrowKey(1.75*geom.Pi)))
|
||||
virtual.RegisterKey("cancel", ui.KeyEscape, tins2021.MustCreateNamedTextureImage(textures, "arrow-left-key", drawArrowKey(geom.Pi)))
|
||||
|
||||
virtual.Enabled = a.settings.Controls.Virtual
|
||||
a.Content = virtual
|
||||
a.context = newAppContext(ctx, a.settings, a.score, func(view ui.Control) {
|
||||
virtual.Content = view
|
||||
})
|
||||
a.context.Virtual = virtual
|
||||
a.context.ShowMainMenu(ctx)
|
||||
|
||||
if err := a.context.Audio.LoadSample(
|
||||
"level_completed.mp3",
|
||||
"level_game_over.mp3",
|
||||
"level_new_high_score.mp3",
|
||||
"player_collect_heart.mp3",
|
||||
"player_collect_star.mp3",
|
||||
"player_hurt.mp3",
|
||||
); err != nil {
|
||||
log.Printf("failed to load samples; %v\n", err)
|
||||
}
|
||||
if err := a.context.Audio.LoadSampleVolume(-.5, "menu_interaction.mp3"); err != nil {
|
||||
log.Printf("failed to load samples; %v\n", err)
|
||||
}
|
||||
if err := a.context.Audio.LoadSampleVolume(-1,
|
||||
"monster_jump.mp3",
|
||||
"player_move.mp3",
|
||||
); err != nil {
|
||||
log.Printf("failed to load samples; %v\n", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"image"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"opslag.de/schobers/tins2021"
|
||||
"opslag.de/schobers/zntg/ui"
|
||||
@ -10,46 +11,68 @@ import (
|
||||
type appContext struct {
|
||||
setView func(ui.Control)
|
||||
|
||||
Debug bool
|
||||
Settings *tins2021.Settings
|
||||
Score *tins2021.ScoreState
|
||||
Virtual *VirtualControls
|
||||
Debug bool
|
||||
|
||||
MonsterTextureNames map[tins2021.MonsterType]string
|
||||
StarTexture tins2021.AnimatedTexture
|
||||
HeartTexture tins2021.AnimatedTexture
|
||||
MonsterTextureNames map[tins2021.MonsterType]string
|
||||
MonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
|
||||
DyingMonsterTextures map[tins2021.MonsterType]tins2021.AnimatedTexture
|
||||
|
||||
Audio *AudioPlayer
|
||||
MenuMusic *Music
|
||||
GameMusic *Music
|
||||
GameMusicSong int
|
||||
}
|
||||
|
||||
func newAppContext(ctx ui.Context, setView func(ui.Control)) *appContext {
|
||||
newAnimatedTexture(ctx, "star", "images/star.png", func() image.Image {
|
||||
return tins2021.AnimatePolygon(tins2021.CreateStar(5), tins2021.Yellow, tins2021.NewRotateAnimation(defaultAnimationFrames))
|
||||
})
|
||||
newAnimatedTexture(ctx, "heart", "images/heart.png", func() image.Image {
|
||||
return tins2021.AnimatePolygon(tins2021.CreateHeart(), tins2021.Red, tins2021.NewRotateAnimation(defaultAnimationFrames))
|
||||
})
|
||||
|
||||
func newAppContext(ctx ui.Context, settings *tins2021.Settings, score *tins2021.ScoreState, setView func(ui.Control)) *appContext {
|
||||
textures := textureGenerator{}
|
||||
app := &appContext{
|
||||
setView: setView,
|
||||
MonsterTextureNames: map[tins2021.MonsterType]string{
|
||||
tins2021.MonsterTypeStraight: "straight-walking-monster",
|
||||
tins2021.MonsterTypeRandom: "random-walking-monster",
|
||||
tins2021.MonsterTypeChaser: "chasing-monster",
|
||||
},
|
||||
setView: setView,
|
||||
Settings: settings,
|
||||
Audio: NewAudioPlayer(ctx.Resources(), "resources/sounds/"),
|
||||
Score: score,
|
||||
StarTexture: newAnimatedTexture(ctx, "star", defaultAnimationFrames, textures.Star),
|
||||
HeartTexture: newAnimatedTexture(ctx, "heart", defaultAnimationFrames, textures.Heart),
|
||||
MonsterTextureNames: map[tins2021.MonsterType]string{},
|
||||
MonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{},
|
||||
DyingMonsterTextures: map[tins2021.MonsterType]tins2021.AnimatedTexture{},
|
||||
}
|
||||
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeStraight], "images/monster-straight.png", func() image.Image {
|
||||
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Green, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
|
||||
})
|
||||
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeRandom], "images/monster-random.png", func() image.Image {
|
||||
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Blue, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
|
||||
})
|
||||
newAnimatedTexture(ctx, app.MonsterTextureNames[tins2021.MonsterTypeChaser], "images/monster-chaser.png", func() image.Image {
|
||||
return tins2021.AnimatePolygon(tins2021.CreateHexagon(), tins2021.Purple, tins2021.NewWobbleAnimation(defaultAnimationFrames, 30))
|
||||
})
|
||||
|
||||
app.Audio.SampleVolume = settings.Audio.SoundVolume
|
||||
app.Audio.MusicVolume = settings.Audio.MusicVolume
|
||||
|
||||
monsterNames := map[tins2021.MonsterType]string{
|
||||
tins2021.MonsterTypeStraight: "straight-walking",
|
||||
tins2021.MonsterTypeRandom: "random-walking",
|
||||
tins2021.MonsterTypeChaser: "chasing",
|
||||
}
|
||||
for typ, name := range monsterNames {
|
||||
textureName := fmt.Sprintf("%s-monster", name)
|
||||
app.MonsterTextureNames[typ] = textureName
|
||||
app.MonsterTextures[typ] = newAnimatedTexture(ctx, textureName, defaultAnimationFrames, textures.Monster(typ))
|
||||
app.DyingMonsterTextures[typ] = newAnimatedTexture(ctx, fmt.Sprintf("%s-dying-monster", name), defaultAnimationFrames, textures.DyingMonster(typ))
|
||||
}
|
||||
|
||||
return app
|
||||
}
|
||||
|
||||
const numberOfStars = 5
|
||||
|
||||
func (app *appContext) MenuInteraction() {
|
||||
app.Audio.PlaySample("menu_interaction.mp3")
|
||||
}
|
||||
|
||||
func (app *appContext) Play(ctx ui.Context) {
|
||||
level := tins2021.NewLevel()
|
||||
level.Randomize(0, numberOfStars)
|
||||
|
||||
app.show(newLevelControl(app, ctx, level))
|
||||
app.ResetCurrentScore()
|
||||
app.show(ctx, newLevelControl(app, ctx, level))
|
||||
app.SetVirtualKeys(true, true, true)
|
||||
}
|
||||
|
||||
func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
|
||||
@ -62,21 +85,141 @@ func (app *appContext) PlayNext(ctx ui.Context, controller *levelController) {
|
||||
level.Score = score
|
||||
level.Lives = lives
|
||||
|
||||
app.SetVirtualKeys(true, true, true)
|
||||
controller.Play(level)
|
||||
}
|
||||
|
||||
func (app *appContext) show(control ui.Control) {
|
||||
func (app *appContext) playNextGameMusic(ctx ui.Context) {
|
||||
if app.GameMusic != nil {
|
||||
return
|
||||
}
|
||||
const songs = 4
|
||||
pick := func() int {
|
||||
for {
|
||||
s := rand.Intn(songs) + 1
|
||||
if s == app.GameMusicSong {
|
||||
continue
|
||||
}
|
||||
app.GameMusicSong = s
|
||||
return s
|
||||
}
|
||||
}
|
||||
song := fmt.Sprintf("song_game_%d.mp3", pick())
|
||||
app.GameMusic, _ = app.Audio.PlayMusic(song, func(m *Music) {
|
||||
m.OnFinished = func() {
|
||||
if app.GameMusic == nil {
|
||||
return
|
||||
}
|
||||
app.GameMusic = nil
|
||||
app.playNextGameMusic(ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func (app *appContext) PlayResume(ctx ui.Context) {
|
||||
level := tins2021.NewLevel()
|
||||
level.Score = app.Score.Current.Score
|
||||
level.Lives = app.Score.CurrentLives
|
||||
level.Randomize(app.Score.Current.Difficulty, numberOfStars)
|
||||
|
||||
app.show(ctx, newLevelControl(app, ctx, level))
|
||||
app.SetVirtualKeys(true, true, true)
|
||||
}
|
||||
|
||||
func (app *appContext) ResetCurrentScore() {
|
||||
app.Score.Current = tins2021.Score{}
|
||||
app.Score.CurrentLives = 0
|
||||
}
|
||||
|
||||
func (app *appContext) SetCurrentScore(level *tins2021.Level) {
|
||||
app.Score.Current = tins2021.NewScore(level.Score, level.Difficulty+1)
|
||||
app.Score.CurrentLives = level.Lives
|
||||
}
|
||||
|
||||
func (app *appContext) show(ctx ui.Context, control ui.Control) {
|
||||
app.setView(control)
|
||||
if _, ok := control.(*levelController); ok {
|
||||
app.switchToPlayMusic(ctx)
|
||||
} else {
|
||||
app.switchToMenuMusic(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) ShowCredits(ctx ui.Context) {
|
||||
app.setView(newCredits(app, ctx))
|
||||
app.show(ctx, newCredits(app, ctx))
|
||||
app.SetVirtualKeys(true, true, false)
|
||||
}
|
||||
|
||||
func (app *appContext) ShowSettings(ctx ui.Context) {
|
||||
app.show(ctx, newSettings(app, ctx))
|
||||
app.SetVirtualKeys(true, true, false)
|
||||
}
|
||||
|
||||
func (app *appContext) ShowHighscores(ctx ui.Context) {
|
||||
app.show(ctx, newHighscores(app, ctx))
|
||||
app.SetVirtualKeys(true, true, false)
|
||||
}
|
||||
|
||||
func (app *appContext) ShowInfo(ctx ui.Context) {
|
||||
app.setView(newInfo(app, ctx))
|
||||
app.show(ctx, newInfo(app, ctx))
|
||||
app.SetVirtualKeys(true, true, false)
|
||||
}
|
||||
|
||||
func (app *appContext) ShowMainMenu(ctx ui.Context) {
|
||||
app.show(newMainMenu(app, ctx))
|
||||
app.show(ctx, newMainMenu(app, ctx))
|
||||
app.SetVirtualKeys(false, false, false)
|
||||
}
|
||||
|
||||
func (app *appContext) setMusicVolume(volume float64) {
|
||||
app.Settings.Audio.MusicVolume = volume
|
||||
app.Audio.MusicVolume = volume
|
||||
menu := app.MenuMusic
|
||||
if menu != nil {
|
||||
menu.Volume.Volume = volume
|
||||
}
|
||||
game := app.GameMusic
|
||||
if game != nil {
|
||||
game.Volume.Volume = volume
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) setSoundVolume(volume float64) {
|
||||
app.Settings.Audio.SoundVolume = volume
|
||||
app.Audio.SampleVolume = volume
|
||||
}
|
||||
|
||||
func (app *appContext) SetVirtualKeys(cancel, confirm, controls bool) {
|
||||
var topLeft, topRight, bottomRight, bottomLeft []string
|
||||
if cancel {
|
||||
topLeft = append(topLeft, "cancel")
|
||||
}
|
||||
if confirm {
|
||||
topRight = append(topRight, "confirm")
|
||||
}
|
||||
if controls {
|
||||
bottomRight = append(bottomRight, "arrow-bottom-right", "arrow-top-right")
|
||||
bottomLeft = append(bottomLeft, "arrow-bottom-left", "arrow-top-left")
|
||||
}
|
||||
app.Virtual.SetControls(topLeft, topRight, bottomRight, bottomLeft)
|
||||
}
|
||||
|
||||
func (app *appContext) switchToPlayMusic(ctx ui.Context) {
|
||||
app.playNextGameMusic(ctx)
|
||||
menuMusic := app.MenuMusic
|
||||
app.MenuMusic = nil
|
||||
if menuMusic != nil {
|
||||
menuMusic.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
func (app *appContext) switchToMenuMusic(ctx ui.Context) {
|
||||
if app.MenuMusic != nil {
|
||||
return
|
||||
}
|
||||
app.MenuMusic, _ = app.Audio.PlayMusic("song_menu.mp3", func(m *Music) { m.AutoRepeat = true })
|
||||
gameMusic := app.GameMusic
|
||||
app.GameMusic = nil
|
||||
if gameMusic != nil {
|
||||
gameMusic.Stop()
|
||||
}
|
||||
}
|
||||
|
198
cmd/tins2021/audio.go
Normal file
@ -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)
|
||||
}
|
64
cmd/tins2021/background.go
Normal 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)
|
||||
}
|
@ -36,14 +36,16 @@ func newCredits(app *appContext, ctx ui.Context) *credits {
|
||||
"zntg: an abstraction for rendering (UIs)",
|
||||
" - https://opslag.de/schobers/zntg", "",
|
||||
"allg5: an Allegro abstraction for Go",
|
||||
" - https://opslag.de/schobers/allg5",
|
||||
" - https://opslag.de/schobers/allg5", "",
|
||||
"ut: a utility library",
|
||||
" - https://opslag.de/schobers/ut", "",
|
||||
"",
|
||||
"**... and also using**",
|
||||
"Allegro: cross-platform development library",
|
||||
" - https://liballeg.org", "",
|
||||
"SDL: another cross-platform development library",
|
||||
" - https://libsdl.org", "",
|
||||
"go-sdl2: an SDL abstraction for Go",
|
||||
"go-sdl2: a SDL abstraction for Go",
|
||||
" - https://github.com/veandco/go-sdl2", "",
|
||||
"Fira Mono: monotype font",
|
||||
"Escheresk: M.C. Escher themed font",
|
||||
@ -58,8 +60,10 @@ func newCredits(app *appContext, ctx ui.Context) *credits {
|
||||
" - https://github.com/nfnt/resize", "",
|
||||
"testify: a testing library for Go",
|
||||
" - https://github.com/stretchr/testify", "",
|
||||
"rice: a library for embedding files in Go",
|
||||
" - https://github.com/GeertJohan/go.rice", "",
|
||||
"beep: a sound library",
|
||||
" - https://github.com/faiface/beep", "",
|
||||
"oto: a low level sound library",
|
||||
" - https://https://github.com/hajimehoshi/oto", "",
|
||||
"",
|
||||
"# THANKS",
|
||||
"",
|
||||
@ -81,7 +85,9 @@ func (c *credits) Handle(ctx ui.Context, e ui.Event) bool {
|
||||
s := c.content[c.hovering]
|
||||
if strings.HasPrefix(s, " - https://") {
|
||||
url := s[3:]
|
||||
c.openBrowser(url)
|
||||
if err := c.openBrowser(url); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
case *ui.KeyDownEvent:
|
||||
@ -148,26 +154,23 @@ func (c *credits) fonts(ctx ui.Context) []ui.Font {
|
||||
return fonts
|
||||
}
|
||||
|
||||
func (c *credits) openBrowser(url string) {
|
||||
var err error
|
||||
func (c *credits) openBrowser(url string) error {
|
||||
switch runtime.GOOS {
|
||||
case "linux":
|
||||
err = exec.Command("xdg-open", url).Start()
|
||||
return exec.Command("xdg-open", url).Start()
|
||||
case "windows":
|
||||
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
return exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
||||
case "darwin":
|
||||
err = exec.Command("open", url).Start()
|
||||
return exec.Command("open", url).Start()
|
||||
default:
|
||||
err = fmt.Errorf("unsupported platform")
|
||||
}
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return fmt.Errorf("unsupported platform")
|
||||
}
|
||||
}
|
||||
|
||||
func (c *credits) Render(ctx ui.Context) {
|
||||
renderer := ctx.Renderer()
|
||||
width := c.Bounds().Dx()
|
||||
bounds := c.Bounds()
|
||||
width := bounds.Dx()
|
||||
defaultColor := ctx.Style().Palette.Text
|
||||
c.enumerateContent(ctx, func(s string, i int, top, height float32, font ui.Font) {
|
||||
color := defaultColor
|
||||
@ -184,6 +187,6 @@ func (c *credits) Render(ctx ui.Context) {
|
||||
}
|
||||
|
||||
textWidth := font.WidthOf(s)
|
||||
renderer.Text(font, geom.PtF32(.5*(width-textWidth), top), color, s)
|
||||
renderer.Text(font, geom.PtF32(bounds.Min.X+.5*(width-textWidth), top), color, s)
|
||||
})
|
||||
}
|
||||
|
47
cmd/tins2021/draw2dfont.go
Normal 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
|
||||
}
|
@ -1,12 +1,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
rice "github.com/GeertJohan/go.rice"
|
||||
)
|
||||
|
||||
func copyFile(path string, file fs.File) error {
|
||||
@ -21,19 +20,19 @@ func copyFile(path string, file fs.File) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func copyBoxToDisk() error {
|
||||
box := rice.MustFindBox(`resources`)
|
||||
return box.Walk("", func(path string, info os.FileInfo, err error) error {
|
||||
func copyBoxToDisk(resources embed.FS) error {
|
||||
return fs.WalkDir(resources, ".", func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
if entry.IsDir() {
|
||||
return nil
|
||||
}
|
||||
src, err := box.Open(path)
|
||||
src, err := resources.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return copyFile(filepath.Join(box.Name(), path), src)
|
||||
defer src.Close()
|
||||
return copyFile(path, src)
|
||||
})
|
||||
}
|
||||
|
52
cmd/tins2021/highscores.go
Normal 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)
|
||||
}
|
@ -6,15 +6,19 @@ import (
|
||||
"opslag.de/schobers/zntg/ui"
|
||||
)
|
||||
|
||||
const infoLegendIconSize = .36
|
||||
const infoLegendIconMargin = 4.
|
||||
const infoLegendSeparatorMargin = 24
|
||||
const infoText = "Qbitter is a game loosly based on the work \"LW305 Kringloop\" of M.C. Escher where a gnome runs down a stairs and morphs into its 2D abstract shape. The game also lends ideas from Q*Bert, a game from the eighties that is based on the works of M.C. Escher. \n\nIn the game you (represented as a gnome) have to collect stars while trying to avoid enemies (hexagons). Every level has increasing difficulty."
|
||||
|
||||
var infoLegendIconRectangle = geom.RectRelF32(0, 0, infoLegendIconSize*tins2021.TextureSize, infoLegendIconSize*tins2021.TextureSize)
|
||||
|
||||
type info struct {
|
||||
ui.StackPanel
|
||||
|
||||
app *appContext
|
||||
legend *ui.StackPanel
|
||||
app *appContext
|
||||
}
|
||||
|
||||
const infoText = "Qbitter is a game loosly based on a work \"LW305 Kringloop\" of M.C. Escher where a gnome runs down a stairs and morphs into its 2D abstract shape. The game also lends ideas from Q*Bert, a game from the eighties that was itself based on the works of M.C. Escher. In the game you (represented as a gnome) have to collect stars while trying to avoid enemies (hexagons). Every level has increasing difficulty."
|
||||
|
||||
type infoLegend struct {
|
||||
ui.ControlBase
|
||||
|
||||
@ -23,44 +27,43 @@ type infoLegend struct {
|
||||
}
|
||||
|
||||
func (l *infoLegend) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 {
|
||||
return geom.PtF32(geom.NaN32(), .4*tins2021.TextureSize)
|
||||
textureHeight := float32(l.Icon.Height())
|
||||
font := ctx.Fonts().Font("score")
|
||||
fontHeight := float32(font.Height())
|
||||
return geom.PtF32(textureHeight+infoLegendSeparatorMargin+font.WidthOf(l.Description), geom.Max32(textureHeight+2*infoLegendIconMargin, fontHeight))
|
||||
}
|
||||
|
||||
const infoLegenIconSize = .36
|
||||
|
||||
var infoLegendIconRectangle = geom.RectRelF32(0, 0, infoLegenIconSize*tins2021.TextureSize, infoLegenIconSize*tins2021.TextureSize)
|
||||
|
||||
func (l *infoLegend) Render(ctx ui.Context) {
|
||||
bounds := l.Bounds()
|
||||
separator := .3 * bounds.Dx()
|
||||
textureHeight := float32(l.Icon.Height())
|
||||
renderer := ctx.Renderer()
|
||||
renderer.DrawTexturePointOptions(l.Icon, bounds.Min.Add2D(separator-textureHeight, 0), ui.DrawOptions{Source: &infoLegendIconRectangle})
|
||||
renderer.DrawTexturePointOptions(l.Icon, bounds.Min, ui.DrawOptions{Source: &infoLegendIconRectangle})
|
||||
font := ctx.Fonts().Font("score")
|
||||
fontHeight := float32(font.Height())
|
||||
renderer.Text(font, bounds.Min.Add2D(separator+12, .5*(textureHeight-fontHeight)), ctx.Style().Palette.Text, l.Description)
|
||||
renderer.Text(font, bounds.Min.Add2D(textureHeight+infoLegendSeparatorMargin, .5*(textureHeight-fontHeight)+infoLegendIconMargin), ctx.Style().Palette.Text, l.Description)
|
||||
}
|
||||
|
||||
func newInfo(app *appContext, ctx ui.Context) ui.Control {
|
||||
monsterName := func(typ tins2021.MonsterType) string { return app.MonsterTextureNames[typ] }
|
||||
legend := ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
|
||||
p.AddChild(&infoLegend{
|
||||
Icon: ctx.Textures().ScaledByName("star", infoLegenIconSize),
|
||||
Icon: ctx.Textures().ScaledByName("star", infoLegendIconSize),
|
||||
Description: "Collect them to complete the level.",
|
||||
})
|
||||
p.AddChild(&infoLegend{
|
||||
Icon: ctx.Textures().ScaledByName("heart", infoLegenIconSize),
|
||||
Icon: ctx.Textures().ScaledByName("heart", infoLegendIconSize),
|
||||
Description: "Gives (back) a life.",
|
||||
})
|
||||
p.AddChild(&infoLegend{
|
||||
Icon: ctx.Textures().ScaledByName("straight-walking-monster", infoLegenIconSize),
|
||||
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeStraight), infoLegendIconSize),
|
||||
Description: "Monster that walks over a fixed diagonal.",
|
||||
})
|
||||
p.AddChild(&infoLegend{
|
||||
Icon: ctx.Textures().ScaledByName("random-walking-monster", infoLegenIconSize),
|
||||
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeRandom), infoLegendIconSize),
|
||||
Description: "Monster that walks randomly.",
|
||||
})
|
||||
p.AddChild(&infoLegend{
|
||||
Icon: ctx.Textures().ScaledByName("chasing-monster", infoLegenIconSize),
|
||||
Icon: ctx.Textures().ScaledByName(monsterName(tins2021.MonsterTypeChaser), infoLegendIconSize),
|
||||
Description: "Monster that walks towards you.",
|
||||
})
|
||||
})
|
||||
@ -71,7 +74,7 @@ func newInfo(app *appContext, ctx ui.Context) ui.Control {
|
||||
label("", "score"), // spacing
|
||||
paragraphOpts(infoText, "score", labelOptions{TextAlignment: ui.AlignCenter}),
|
||||
label("", "score"), // spacing
|
||||
legend,
|
||||
Center(legend),
|
||||
},
|
||||
},
|
||||
Orientation: ui.OrientationVertical,
|
||||
|
@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"image/color"
|
||||
"log"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
"time"
|
||||
@ -20,25 +21,71 @@ type levelController struct {
|
||||
|
||||
Level *tins2021.Level
|
||||
|
||||
Cubes cubeTexture
|
||||
Cube tins2021.NamedTexture
|
||||
Inverted tins2021.NamedTexture
|
||||
Animations map[string]*tins2021.Animations
|
||||
|
||||
IdleMonsters *tins2021.Animations
|
||||
MovingMonsters *tins2021.Animations
|
||||
IdleMonsters *tins2021.Animations
|
||||
MovingMonsters *tins2021.Animations
|
||||
DyingMonsters *tins2021.Animations
|
||||
DyingMonsterTypes map[geom.Point]tins2021.MonsterType
|
||||
|
||||
SmallFont *tins2021.BitmapFont
|
||||
|
||||
Controls map[ui.Key]tins2021.Direction
|
||||
|
||||
Highscore bool
|
||||
}
|
||||
|
||||
func newLevelControl(app *appContext, ctx ui.Context, level *tins2021.Level) *levelController {
|
||||
control := &levelController{app: app}
|
||||
textures := ctx.Textures()
|
||||
control.Cubes = newCubeTexture(textures, tins2021.Orange)
|
||||
control.Cube = tins2021.MustCreateNamedTextureImage(textures, "cube", tins2021.GenerateCube(tins2021.Orange))
|
||||
control.Inverted = tins2021.MustCreateNamedTextureImage(textures, "cube_inverted", tins2021.GenerateHole(tins2021.Orange))
|
||||
|
||||
small, err := tins2021.NewBitmapFont(ctx.Renderer(), ctx.Fonts().Font("small"), tins2021.NumericCharacters...)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
control.SmallFont = small
|
||||
switch app.Settings.Controls.Type {
|
||||
case controlsTypeArrows:
|
||||
control.Controls = map[ui.Key]tins2021.Direction{
|
||||
ui.KeyUp: tins2021.DirectionUpLeft,
|
||||
ui.KeyLeft: tins2021.DirectionDownLeft,
|
||||
ui.KeyDown: tins2021.DirectionDownRight,
|
||||
ui.KeyRight: tins2021.DirectionUpRight,
|
||||
}
|
||||
case controlsTypeCustom:
|
||||
find := func(s string, defaultKey ui.Key) ui.Key {
|
||||
for key, setting := range supportedCustomKeys {
|
||||
if s == setting {
|
||||
return key
|
||||
}
|
||||
}
|
||||
return defaultKey
|
||||
}
|
||||
control.Controls = map[ui.Key]tins2021.Direction{
|
||||
find(app.Settings.Controls.MoveUpLeft, ui.KeyW): tins2021.DirectionUpLeft,
|
||||
find(app.Settings.Controls.MoveDownLeft, ui.KeyA): tins2021.DirectionDownLeft,
|
||||
find(app.Settings.Controls.MoveDownRight, ui.KeyS): tins2021.DirectionDownRight,
|
||||
find(app.Settings.Controls.MoveUpRight, ui.KeyD): tins2021.DirectionUpRight,
|
||||
}
|
||||
default:
|
||||
control.Controls = map[ui.Key]tins2021.Direction{
|
||||
ui.KeyW: tins2021.DirectionUpLeft,
|
||||
ui.KeyA: tins2021.DirectionDownLeft,
|
||||
ui.KeyS: tins2021.DirectionDownRight,
|
||||
ui.KeyD: tins2021.DirectionUpRight,
|
||||
}
|
||||
}
|
||||
|
||||
if app.Virtual.Enabled {
|
||||
control.Controls[ui.KeyUp] = tins2021.DirectionUpLeft
|
||||
control.Controls[ui.KeyLeft] = tins2021.DirectionDownLeft
|
||||
control.Controls[ui.KeyDown] = tins2021.DirectionDownRight
|
||||
control.Controls[ui.KeyRight] = tins2021.DirectionUpRight
|
||||
}
|
||||
|
||||
control.Play(level)
|
||||
|
||||
@ -49,11 +96,27 @@ func IsModifierPressed(modifiers ui.KeyModifier, pressed ui.KeyModifier) bool {
|
||||
return modifiers&pressed == pressed
|
||||
}
|
||||
|
||||
func (r *levelController) updateHighscore() bool {
|
||||
highscores, highscore := r.app.Score.Highscores.AddScore(r.Level.Score, r.Level.Difficulty)
|
||||
if highscore {
|
||||
r.app.Score.Highscores = highscores
|
||||
}
|
||||
r.app.ResetCurrentScore()
|
||||
return highscore
|
||||
}
|
||||
|
||||
func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
||||
switch e := e.(type) {
|
||||
case *ui.KeyDownEvent:
|
||||
switch e.Key {
|
||||
case ui.KeyEnter:
|
||||
if r.Level.GameOver {
|
||||
r.app.ShowHighscores(ctx)
|
||||
}
|
||||
case ui.KeyEscape:
|
||||
if r.Level.StarsCollected == r.Level.Stars {
|
||||
r.app.SetCurrentScore(r.Level)
|
||||
}
|
||||
r.app.ShowMainMenu(ctx)
|
||||
}
|
||||
}
|
||||
@ -67,23 +130,59 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
||||
case *ui.KeyDownEvent:
|
||||
switch e.Key {
|
||||
case ui.KeyEnter:
|
||||
r.app.SetCurrentScore(r.Level)
|
||||
r.app.PlayNext(ctx, r)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
checkGameOver := func() {
|
||||
if r.Level.GameOver {
|
||||
r.app.SetVirtualKeys(true, true, false)
|
||||
|
||||
r.Highscore = r.updateHighscore()
|
||||
if r.Highscore {
|
||||
r.app.Audio.PlaySample("level_new_high_score.mp3")
|
||||
} else {
|
||||
r.app.Audio.PlaySample("level_game_over.mp3")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkLevelCompleted := func() {
|
||||
if r.Level.StarsCollected == r.Level.Stars {
|
||||
r.app.SetVirtualKeys(true, true, false)
|
||||
r.app.Audio.PlaySample("level_completed.mp3")
|
||||
}
|
||||
}
|
||||
|
||||
monsterHit := func(hit *tins2021.MonsterHit) {
|
||||
r.app.Audio.PlaySample("player_hurt.mp3")
|
||||
if hit == nil {
|
||||
log.Printf("player was hit by monster but we don't know exactly where?\n")
|
||||
}
|
||||
r.DyingMonsters.Frame(hit.Position)
|
||||
r.DyingMonsterTypes[hit.Position] = hit.Type
|
||||
}
|
||||
|
||||
switch e := e.(type) {
|
||||
case *ui.KeyDownEvent:
|
||||
switch e.Key {
|
||||
case ui.KeyW:
|
||||
r.Level.MovePlayer(tins2021.DirectionUpLeft)
|
||||
case ui.KeyD:
|
||||
r.Level.MovePlayer(tins2021.DirectionUpRight)
|
||||
case ui.KeyS:
|
||||
r.Level.MovePlayer(tins2021.DirectionDownRight)
|
||||
case ui.KeyA:
|
||||
r.Level.MovePlayer(tins2021.DirectionDownLeft)
|
||||
dir, ok := r.Controls[e.Key]
|
||||
if ok {
|
||||
stars, lives := r.Level.StarsCollected, r.Level.Lives
|
||||
_, hit := r.Level.MovePlayer(dir)
|
||||
switch {
|
||||
case r.Level.StarsCollected > stars:
|
||||
r.app.Audio.PlaySample("player_collect_star.mp3")
|
||||
case r.Level.Lives < lives:
|
||||
monsterHit(hit)
|
||||
case r.Level.Lives > lives:
|
||||
r.app.Audio.PlaySample("player_collect_heart.mp3")
|
||||
}
|
||||
r.app.Audio.PlaySample("player_move.mp3")
|
||||
checkGameOver()
|
||||
checkLevelCompleted()
|
||||
}
|
||||
}
|
||||
|
||||
@ -92,19 +191,22 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
||||
}
|
||||
r.IdleMonsters.Update()
|
||||
r.MovingMonsters.Update()
|
||||
r.DyingMonsters.Update()
|
||||
var jumped []geom.Point
|
||||
for pos, animation := range r.MovingMonsters.Values {
|
||||
if animation.Frame < 15 {
|
||||
if animation.Frame < 40 { // after 40 frames the player hit is checked
|
||||
continue
|
||||
}
|
||||
target := r.Level.MonsterTargets[pos]
|
||||
if target == r.Level.Player { // player is hit
|
||||
monsterHit(&tins2021.MonsterHit{Position: target, Type: r.Level.Monsters[pos].Type()})
|
||||
r.Level.DestroyMonster(pos)
|
||||
jumped = append(jumped, pos)
|
||||
r.Level.DecrementLive()
|
||||
checkGameOver()
|
||||
continue
|
||||
}
|
||||
if animation.Frame < 20 {
|
||||
if animation.Frame < 50 { // after 50 frames the animation has finished
|
||||
continue
|
||||
}
|
||||
r.Level.MoveMonster(target, pos)
|
||||
@ -119,7 +221,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
||||
var jumping []geom.Point
|
||||
for pos, animation := range r.IdleMonsters.Values {
|
||||
for animation.Frame > 0 {
|
||||
if rand.Intn(10) != 0 {
|
||||
if rand.Intn(100) < 37 { // P = .37
|
||||
monster, ok := r.Level.Monsters[pos]
|
||||
if ok && monster != nil {
|
||||
target, ok := monster.FindTarget(r.Level, pos)
|
||||
@ -127,6 +229,7 @@ func (r *levelController) Handle(ctx ui.Context, e ui.Event) bool {
|
||||
r.Level.MonsterTargets[pos] = target
|
||||
r.MovingMonsters.Frame(pos)
|
||||
jumping = append(jumping, pos)
|
||||
r.app.Audio.PlaySample("monster_jump.mp3")
|
||||
break
|
||||
}
|
||||
}
|
||||
@ -146,17 +249,17 @@ func (r *levelController) Play(level *tins2021.Level) {
|
||||
r.Level = level
|
||||
|
||||
r.Animations = map[string]*tins2021.Animations{
|
||||
"star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true),
|
||||
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
|
||||
"star": tins2021.NewAnimations(50*time.Millisecond, defaultAnimationFrames, true, true),
|
||||
"heart": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
|
||||
"monster": tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true),
|
||||
}
|
||||
r.IdleMonsters = tins2021.NewAnimations(500*time.Millisecond, 100, false, false)
|
||||
r.MovingMonsters = tins2021.NewAnimations(50*time.Millisecond, 20, false, false)
|
||||
r.IdleMonsters = tins2021.NewAnimations(40*time.Millisecond, 100, false, false)
|
||||
r.MovingMonsters = tins2021.NewAnimations(16*time.Millisecond, 50, false, false)
|
||||
r.DyingMonsters = tins2021.NewAnimations(16*time.Millisecond, 20, false, false)
|
||||
r.DyingMonsterTypes = map[geom.Point]tins2021.MonsterType{}
|
||||
for monster := range level.Monsters {
|
||||
r.IdleMonsters.Frame(monster)
|
||||
}
|
||||
for _, monster := range r.app.MonsterTextureNames {
|
||||
r.Animations[monster] = tins2021.NewAnimations(80*time.Millisecond, defaultAnimationFrames, true, true)
|
||||
}
|
||||
}
|
||||
|
||||
const defaultAnimationFrames = 20
|
||||
@ -167,7 +270,8 @@ func (r levelController) Render(ctx ui.Context) {
|
||||
centerTopSquare := geom.PtF32(.5, .5*geom.Sin32(twelfth))
|
||||
delta := geom.PtF32(geom.Cos32(twelfth), .5+centerTopSquare.Y)
|
||||
|
||||
view := r.Bounds().Size()
|
||||
bounds := r.Bounds()
|
||||
view := bounds.Size()
|
||||
levelView := geom.PtF32(float32(r.Level.Bounds.Dx()+2)*delta.X, float32(r.Level.Bounds.Dy()+2)*delta.Y)
|
||||
textureWidth := geom.Min32(
|
||||
geom.Floor32(tins2021.TextureSize*view.X*.75/(levelView.X*tins2021.TextureSize)),
|
||||
@ -177,38 +281,43 @@ func (r levelController) Render(ctx ui.Context) {
|
||||
|
||||
delta = delta.Mul(textureWidth)
|
||||
centerTopSquare = centerTopSquare.Mul(textureWidth)
|
||||
scoreView := geom.RectF32(levelView.X*textureWidth, offsetY+delta.Y, view.X, view.Y-delta.Y-offsetY)
|
||||
scoreView := geom.RectF32(levelView.X*textureWidth, offsetY+delta.Y, view.X, view.Y-delta.Y-offsetY).Add(bounds.Min)
|
||||
|
||||
delta.X = geom.Round32(delta.X)
|
||||
delta.Y = geom.Round32(delta.Y)
|
||||
toScreen := func(p geom.Point) geom.PointF32 {
|
||||
if p.Y%2 == 0 {
|
||||
return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, offsetY)
|
||||
return p.ToF32().Mul2D(delta.XY()).Add2D(.5*delta.X, offsetY).Add(bounds.Min)
|
||||
}
|
||||
return p.ToF32().Mul2D(delta.XY()).Add2D(0, offsetY)
|
||||
return p.ToF32().Mul2D(delta.XY()).Add2D(0, offsetY).Add(bounds.Min)
|
||||
}
|
||||
|
||||
renderer := ctx.Renderer()
|
||||
textures := ctx.Textures()
|
||||
cubes := r.Cubes.Scaled(textures, scale)
|
||||
cube := r.Cube.Scaled(scale)
|
||||
inverted := r.Inverted.Scaled(scale)
|
||||
|
||||
cubeWidth := float32(cubes.Normal.Width())
|
||||
cubeHeight := float32(cubes.Normal.Height())
|
||||
cubeWidth := float32(cube.Width())
|
||||
cubeHeight := float32(cube.Height())
|
||||
|
||||
player := ctx.Textures().ScaledByName("dwarf", scale*.6)
|
||||
star := tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName("star", scale*.4), defaultAnimationFrames)
|
||||
heart := tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName("heart", scale*.4), defaultAnimationFrames)
|
||||
star := r.app.StarTexture.Scale(scale * .4)
|
||||
heart := r.app.HeartTexture.Scale(scale * .4)
|
||||
monsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{}
|
||||
for typ, name := range r.app.MonsterTextureNames {
|
||||
monsterTextures[typ] = tins2021.NewAnimatedTexture(ctx.Textures().ScaledByName(name, scale*.4), defaultAnimationFrames)
|
||||
for typ, animation := range r.app.MonsterTextures {
|
||||
monsterTextures[typ] = animation.Scale(scale * .4)
|
||||
}
|
||||
propOffset := geom.PtF32(-.5*float32(star.Texture.Height()), -.8*float32(star.Texture.Height()))
|
||||
dyingMonsterTextures := map[tins2021.MonsterType]tins2021.AnimatedTexture{}
|
||||
for typ, animation := range r.app.DyingMonsterTextures {
|
||||
dyingMonsterTextures[typ] = animation.Scale(scale * .4)
|
||||
}
|
||||
propHeight := star.FrameSize(0).Y
|
||||
propOffset := geom.PtF32(-.5*float32(propHeight), -.8*float32(propHeight))
|
||||
|
||||
distances := r.Level.Tiles.Distances(r.Level.Player)
|
||||
|
||||
positionOfTile := func(position geom.Point, tile *tins2021.Tile) (topLeft, centerOfPlatform geom.PointF32) {
|
||||
positionOfTile := func(position geom.Point) (topLeft, centerOfPlatform geom.PointF32) {
|
||||
topLeft = toScreen(position)
|
||||
if tile.Inversed {
|
||||
if r.Level.Tiles[position].Inversed {
|
||||
return topLeft, topLeft.Add2D(.5*float32(cubeWidth), .6*float32(cubeHeight))
|
||||
}
|
||||
return topLeft, topLeft.Add2D(.5*float32(cubeWidth), .2*float32(cubeHeight))
|
||||
@ -221,10 +330,10 @@ func (r levelController) Render(ctx ui.Context) {
|
||||
if tile == nil {
|
||||
continue
|
||||
}
|
||||
screenPos, platformPos := positionOfTile(pos, tile)
|
||||
tileTexture := cubes.Normal.Texture
|
||||
screenPos, platformPos := positionOfTile(pos)
|
||||
tileTexture := cube
|
||||
if tile.Inversed {
|
||||
tileTexture = cubes.Inversed.Texture
|
||||
tileTexture = inverted
|
||||
}
|
||||
renderer.DrawTexturePoint(tileTexture, screenPos)
|
||||
if r.app.Debug {
|
||||
@ -248,19 +357,51 @@ func (r levelController) Render(ctx ui.Context) {
|
||||
renderer.DrawTexturePoint(player, playerPosition.Add(centerTopSquare))
|
||||
}
|
||||
|
||||
for pos, monsterType := range r.Level.Monsters {
|
||||
tile := r.Level.Tiles[pos]
|
||||
if tile == nil {
|
||||
for y := r.Level.Bounds.Min.Y; y < r.Level.Bounds.Max.Y; y++ {
|
||||
for x := r.Level.Bounds.Min.X; x < r.Level.Bounds.Max.X; x++ {
|
||||
pos := geom.Pt(x, y)
|
||||
monsterType, ok := r.Level.Monsters[pos]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tile := r.Level.Tiles[pos]
|
||||
if tile == nil {
|
||||
continue
|
||||
}
|
||||
texture := monsterTextures[monsterType.Type()]
|
||||
_, platformPos := positionOfTile(pos)
|
||||
if target, ok := r.Level.MonsterTargets[pos]; ok {
|
||||
_, targetPlatformPos := positionOfTile(target)
|
||||
dt := float32(r.MovingMonsters.Frame(pos)) / 50.
|
||||
delta := targetPlatformPos.Sub(platformPos)
|
||||
curve := geom.PtF32(0, .6*geom.Sin32(dt*geom.Pi)*textureWidth)
|
||||
interpolatedPos := platformPos.Add(delta.Mul(dt)).Sub(curve)
|
||||
texture.Draw(renderer, interpolatedPos.Add(propOffset), r.Animations["monster"].Frame(pos))
|
||||
} else {
|
||||
texture.Draw(renderer, platformPos.Add(propOffset), r.Animations["monster"].Frame(pos))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var died []geom.Point
|
||||
for pos, monster := range r.DyingMonsterTypes {
|
||||
frame := r.DyingMonsters.Frame(pos)
|
||||
if frame == 20 {
|
||||
died = append(died, pos)
|
||||
continue
|
||||
}
|
||||
_, platformPos := positionOfTile(pos, tile)
|
||||
name := r.app.MonsterTextureNames[monsterType.Type()]
|
||||
monsterTextures[monsterType.Type()].Draw(renderer, platformPos.Add(propOffset), r.Animations[name].Frame(pos))
|
||||
texture := dyingMonsterTextures[monster]
|
||||
_, platformPos := positionOfTile(pos)
|
||||
texture.Draw(renderer, platformPos.Add(propOffset), frame)
|
||||
}
|
||||
for _, pos := range died {
|
||||
delete(r.DyingMonsters.Values, pos)
|
||||
delete(r.DyingMonsterTypes, pos)
|
||||
}
|
||||
|
||||
textColor := ctx.Style().Palette.Text
|
||||
scoreFont := ctx.Fonts().Font("score")
|
||||
fontOffsetY := .5 * (float32(star.Texture.Height()) - scoreFont.Height())
|
||||
fontOffsetY := .5 * (float32(propHeight) - scoreFont.Height())
|
||||
|
||||
// stars & hearts
|
||||
scoreTopLeft := scoreView.Min
|
||||
@ -282,24 +423,34 @@ func (r levelController) Render(ctx ui.Context) {
|
||||
scoreTopLeft.Y -= scoreFont.Height()
|
||||
renderer.Text(scoreFont, scoreTopLeft, textColor, "Score:")
|
||||
|
||||
bounds := r.Bounds()
|
||||
centerX := .5 * bounds.Dx()
|
||||
centerX := bounds.Min.X + .5*bounds.Dx()
|
||||
titleFont := ctx.Fonts().Font("title")
|
||||
|
||||
screenSize := ctx.Renderer().Size().ToF32()
|
||||
screen := geom.RectF32(0, 0, screenSize.X, screenSize.Y)
|
||||
if r.Level.GameOver {
|
||||
renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`))
|
||||
renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`))
|
||||
offsetY := .5*bounds.Dy() - titleFont.Height()
|
||||
renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "GAME OVER", ui.AlignCenter)
|
||||
|
||||
if r.Highscore {
|
||||
highscoreFont := ctx.Fonts().Font("default")
|
||||
offsetY += titleFont.Height() + scoreFont.Height()
|
||||
renderer.TextAlign(highscoreFont, geom.PtF32(centerX, offsetY), ctx.Style().Palette.Primary, "NEW HIGHSCORE", ui.AlignCenter)
|
||||
}
|
||||
|
||||
offsetY += titleFont.Height() + scoreFont.Height()
|
||||
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Final score: %d", r.Level.Score), ui.AlignCenter)
|
||||
|
||||
offsetY += 2 * scoreFont.Height()
|
||||
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [enter] to show highscores.", ui.AlignCenter)
|
||||
|
||||
offsetY += 2 * scoreFont.Height()
|
||||
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, "Press [escape] to quit.", ui.AlignCenter)
|
||||
} else if r.Level.StarsCollected == r.Level.Stars {
|
||||
renderer.FillRectangle(bounds, zntg.MustHexColor(`#0000007F`))
|
||||
renderer.FillRectangle(screen, zntg.MustHexColor(`#0000007F`))
|
||||
offsetY := .5*bounds.Dy() - titleFont.Height()
|
||||
renderer.TextAlign(titleFont, geom.PtF32(.5*bounds.Dx(), offsetY), textColor, "COMPLETED", ui.AlignCenter)
|
||||
renderer.TextAlign(titleFont, geom.PtF32(centerX, offsetY), textColor, "COMPLETED", ui.AlignCenter)
|
||||
|
||||
offsetY += titleFont.Height() + scoreFont.Height()
|
||||
renderer.TextAlign(scoreFont, geom.PtF32(centerX, offsetY), textColor, fmt.Sprintf("Score: %d", r.Level.Score), ui.AlignCenter)
|
||||
|
@ -29,6 +29,8 @@ func (c *center) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.P
|
||||
|
||||
type mainMenu struct {
|
||||
ui.StackPanel
|
||||
|
||||
music *Music
|
||||
}
|
||||
|
||||
func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
|
||||
@ -37,10 +39,23 @@ func newMainMenu(app *appContext, ctx ui.Context) ui.Control {
|
||||
menu.Add("Play", func(ctx ui.Context) {
|
||||
app.Play(ctx)
|
||||
})
|
||||
resume := app.Score.Current.Difficulty > 0
|
||||
if resume {
|
||||
menu.Add("Resume", func(ctx ui.Context) { app.PlayResume(ctx) })
|
||||
}
|
||||
menu.Add("Highscores", func(ctx ui.Context) { app.ShowHighscores(ctx) })
|
||||
menu.Add("Settings", func(ctx ui.Context) { app.ShowSettings(ctx) })
|
||||
menu.Add("Credits", func(ctx ui.Context) { app.ShowCredits(ctx) })
|
||||
menu.Add("Quit", func(ctx ui.Context) { ctx.Quit() })
|
||||
|
||||
menu.Activate(1) // play
|
||||
if resume {
|
||||
menu.Activate(2) // resume
|
||||
} else {
|
||||
menu.Activate(1) // play
|
||||
}
|
||||
menu.ActiveChanged.AddHandlerEmpty((func(ui.Context) {
|
||||
app.MenuInteraction()
|
||||
}))
|
||||
ctx.Animate()
|
||||
|
||||
return Center(&mainMenu{
|
||||
|
@ -11,6 +11,8 @@ type Menu struct {
|
||||
|
||||
active int
|
||||
buttons []*MenuButton
|
||||
|
||||
ActiveChanged ui.Events
|
||||
}
|
||||
|
||||
func NewMenu() *Menu {
|
||||
@ -25,7 +27,7 @@ func (m *Menu) Activate(i int) {
|
||||
if len(m.buttons) == 0 || i < 0 {
|
||||
return
|
||||
}
|
||||
m.updateActiveButton(i % len(m.buttons))
|
||||
m.updateActiveButton(nil, i%len(m.buttons))
|
||||
}
|
||||
|
||||
func (m *Menu) Add(text string, click func(ui.Context)) {
|
||||
@ -54,29 +56,34 @@ func (m *Menu) Handle(ctx ui.Context, e ui.Event) bool {
|
||||
onEscape(ctx)
|
||||
}
|
||||
case ui.KeyDown:
|
||||
m.updateActiveButton((m.active + 1) % len(m.buttons))
|
||||
m.updateActiveButton(ctx, (m.active+1)%len(m.buttons))
|
||||
case ui.KeyUp:
|
||||
m.updateActiveButton((m.active + len(m.buttons) - 1) % len(m.buttons))
|
||||
m.updateActiveButton(ctx, (m.active+len(m.buttons)-1)%len(m.buttons))
|
||||
case ui.KeyEnter:
|
||||
m.buttons[m.active].InvokeClick(ctx)
|
||||
}
|
||||
case *ui.MouseMoveEvent:
|
||||
for i, button := range m.buttons {
|
||||
if button.IsOver() {
|
||||
m.updateActiveButton(i)
|
||||
m.updateActiveButton(ctx, i)
|
||||
ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer)
|
||||
break
|
||||
}
|
||||
}
|
||||
m.updateActiveButton(m.active)
|
||||
m.updateActiveButton(ctx, m.active)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *Menu) updateActiveButton(active int) {
|
||||
func (m *Menu) updateActiveButton(ctx ui.Context, active int) {
|
||||
change := m.active != active
|
||||
m.active = active
|
||||
for i, btn := range m.buttons {
|
||||
btn.Over = i == m.active
|
||||
}
|
||||
if change && ctx != nil {
|
||||
m.ActiveChanged.Notify(ctx, nil)
|
||||
}
|
||||
}
|
||||
|
||||
type MenuButton struct {
|
||||
|
@ -1 +1 @@
|
||||
dwarf: images/dwarf.png
|
||||
dwarf: resources/textures/gnome.png
|
BIN
cmd/tins2021/resources/textures/chasing-dying-monster.png
Normal file
After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 36 KiB |
BIN
cmd/tins2021/resources/textures/random-walking-dying-monster.png
Normal file
After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 37 KiB After Width: | Height: | Size: 37 KiB |
After Width: | Height: | Size: 86 KiB |
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
899
cmd/tins2021/settings.go
Normal 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()
|
||||
}
|
44
cmd/tins2021/texturegenerator.go
Normal 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{})
|
||||
}
|
||||
}
|
42
cmd/tins2021/texturemap.go
Normal 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)}
|
||||
}
|
@ -1,48 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"opslag.de/schobers/tins2021"
|
||||
"opslag.de/schobers/zntg/ui"
|
||||
)
|
||||
|
||||
type namedTexture struct {
|
||||
ui.Texture
|
||||
|
||||
Name string
|
||||
func chrono(action func()) time.Duration {
|
||||
start := time.Now()
|
||||
action()
|
||||
return time.Now().Sub(start)
|
||||
}
|
||||
|
||||
func newNamedTexture(textures *ui.Textures, name string, im image.Image) namedTexture {
|
||||
texture, err := textures.CreateTextureGo(name, im, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return namedTexture{texture, name}
|
||||
}
|
||||
|
||||
func (t namedTexture) Scaled(textures *ui.Textures, scale float32) namedTexture {
|
||||
return namedTexture{textures.ScaledByName(t.Name, scale), t.Name}
|
||||
}
|
||||
|
||||
type cubeTexture struct {
|
||||
Normal, Inversed namedTexture
|
||||
}
|
||||
|
||||
func newCubeTexture(textures *ui.Textures, color string) cubeTexture {
|
||||
return cubeTexture{
|
||||
Normal: newNamedTexture(textures, "cube_"+color, tins2021.GenerateCube(color)),
|
||||
Inversed: newNamedTexture(textures, "cube_"+color+"_inversed", tins2021.GenerateHole(color)),
|
||||
}
|
||||
}
|
||||
|
||||
func (t cubeTexture) Scaled(textures *ui.Textures, scale float32) cubeTexture {
|
||||
return cubeTexture{
|
||||
Normal: t.Normal.Scaled(textures, scale),
|
||||
Inversed: t.Inversed.Scaled(textures, scale),
|
||||
}
|
||||
func chronoErr(action func() error) (time.Duration, error) {
|
||||
start := time.Now()
|
||||
err := action()
|
||||
return time.Now().Sub(start), err
|
||||
}
|
||||
|
||||
func loadTextureImage(ctx ui.Context, resource string) image.Image {
|
||||
@ -58,26 +38,29 @@ func loadTextureImage(ctx ui.Context, resource string) image.Image {
|
||||
return im
|
||||
}
|
||||
|
||||
func saveTextureImage(resource string, im image.Image) {
|
||||
func newAnimatedTexture(ctx ui.Context, name string, frames int, render func() image.Image) tins2021.AnimatedTexture {
|
||||
resourceName := fmt.Sprintf("resources/textures/%s.png", name)
|
||||
raw := loadTextureImage(ctx, resourceName)
|
||||
if raw == nil {
|
||||
renderTime := chrono(func() {
|
||||
raw = render()
|
||||
})
|
||||
if err := saveTextureImage(resourceName, raw); err != nil {
|
||||
log.Printf("failed to write animated texture \"%s\" to disk (%s); error: %v\n", name, resourceName, err)
|
||||
} else {
|
||||
log.Printf("generated animated texture \"%s\" in %v (and saved to disk \"%s\")\n", name, renderTime, resourceName)
|
||||
}
|
||||
}
|
||||
texture := tins2021.MustCreateNamedTextureImage(ctx.Textures(), name, raw)
|
||||
return tins2021.NewAnimatedTexture(texture, frames)
|
||||
}
|
||||
|
||||
func saveTextureImage(resource string, im image.Image) error {
|
||||
os.MkdirAll(filepath.Dir(resource), 0744)
|
||||
out, err := os.Create(resource)
|
||||
if err != nil {
|
||||
return
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
png.Encode(out, im)
|
||||
return png.Encode(out, im)
|
||||
}
|
||||
|
||||
func newAnimatedTexture(ctx ui.Context, name, resource string, render animatedTextureRenderFn) tins2021.AnimatedTexture {
|
||||
raw := loadTextureImage(ctx, resource)
|
||||
if raw == nil {
|
||||
raw = render()
|
||||
saveTextureImage(resource, raw)
|
||||
}
|
||||
texture, err := ctx.Textures().CreateTextureGo(name, raw, true)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return tins2021.NewSquareAnimatedTexture(texture)
|
||||
}
|
||||
|
||||
type animatedTextureRenderFn func() image.Image
|
||||
|
@ -1,23 +1,20 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"flag"
|
||||
"image/color"
|
||||
"log"
|
||||
|
||||
rice "github.com/GeertJohan/go.rice"
|
||||
"opslag.de/schobers/geom"
|
||||
"opslag.de/schobers/tins2021"
|
||||
"opslag.de/schobers/zntg"
|
||||
"opslag.de/schobers/zntg/addons/riceres"
|
||||
"opslag.de/schobers/zntg/addons/embedres"
|
||||
"opslag.de/schobers/zntg/ui"
|
||||
)
|
||||
|
||||
// #cgo windows LDFLAGS: -Wl,-subsystem,windows
|
||||
import "C"
|
||||
|
||||
//go:generate go get -u github.com/GeertJohan/go.rice/rice
|
||||
//go:generate rice embed
|
||||
//go:embed resources
|
||||
var resources embed.FS
|
||||
|
||||
func main() {
|
||||
err := run()
|
||||
@ -27,20 +24,25 @@ func main() {
|
||||
}
|
||||
|
||||
func openResources() ui.Resources {
|
||||
box := rice.MustFindBox(`resources`)
|
||||
embedded := riceres.New(box)
|
||||
return ui.NewFallbackResources(ui.NewPathResources(nil, box.Name()), embedded)
|
||||
embedded := embedres.New(resources)
|
||||
return ui.NewFallbackResources(ui.NewPathResources(nil, `resources`), embedded)
|
||||
}
|
||||
|
||||
func run() error {
|
||||
var background string
|
||||
var extract bool
|
||||
flag.BoolVar(&extract, "extract-resources", false, "extracts all resources to the current working directory")
|
||||
flag.StringVar(&background, "background", "", "generates a background")
|
||||
flag.BoolVar(&extract, "extract", false, "extracts all resources to the current working directory")
|
||||
flag.Parse()
|
||||
|
||||
if extract {
|
||||
return copyBoxToDisk()
|
||||
return copyBoxToDisk(resources)
|
||||
}
|
||||
|
||||
res := openResources()
|
||||
if background != "" {
|
||||
return GenerateBackground(res, background)
|
||||
}
|
||||
|
||||
ptPtr := func(x, y int) *geom.Point {
|
||||
p := geom.Pt(x, y)
|
||||
@ -54,6 +56,11 @@ func run() error {
|
||||
}
|
||||
defer settings.Store()
|
||||
|
||||
score, err := tins2021.LoadScores()
|
||||
if err != nil {
|
||||
log.Printf("unable to load score; error: %v", err)
|
||||
}
|
||||
|
||||
var location *geom.PointF32
|
||||
if settings.Window.Location != nil {
|
||||
location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)}
|
||||
@ -78,7 +85,10 @@ func run() error {
|
||||
|
||||
app := &app{
|
||||
settings: settings,
|
||||
score: &score,
|
||||
}
|
||||
defer tins2021.SaveScores(app.score)
|
||||
|
||||
style := ui.DefaultStyle()
|
||||
style.Palette = &ui.Palette{
|
||||
Background: zntg.MustHexColor(`#494949`),
|
||||
|
4
cmd/tins2021/tins2021_console.go
Normal file
@ -0,0 +1,4 @@
|
||||
package main
|
||||
|
||||
// #cgo windows,!console LDFLAGS: -Wl,-subsystem,windows
|
||||
import "C"
|
238
cmd/tins2021/virtualcontrols.go
Normal 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
@ -1,11 +1,43 @@
|
||||
package tins2021
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"opslag.de/schobers/zntg"
|
||||
)
|
||||
|
||||
const appName = "tins2021_qbitter"
|
||||
|
||||
func UserDir() (string, error) { return zntg.UserDir(appName) }
|
||||
func UserDir() (string, error) { return zntg.UserConfigDir(appName) }
|
||||
|
||||
func UserFile(name string) (string, error) { return zntg.UserFile(appName, name) }
|
||||
func UserFile(name string) (string, error) { return zntg.UserConfigFile(appName, name) }
|
||||
|
||||
func LoadUserFileJSON(name string, v interface{}) error {
|
||||
path, err := UserFile(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return json.NewDecoder(f).Decode(v)
|
||||
}
|
||||
|
||||
func SaveUserFileJSON(name string, v interface{}) error {
|
||||
path, err := UserFile(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
return json.NewEncoder(f).Encode(v)
|
||||
}
|
||||
|
24
level.go
@ -92,10 +92,15 @@ func (l *Level) MoveMonster(target, source geom.Point) {
|
||||
l.DestroyMonster(source)
|
||||
}
|
||||
|
||||
func (l *Level) MovePlayer(dir Direction) bool {
|
||||
type MonsterHit struct {
|
||||
Position geom.Point
|
||||
Type MonsterType
|
||||
}
|
||||
|
||||
func (l *Level) MovePlayer(dir Direction) (bool, *MonsterHit) {
|
||||
towards, allowed := l.CanPlayerMove(dir)
|
||||
if !allowed {
|
||||
return false
|
||||
return false, nil
|
||||
}
|
||||
l.Player = towards
|
||||
tile := l.Tiles[towards]
|
||||
@ -109,13 +114,18 @@ func (l *Level) MovePlayer(dir Direction) bool {
|
||||
tile.Star = false
|
||||
l.Score += 25
|
||||
}
|
||||
var hit *MonsterHit
|
||||
if l.Monsters[towards] != nil {
|
||||
hit = &MonsterHit{
|
||||
Position: towards,
|
||||
Type: l.Monsters[towards].Type(),
|
||||
}
|
||||
l.DecrementLive()
|
||||
l.DestroyMonster(towards)
|
||||
l.Score -= 5
|
||||
}
|
||||
l.Score -= 1 // for every move
|
||||
return true
|
||||
return true, hit
|
||||
}
|
||||
|
||||
func (l *Level) Randomize(difficulty int, stars int) {
|
||||
@ -170,7 +180,7 @@ func (l *Level) Randomize(difficulty int, stars int) {
|
||||
l.Tiles[pos].Star = true
|
||||
stars--
|
||||
}
|
||||
hearts := 1 + (100-difficulty)/50 // [3..1] (only difficulty has 3 hearts)
|
||||
hearts := rand.Intn(2 + (100-difficulty)/50) // [3..1] (only difficulty 0 has 3 hearts)
|
||||
for hearts > 0 {
|
||||
pos := pick(difficulty)
|
||||
if l.Tiles[pos].Occupied() {
|
||||
@ -179,9 +189,9 @@ func (l *Level) Randomize(difficulty int, stars int) {
|
||||
l.Tiles[pos].Heart = true
|
||||
hearts--
|
||||
}
|
||||
monsters := 2 + (8 * difficulty / 100)
|
||||
minRandomMonster := (100-difficulty)*80/100 + 10 // [90..10]
|
||||
minChaserMonster := (100-difficulty)*50/100 + 50 // [100..50]
|
||||
monsters := 5 + (25 * difficulty / 100)
|
||||
minRandomMonster := (100-difficulty)*60/100 + 10 // [70..10]
|
||||
minChaserMonster := (100-difficulty)*70/100 + 25 // [95..25]
|
||||
for monsters > 0 {
|
||||
pos := pick(100 - difficulty)
|
||||
curr := l.Monsters[pos]
|
||||
|
15
math.go
Normal file
@ -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
@ -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)
|
||||
}
|
@ -4,6 +4,8 @@ import (
|
||||
"opslag.de/schobers/geom"
|
||||
)
|
||||
|
||||
var AllMonsterTypes = []MonsterType{MonsterTypeStraight, MonsterTypeRandom, MonsterTypeChaser}
|
||||
|
||||
type ChasingMonster struct{}
|
||||
|
||||
func (m ChasingMonster) Type() MonsterType { return MonsterTypeChaser }
|
||||
|
39
namedtexture.go
Normal file
@ -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) }
|
@ -13,7 +13,7 @@ func CreateHeart() geom.PolygonF {
|
||||
var polygon geom.PolygonF
|
||||
const segments = 100
|
||||
for segment := 0; segment < 100; segment++ {
|
||||
t := 2 * geom.Pi * float64(segment) / segments
|
||||
t := 2 * geom.Pi * Percentage(segment, segments)
|
||||
st := geom.Sin(t)
|
||||
polygon.Points = append(polygon.Points, geom.PtF(
|
||||
16*st*st*st,
|
||||
@ -23,15 +23,15 @@ func CreateHeart() geom.PolygonF {
|
||||
}
|
||||
|
||||
func CreateHexagon() geom.PolygonF {
|
||||
var polygon geom.PolygonF
|
||||
pt := func(rotation float64) geom.PointF {
|
||||
a := .5*geom.Pi + 2*geom.Pi*rotation
|
||||
return geom.PtF(geom.Cos(a), geom.Sin(a))
|
||||
}
|
||||
const sides = 6
|
||||
for side := 0; side < 6; side++ {
|
||||
return CreateRegularPolygon(6)
|
||||
}
|
||||
|
||||
func CreateRegularPolygon(sides int) geom.PolygonF {
|
||||
var polygon geom.PolygonF
|
||||
for side := 0; side < sides; side++ {
|
||||
polygon.Points = append(polygon.Points,
|
||||
pt(float64(side)/float64(sides)),
|
||||
PolarUnity(float64(side)/float64(sides)),
|
||||
)
|
||||
}
|
||||
return polygon
|
||||
@ -39,14 +39,10 @@ func CreateHexagon() geom.PolygonF {
|
||||
|
||||
func CreateStar(sides int) geom.PolygonF {
|
||||
var polygon geom.PolygonF
|
||||
pt := func(rotation float64) geom.PointF {
|
||||
a := .5*geom.Pi + 2*geom.Pi*rotation
|
||||
return geom.PtF(geom.Cos(a), geom.Sin(a))
|
||||
}
|
||||
for side := 0; side < sides; side++ {
|
||||
polygon.Points = append(polygon.Points,
|
||||
pt(float64(side)/float64(sides)),
|
||||
pt((float64(side)+0.5)/float64(sides)).Mul(.5),
|
||||
PolarUnity(float64(side)/float64(sides)),
|
||||
PolarUnity((float64(side)+0.5)/float64(sides)).Mul(.5),
|
||||
)
|
||||
}
|
||||
return polygon
|
@ -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
@ -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
@ -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
@ -0,0 +1 @@
|
||||
go build -tags static -ldflags "-s -w" -o "%GOPATH%/bin/qbitter.exe" opslag.de/schobers/tins2021/cmd/tins2021
|
@ -15,7 +15,8 @@ rm -rf build/linux*
|
||||
rm -rf build/macosx*
|
||||
rm -rf build/windows*
|
||||
|
||||
mkdir -p build/release
|
||||
qbitter_release_dir=release/qbitter_${version_safe}
|
||||
mkdir -p build/${qbitter_release_dir}
|
||||
|
||||
go generate ../cmd/tins2021
|
||||
|
||||
@ -23,38 +24,38 @@ mkdir -p build/linux
|
||||
go build -tags static -ldflags "-s -w" -o build/linux/qbitter ../cmd/tins2021
|
||||
cp ../README.md build/linux
|
||||
cd build/linux
|
||||
zip -9 -q ../release/qbitter_${version_safe}_linux_amd64.zip *
|
||||
echo "Created Linux release: build/release/qbitter_${version_safe}_linux_amd64.zip"
|
||||
zip -9 -q ../${qbitter_release_dir}/qbitter_${version_safe}_linux_amd64.zip *
|
||||
echo "Created Linux release: build/${qbitter_release_dir}/qbitter_${version_safe}_linux_amd64.zip"
|
||||
cd ../..
|
||||
|
||||
mkdir -p build/linux-allegro
|
||||
go build -tags static,allegro -ldflags "-s -w" -o build/linux-allegro/qbitter ../cmd/tins2021
|
||||
cp ../README.md build/linux-allegro
|
||||
cd build/linux-allegro
|
||||
zip -9 -q ../release/qbitter_allegro_${version_safe}_linux_amd64.zip *
|
||||
echo "Created Linux (Allegro) release: build/release/qbitter_allegro_${version_safe}_linux_amd64.zip"
|
||||
zip -9 -q ../${qbitter_release_dir}/qbitter_allegro_${version_safe}_linux_amd64.zip *
|
||||
echo "Created Linux (Allegro) release: build/${qbitter_release_dir}/qbitter_allegro_${version_safe}_linux_amd64.zip"
|
||||
cd ../..
|
||||
|
||||
mkdir -p build/macosx
|
||||
CGO_ENABLED=1 CC=o64-clang CXX=o64-clang++ GOOS=darwin GOARCH=amd64 go build -tags static -ldflags "-s -w" -o build/macosx/qbitter ../cmd/tins2021
|
||||
cp ../README.md build/macosx
|
||||
cd build/macosx
|
||||
zip -9 -q ../release/qbitter_${version_safe}_macosx_amd64.zip *
|
||||
echo "Created Mac OS X release: build/release/qbitter_${version_safe}_macosx_amd64.zip"
|
||||
zip -9 -q ../${qbitter_release_dir}/qbitter_${version_safe}_macosx_amd64.zip *
|
||||
echo "Created Mac OS X release: build/${qbitter_release_dir}/qbitter_${version_safe}_macosx_amd64.zip"
|
||||
cd ../..
|
||||
|
||||
mkdir -p build/windows
|
||||
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 go build -tags static -ldflags "-s -w" -o build/windows/qbitter.exe ../cmd/tins2021
|
||||
cp ../README.md build/windows
|
||||
cd build/windows
|
||||
zip -9 -q ../release/qbitter_${version_safe}_windows_amd64.zip *
|
||||
echo "Created Windows release: build/release/qbitter_${version_safe}_windows_amd64.zip"
|
||||
zip -9 -q ../${qbitter_release_dir}/qbitter_${version_safe}_windows_amd64.zip *
|
||||
echo "Created Windows release: build/${qbitter_release_dir}/qbitter_${version_safe}_windows_amd64.zip"
|
||||
cd ../..
|
||||
|
||||
mkdir -p build/windows-allegro
|
||||
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 go build -tags static,allegro,mingw64_7_3 -ldflags "-s -w" -o build/windows-allegro/qbitter.exe ../cmd/tins2021
|
||||
cp ../README.md build/windows-allegro
|
||||
cd build/windows-allegro
|
||||
zip -9 -q ../release/qbitter_allegro_${version_safe}_windows_amd64.zip *
|
||||
echo "Created Windows (Allegro) release: build/release/qbitter_allegro_${version_safe}_windows_amd64.zip"
|
||||
zip -9 -q ../${qbitter_release_dir}/qbitter_allegro_${version_safe}_windows_amd64.zip *
|
||||
echo "Created Windows (Allegro) release: build/${qbitter_release_dir}/qbitter_allegro_${version_safe}_windows_amd64.zip"
|
||||
cd ../..
|
||||
|
38
settings.go
@ -4,34 +4,40 @@ import (
|
||||
"os"
|
||||
|
||||
"opslag.de/schobers/geom"
|
||||
"opslag.de/schobers/zntg"
|
||||
)
|
||||
|
||||
type Settings struct {
|
||||
Window WindowSettings
|
||||
const settingsFileName = "settings.json"
|
||||
|
||||
type AudioSettings struct {
|
||||
SoundVolume float64
|
||||
MusicVolume float64
|
||||
}
|
||||
|
||||
func SettingsPath() (string, error) {
|
||||
return UserFile("settings.json")
|
||||
type ControlsSettings struct {
|
||||
Type string
|
||||
MoveDownRight string
|
||||
MoveDownLeft string
|
||||
MoveUpLeft string
|
||||
MoveUpRight string
|
||||
Virtual bool
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Audio AudioSettings
|
||||
Controls ControlsSettings
|
||||
Window WindowSettings
|
||||
}
|
||||
|
||||
func (s *Settings) Init() error {
|
||||
path, err := SettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
err := LoadUserFileJSON(settingsFileName, s)
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return zntg.DecodeJSON(path, s)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Settings) Store() error {
|
||||
path, err := SettingsPath()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return zntg.EncodeJSON(path, s)
|
||||
return SaveUserFileJSON(settingsFileName, s)
|
||||
}
|
||||
|
||||
type WindowSettings struct {
|
||||
|