Compare commits

...

23 Commits

Author SHA1 Message Date
d0e6721953 Changed appearance of close button on (large) dialogs back to original. 2020-05-26 18:59:59 +02:00
1ae7e9ebe4 Added Mac OS X cross-compilation to release script (only SDL). 2020-05-26 18:58:17 +02:00
8264942602 Combined two Allegro related code units. 2020-05-26 18:54:26 +02:00
6ce7baed2b Removed (already redundant) resources.go. 2020-05-26 09:05:08 +02:00
bd1d685c20 Hid console window for Windows Allegro builds.
Removed explicit SDL dependency in game.
2020-05-25 22:54:58 +02:00
93002a784b Added hints on keyboard shortcuts. 2020-05-25 22:26:08 +02:00
7230ac966b Using the refactored Resources system. 2020-05-25 22:24:58 +02:00
62cbe14170 Updated README.md on CLI. 2020-05-25 20:03:30 +02:00
df97e826c8 Replacing dots with underscores in the version used in the filenames. 2020-05-25 20:02:19 +02:00
1bea3dd412 Fixed bug where the buy flower button wasn't enabled on load. 2020-05-24 19:57:39 +02:00
ddef723f80 Added keyboard shortcuts for load/new/save game. 2020-05-24 19:57:20 +02:00
cb58ebdf77 Added confirmation dialogs for save/load/new game. 2020-05-24 19:52:46 +02:00
6a8a23bf61 Elaborated README a bit on using the Allegro backend. 2020-05-23 19:14:45 +02:00
820d0f109a Cursor changes when tool is selected. 2020-05-23 16:03:18 +02:00
2f5f682a59 Fixed bug where hovering a buy flower button wasn't visualized properly. 2020-05-23 12:36:44 +02:00
064ec4619d Added tooltips when planting flowers. 2020-05-23 12:34:17 +02:00
ef6a60e155 Made coneflower prefer cold climates more. 2020-05-23 12:34:09 +02:00
ea26db29f3 Added allegro build tag to the README. 2020-05-23 12:08:29 +02:00
d24b4eb376 Updated release script to produce Allegro build as well.
Moved zipping directly after the build folder is ready.
2020-05-23 12:03:45 +02:00
855db3375c Added build flag to switch to Allegro backend.
Added logs that print which backend is used.
2020-05-23 11:08:50 +02:00
822d8ab584 Fixed incorrect panning. 2020-05-23 11:02:30 +02:00
b7b663ddb5 Fixed bug where mouse was released outside terrain area when dragging it kept dragging. 2020-05-23 10:43:05 +02:00
66eaa054c5 Refactored Botanim to use zntg (rendering abstraction). 2020-05-23 10:30:49 +02:00
49 changed files with 1329 additions and 2270 deletions

View File

@ -118,6 +118,15 @@ go generate opslag.de/schobers/tins2020/cmd/tins2020
go install opslag.de/schobers/tins2020/cmd/tins2020 -tags static -ldflags "-s -w" go install opslag.de/schobers/tins2020/cmd/tins2020 -tags static -ldflags "-s -w"
``` ```
If you want to use the Allegro backend you can add the build tag `allegro` to the `go install` command. E.g.:
```
go install opslag.de/schobers/tins2020/cmd/tins2020 -tags static,allegro -ldflags "-s -w"
```
In this case the SDL 2.0 prerequisite is replaced by the 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.
## Sources ## Sources
Can be found at https://opslag.de/schobers/tins2020 (Git repository). Can be found at https://opslag.de/schobers/tins2020 (Git repository).

View File

@ -1,57 +0,0 @@
package tins2020
import "time"
type Animation struct {
active bool
start time.Time
interval time.Duration
lastUpdate time.Duration
}
func NewAnimation(interval time.Duration) Animation {
return Animation{
active: true,
start: time.Now(),
interval: interval,
}
}
func NewAnimationPtr(interval time.Duration) *Animation {
ani := NewAnimation(interval)
return &ani
}
func (a *Animation) Animate() bool {
since := time.Since(a.start)
if !a.active || since < a.lastUpdate+a.interval {
return false
}
a.lastUpdate += a.interval
return true
}
func (a *Animation) AnimateFn(fn func()) {
since := time.Since(a.start)
for a.active && since > a.lastUpdate+a.interval {
fn()
a.lastUpdate += a.interval
}
}
func (a *Animation) Pause() {
a.active = false
}
func (a *Animation) Run() {
if a.active {
return
}
a.active = true
a.start = time.Now()
a.lastUpdate = 0
}
func (a *Animation) SetInterval(interval time.Duration) {
a.interval = interval
}

View File

@ -1,59 +0,0 @@
package tins2020
import "github.com/veandco/go-sdl2/sdl"
type ButtonBar struct {
Container
Background sdl.Color
ButtonLength int32
Orientation Orientation
Buttons []Control
}
const buttonBarWidth = 96
func (b *ButtonBar) Init(ctx *Context) error {
for i := range b.Buttons {
b.AddChild(b.Buttons[i])
}
return b.Container.Init(ctx)
}
func (b *ButtonBar) Arrange(ctx *Context, bounds Rectangle) {
b.Container.Arrange(ctx, bounds)
length := b.ButtonLength
switch b.Orientation {
case OrientationHorizontal:
if length == 0 {
length = bounds.H
}
offset := bounds.X
for i := range b.Buttons {
b.Buttons[i].Arrange(ctx, Rect(offset, bounds.Y, length, bounds.H))
offset += length
}
default:
if length == 0 {
length = bounds.W
}
offset := bounds.Y
for i := range b.Buttons {
b.Buttons[i].Arrange(ctx, Rect(bounds.X, offset, bounds.W, length))
offset += length
}
}
}
func (b *ButtonBar) Render(ctx *Context) {
SetDrawColor(ctx.Renderer, b.Background)
ctx.Renderer.FillRect(b.Bounds.SDLPtr())
b.Container.Render(ctx)
}
type Orientation int
const (
OrientationVertical Orientation = iota
OrientationHorizontal
)

View File

@ -2,29 +2,68 @@ package tins2020
import ( import (
"fmt" "fmt"
"image/color"
"time" "time"
"github.com/veandco/go-sdl2/sdl" "opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
) )
type TextureCache struct {
Value ui.Texture
}
func (c *TextureCache) Height() float32 {
if c.Value == nil {
return 0
}
return c.Value.Height()
}
func (c *TextureCache) Update(update func() (ui.Texture, error)) error {
texture, err := update()
if err != nil {
return err
}
if c.Value != nil {
c.Value.Destroy()
}
c.Value = texture
return nil
}
func (c *TextureCache) Width() float32 {
if c.Value == nil {
return 0
}
return c.Value.Width()
}
func textUpdate(render ui.Renderer, font ui.Font, color color.Color, text string) func() (ui.Texture, error) {
return func() (ui.Texture, error) { return render.TextTexture(font, color, text) }
}
type BuyFlowerButton struct { type BuyFlowerButton struct {
IconButton IconButton
IconDisabled string
FlowerID string FlowerID string
Flower FlowerDescriptor Flower FlowerDescriptor
hoverAnimation *Animation upToDate bool
hoverOffset int32 hoverAnimation zntg.Animation
hoverTexture *Texture hoverOffset float32
priceTexture *Texture hoverTexture TextureCache
priceTexture TextureCache
} }
func NewBuyFlowerButton(icon, iconDisabled, flowerID string, flower FlowerDescriptor, onClick EventContextFn) *BuyFlowerButton { func NewBuyFlowerButton(icon, iconDisabled, flowerID string, flower FlowerDescriptor, click ui.EventEmptyFn) *BuyFlowerButton {
return &BuyFlowerButton{ return &BuyFlowerButton{
IconButton: *NewIconButtonConfigure(icon, onClick, func(b *IconButton) { IconButton: *NewIconButtonConfigure(icon, click, func(b *IconButton) {
b.IconDisabled = iconDisabled b.Disabled = !flower.Unlocked
b.IsDisabled = !flower.Unlocked
}), }),
IconDisabled: iconDisabled,
FlowerID: flowerID, FlowerID: flowerID,
Flower: flower, Flower: flower,
} }
@ -32,7 +71,7 @@ func NewBuyFlowerButton(icon, iconDisabled, flowerID string, flower FlowerDescri
func (b *BuyFlowerButton) animate() { func (b *BuyFlowerButton) animate() {
b.hoverOffset++ b.hoverOffset++
if b.hoverOffset > b.hoverTexture.Size().X+b.Bounds.W { if b.hoverOffset > b.hoverTexture.Width()+b.Bounds().Dx() {
b.hoverOffset = 0 b.hoverOffset = 0
} }
} }
@ -44,75 +83,70 @@ func (b *BuyFlowerButton) fmtTooltipText() string {
return fmt.Sprintf("%s - %s - %s", FmtMoney(b.Flower.BuyPrice), b.Flower.Name, b.Flower.Description) return fmt.Sprintf("%s - %s - %s", FmtMoney(b.Flower.BuyPrice), b.Flower.Name, b.Flower.Description)
} }
func (b *BuyFlowerButton) updateTexts(ctx *Context) error { func (b *BuyFlowerButton) updateTexts(ctx ui.Context) error {
if b.upToDate {
return nil
}
text := b.fmtTooltipText() text := b.fmtTooltipText()
font := ctx.Fonts.Font("small") font := ctx.Fonts().Font("small")
color := MustHexColor("#ffffff") color := zntg.MustHexColor("#FFFFFF")
texture, err := font.Render(ctx.Renderer, text, color) if err := b.hoverTexture.Update(textUpdate(ctx.Renderer(), font, color, text)); err != nil {
if err != nil {
return err return err
} }
if b.hoverTexture != nil { if err := b.priceTexture.Update(textUpdate(ctx.Renderer(), font, color, FmtMoney(b.Flower.BuyPrice))); err != nil {
b.hoverTexture.Destroy()
}
b.hoverTexture = texture
texture, err = font.Render(ctx.Renderer, FmtMoney(b.Flower.BuyPrice), color)
if err != nil {
return err return err
} }
if b.priceTexture != nil { b.Disabled = !b.Flower.Unlocked
b.priceTexture.Destroy() b.upToDate = true
}
b.priceTexture = texture
return nil return nil
} }
func (b *BuyFlowerButton) Init(ctx *Context) error { func (b *BuyFlowerButton) Handle(ctx ui.Context, e ui.Event) bool {
return b.updateTexts(ctx) b.updateTexts(ctx)
} b.IconButton.Handle(ctx, e)
if b.IsOver() && !b.hoverAnimation.IsActive() {
func (b *BuyFlowerButton) Handle(ctx *Context, event sdl.Event) bool { b.hoverAnimation.Interval = 10 * time.Millisecond
if b.IconButton.Handle(ctx, event) { b.hoverAnimation.Start()
return true b.hoverOffset = b.priceTexture.Width()
} } else if !b.IsOver() {
if b.IsMouseOver && b.hoverAnimation == nil { b.hoverAnimation.Pause()
b.hoverAnimation = NewAnimationPtr(10 * time.Millisecond)
b.hoverOffset = b.priceTexture.Size().X
} else if !b.IsMouseOver {
b.hoverAnimation = nil
} }
return false return false
} }
func (b *BuyFlowerButton) Render(ctx *Context) { func (b *BuyFlowerButton) Render(ctx ui.Context) {
iconTexture := b.activeTexture(ctx)
pos := Pt(b.Bounds.X, b.Bounds.Y)
iconTexture.CopyResize(ctx.Renderer, Rect(pos.X, pos.Y-60, b.Bounds.W, 120))
if (b.IsMouseOver && !b.IsDisabled) || b.IsActive {
SetDrawColor(ctx.Renderer, TransparentWhite)
ctx.Renderer.FillRect(b.Bounds.SDLPtr())
}
if b.hoverAnimation != nil {
b.hoverAnimation.AnimateFn(b.animate)
}
if b.IsMouseOver {
left := b.Bounds.W - 8 - b.hoverOffset
top := pos.Y + b.Bounds.H - 20
if left < 0 {
part := RectAbs(-left, 0, b.hoverTexture.Size().X, b.hoverTexture.Size().Y)
b.hoverTexture.CopyPart(ctx.Renderer, part, Pt(pos.X, top))
} else {
b.hoverTexture.Copy(ctx.Renderer, Pt(pos.X+left, top))
}
} else {
b.priceTexture.Copy(ctx.Renderer, Pt(pos.X+b.Bounds.W-8-b.priceTexture.Size().X, pos.Y+b.Bounds.H-20))
}
}
func (b *BuyFlowerButton) Update(ctx *Context, desc FlowerDescriptor) {
b.Flower = desc
b.updateTexts(ctx) b.updateTexts(ctx)
bounds := b.Bounds()
pos := bounds.Min
icon := ctx.Textures().Texture(b.Icon)
if !b.Flower.Unlocked {
disabled := ctx.Textures().Texture(b.IconDisabled)
if disabled != nil {
icon = disabled
}
}
ctx.Renderer().DrawTexture(icon, geom.RectRelF32(pos.X, pos.Y-60, bounds.Dx(), 120))
b.RenderActive(ctx)
b.hoverAnimation.AnimateFn(b.animate)
if b.IsOver() {
left := bounds.Dx() - 8 - b.hoverOffset
top := pos.Y + bounds.Dy() - 20
if left < 0 {
part := geom.RectF32(-left, 0, b.hoverTexture.Width(), b.hoverTexture.Height())
ctx.Renderer().DrawTexturePointOptions(b.hoverTexture.Value, geom.PtF32(pos.X, top), ui.DrawOptions{Source: &part})
} else {
ctx.Renderer().DrawTexturePoint(b.hoverTexture.Value, geom.PtF32(pos.X+left, top))
}
} else {
ctx.Renderer().DrawTexturePoint(b.priceTexture.Value, geom.PtF32(pos.X+bounds.Dx()-8-b.priceTexture.Width(), pos.Y+bounds.Dy()-20))
}
}
func (b *BuyFlowerButton) Update(desc FlowerDescriptor) {
b.Flower = desc
b.upToDate = false
} }

View File

@ -2,11 +2,16 @@ package main
import ( import (
"flag" "flag"
"image/color"
"log" "log"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/addons/riceres"
"opslag.de/schobers/zntg/play"
"opslag.de/schobers/zntg/ui"
rice "github.com/GeertJohan/go.rice" rice "github.com/GeertJohan/go.rice"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"opslag.de/schobers/tins2020" "opslag.de/schobers/tins2020"
) )
@ -20,10 +25,90 @@ func main() {
} }
} }
func logSDLVersion() { func openResources(box *rice.Box) ui.Resources {
var version sdl.Version embedded := riceres.New(box)
sdl.GetVersion(&version) return ui.NewFallbackResources(ui.NewPathResources(nil, box.Name()), embedded)
log.Printf("SDL version: %d.%d.%d", version.Major, version.Minor, version.Patch) }
type app struct {
ui.Proxy
settings *tins2020.Settings
game *tins2020.Game
dialogs *tins2020.Dialogs
}
type fontDescriptor struct {
Name string
Path string
Size int
}
func (a *app) loadFonts(ctx ui.Context, descriptors ...fontDescriptor) error {
fonts := ctx.Fonts()
for _, desc := range descriptors {
_, err := fonts.CreateFontPath(desc.Name, desc.Path, desc.Size)
if err != nil {
return err
}
}
return nil
}
func (a *app) Init(ctx ui.Context) error {
if err := a.loadFonts(ctx,
fontDescriptor{"balance", "fonts/OpenSans-Bold.ttf", 40},
fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12},
fontDescriptor{"default", "fonts/OpenSans-Regular.ttf", 14},
fontDescriptor{"small", "fonts/OpenSans-Regular.ttf", 12},
fontDescriptor{"title", "fonts/OpenSans-Bold.ttf", 40},
); err != nil {
return err
}
textureLoader := tins2020.NewResourceLoader()
textures := ctx.Textures()
if err := textureLoader.LoadFromFile(ctx.Resources(), "textures.txt", func(name, content string) error {
_, err := textures.CreateTexturePath(name, content, true)
return err
}); err != nil {
return err
}
ctx.Overlays().AddOnTop("fps", &play.FPS{}, false)
content := tins2020.NewContent(a.dialogs)
content.AddChild(tins2020.NewTerrainRenderer(a.game))
controls := tins2020.NewGameControls(a.game, a.dialogs)
content.AddChild(controls)
a.Content = content
a.dialogs.Init(ctx)
a.game.Reset(ctx)
controls.Init(ctx)
a.dialogs.ShowIntro(ctx)
return nil
}
func (a *app) Handle(ctx ui.Context, e ui.Event) bool {
switch e := e.(type) {
case *ui.DisplayMoveEvent:
location := e.Bounds.Min.ToInt()
a.settings.Window.Location = &location
case *ui.DisplayResizeEvent:
a.Arrange(ctx, e.Bounds, geom.ZeroPtF32, nil)
size := e.Bounds.Size().ToInt()
a.settings.Window.Size = &size
}
return a.Proxy.Handle(ctx, e)
}
func (a *app) Render(ctx ui.Context) {
a.game.Update()
ctx.Renderer().Clear(color.White)
a.Proxy.Render(ctx)
} }
func run() error { func run() error {
@ -31,135 +116,67 @@ func run() error {
flag.BoolVar(&extract, "extract-resources", false, "extracts all resources to the current working directory") flag.BoolVar(&extract, "extract-resources", false, "extracts all resources to the current working directory")
flag.Parse() flag.Parse()
ctx, err := tins2020.NewContext(rice.MustFindBox("res")) box := rice.MustFindBox(`res`)
if err != nil {
return err
}
defer ctx.Destroy()
if extract { if extract {
return copyBoxToDisk(ctx.Resources.Box()) return copyBoxToDisk(box)
}
res := openResources(box)
ptPtr := func(x, y int) *geom.Point {
p := geom.Pt(x, y)
return &p
} }
if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil { settings := &tins2020.Settings{}
err := settings.Init()
if err != nil {
return err return err
} }
defer sdl.Quit() defer settings.Store()
logSDLVersion() var location *geom.PointF32
if settings.Window.Location != nil {
if err := ttf.Init(); err != nil { location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)}
return err
} }
defer ttf.Quit() if settings.Window.Size == nil {
settings.Window.Size = ptPtr(800, 600)
if ctx.Settings.Window.Location == nil {
ctx.Settings.Window.Location = tins2020.PtPtr(sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED)
} }
if ctx.Settings.Window.Size == nil { if settings.Window.VSync == nil {
ctx.Settings.Window.Size = tins2020.PtPtr(800, 600)
}
if ctx.Settings.Window.VSync == nil {
vsync := true vsync := true
ctx.Settings.Window.VSync = &vsync settings.Window.VSync = &vsync
} }
renderer, err := ui.NewRenderer("Botanim - TINS 2020", settings.Window.Size.X, settings.Window.Size.Y, ui.NewRendererOptions{
if *ctx.Settings.Window.VSync { Location: location,
sdl.SetHint(sdl.HINT_RENDER_VSYNC, "1") Resizable: true,
} VSync: *settings.Window.VSync,
sdl.SetHint(sdl.HINT_RENDER_SCALE_QUALITY, "1")
window, err := sdl.CreateWindow("Botanim - TINS 2020",
ctx.Settings.Window.Location.X, ctx.Settings.Window.Location.Y,
ctx.Settings.Window.Size.X, ctx.Settings.Window.Size.Y,
sdl.WINDOW_SHOWN|sdl.WINDOW_RESIZABLE)
if err != nil {
return err
}
defer window.Destroy()
renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED)
if err != nil {
return err
}
defer renderer.Destroy()
ctx.Init(renderer)
err = ctx.Fonts.LoadDesc(
tins2020.FontDescriptor{Name: "balance", Path: "fonts/OpenSans-Bold.ttf", Size: 40},
tins2020.FontDescriptor{Name: "debug", Path: "fonts/FiraMono-Regular.ttf", Size: 12},
tins2020.FontDescriptor{Name: "default", Path: "fonts/OpenSans-Regular.ttf", Size: 14},
tins2020.FontDescriptor{Name: "small", Path: "fonts/OpenSans-Regular.ttf", Size: 12},
tins2020.FontDescriptor{Name: "title", Path: "fonts/OpenSans-Bold.ttf", Size: 40},
)
if err != nil {
return err
}
textureLoader := tins2020.NewResourceLoader()
err = textureLoader.LoadFromFile(&ctx.Resources, "textures.txt", func(name, content string) error {
return ctx.Textures.Load(name, content)
}) })
if err != nil { if err != nil {
return err return err
} }
defer renderer.Destroy()
renderer.SetResourceProvider(func() ui.Resources { return res })
game := tins2020.NewGame() game := tins2020.NewGame()
app := &app{
app := tins2020.NewContainer() game: game,
dialogs: tins2020.NewDialogs(game),
overlays := tins2020.NewContainer() settings: settings,
dialogs := tins2020.NewDialogs(game)
overlays.AddChild(dialogs)
overlays.AddChild(&tins2020.FPS{Show: &game.Debug})
content := tins2020.NewContent(dialogs)
content.AddChild(tins2020.NewTerrainRenderer(game))
content.AddChild(tins2020.NewGameControls(game, dialogs))
app.AddChild(content)
app.AddChild(overlays)
err = app.Init(ctx)
if err != nil {
return err
} }
dialogs.ShowIntro(ctx) style := ui.DefaultStyle()
style.Palette = &ui.Palette{
w, h := window.GetSize() Background: zntg.MustHexColor(`#356DAD`),
app.Arrange(ctx, tins2020.RectAbs(0, 0, w, h)) Disabled: zntg.MustHexColor(`#DEDEDE`),
Primary: zntg.MustHexColor(`#356DAD`),
for { PrimaryDark: zntg.MustHexColor(`#15569F`),
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { PrimaryLight: zntg.MustHexColor(`#ABCAED`),
switch e := event.(type) { Secondary: zntg.MustHexColor(`#4AC69A`),
case *sdl.QuitEvent: SecondaryDark: zntg.MustHexColor(`#0AA36D`),
ctx.Quit() SecondaryLight: zntg.MustHexColor(`#A6EED4`),
break Text: color.White,
case *sdl.WindowEvent: TextOnPrimary: color.White,
switch e.Event { TextOnSecondary: color.White,
case sdl.WINDOWEVENT_MOVED: TextNegative: zntg.MustHexColor(`#F3590E`),
x, y := window.GetPosition() TextPositive: zntg.MustHexColor(`#65D80D`),
ctx.Settings.Window.Location = tins2020.PtPtr(x, y)
case sdl.WINDOWEVENT_SIZE_CHANGED:
w, h := window.GetSize()
app.Arrange(ctx, tins2020.RectAbs(0, 0, w, h))
ctx.Settings.Window.Size = tins2020.PtPtr(w, h)
} }
case *sdl.MouseMotionEvent: return ui.Run(renderer, style, app)
ctx.MousePosition = tins2020.Pt(e.X, e.Y)
}
app.Handle(ctx, event)
}
game.Update()
if ctx.ShouldQuit {
break
}
renderer.SetDrawColor(0, 0, 0, 255)
renderer.Clear()
app.Render(ctx)
renderer.Present()
}
return nil
} }

View File

@ -0,0 +1,16 @@
// +build allegro
package main
import (
"log"
_ "opslag.de/schobers/zntg/allg5ui" // rendering backend
)
// #cgo windows,allegro LDFLAGS: -Wl,-subsystem,windows
import "C"
func init() {
log.Println("Using Allegro5 rendering backend")
}

View File

@ -0,0 +1,13 @@
// +build !allegro
package main
import (
"log"
_ "opslag.de/schobers/zntg/sdlui" // SDL rendering backend
)
func init() {
log.Println("Using SDL rendering backend")
}

View File

@ -1,30 +0,0 @@
package tins2020
import (
"opslag.de/schobers/tins2020/img"
"github.com/veandco/go-sdl2/sdl"
)
var Black = sdl.Color{R: 0, G: 0, B: 0, A: 255}
var Transparent = sdl.Color{R: 0, G: 0, B: 0, A: 0}
var TransparentWhite = sdl.Color{R: 255, G: 255, B: 255, A: 31}
var White = sdl.Color{R: 255, G: 255, B: 255, A: 255}
func HexColor(s string) (sdl.Color, error) {
c, err := img.HexColor(s)
if err != nil {
return sdl.Color{}, err
}
return sdl.Color(c), nil
}
func MustHexColor(s string) sdl.Color { return sdl.Color(img.MustHexColor(s)) }
func SetDrawColor(renderer *sdl.Renderer, color sdl.Color) {
renderer.SetDrawColor(color.R, color.G, color.B, color.A)
}
func SetDrawColorHex(renderer *sdl.Renderer, s string) {
SetDrawColor(renderer, MustHexColor(s))
}

View File

@ -1,33 +0,0 @@
package tins2020
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/veandco/go-sdl2/sdl"
)
func TestHexColor(t *testing.T) {
type test struct {
Hex string
Success bool
Expected sdl.Color
}
tests := []test{
test{Hex: "#AA3939", Success: true, Expected: sdl.Color{R: 170, G: 57, B: 57, A: 255}},
test{Hex: "AA3939", Success: true, Expected: sdl.Color{R: 170, G: 57, B: 57, A: 255}},
test{Hex: "#AA3939BB", Success: true, Expected: sdl.Color{R: 170, G: 57, B: 57, A: 187}},
test{Hex: "AG3939", Success: false, Expected: sdl.Color{}},
test{Hex: "AA3939A", Success: false, Expected: sdl.Color{}},
test{Hex: "AA39A", Success: false, Expected: sdl.Color{}},
}
for _, test := range tests {
color, err := HexColor(test.Hex)
if test.Success {
assert.Nil(t, err)
assert.Equal(t, color, test.Expected)
} else {
assert.NotNil(t, err)
}
}
}

140
confirmationdialog.go Normal file
View File

@ -0,0 +1,140 @@
package tins2020
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
)
type confirmationDialog struct {
ui.Proxy
active string
cancel ui.Button
confirm ui.Button
question ui.Paragraph
userDecided ui.Events
}
func newConfirmationDialog(caption, question string) *confirmationDialog {
dialog := &confirmationDialog{}
dialog.active = "confirm"
dialog.cancel.Text = "Cancel"
dialog.cancel.ButtonClicked().AddHandler(func(ctx ui.Context, _ ui.ControlClickedArgs) { dialog.userCanceled(ctx) })
dialog.cancel.Type = ui.ButtonTypeText
dialog.cancel.HoverColor = zntg.MustHexColor(`FFFFFF1F`)
dialog.confirm.Text = "OK"
dialog.confirm.ButtonClicked().AddHandler(func(ctx ui.Context, _ ui.ControlClickedArgs) { dialog.userConfirmed(ctx) })
dialog.confirm.Type = ui.ButtonTypeText
dialog.confirm.HoverColor = zntg.MustHexColor(`FFFFFF1F`)
dialog.question.Text = question
dialog.updateActive()
responses := ui.BuildStackPanel(ui.OrientationHorizontal, func(p *ui.StackPanel) {
p.AddChild(ui.FixedWidth(&dialog.cancel, 160))
p.AddChild(ui.FixedWidth(&dialog.confirm, 160))
})
content := &dialogBase{}
content.Background = zntg.MustHexColor(`#0000007F`)
content.Init(caption, ui.BuildStackPanel(ui.OrientationVertical, func(p *ui.StackPanel) {
p.AddChild(&dialog.question)
p.AddChild(responses)
}))
dialog.Content = ui.Background(ui.BuildSpacing(content, func(s *ui.Spacing) {
s.Width = ui.Fixed(320)
s.Center()
}), zntg.MustHexColor(`#0000007F`))
return dialog
}
func (d *confirmationDialog) toggleActive() {
if d.active == "confirm" {
d.active = "cancel"
} else {
d.active = "confirm"
}
d.updateActive()
}
func (d *confirmationDialog) updateActive() {
d.cancel.Background = nil
d.confirm.Background = nil
switch d.active {
case "cancel":
d.cancel.Background = hoverTransparentColor
case "confirm":
d.confirm.Background = hoverTransparentColor
}
}
func (d *confirmationDialog) userCanceled(ctx ui.Context) {
d.userDecided.Notify(ctx, false)
}
func (d *confirmationDialog) userConfirmed(ctx ui.Context) {
d.userDecided.Notify(ctx, true)
}
func (d *confirmationDialog) Handle(ctx ui.Context, e ui.Event) bool {
if d.Proxy.Handle(ctx, e) {
return true
}
switch e := e.(type) {
case *ui.MouseButtonDownEvent:
if e.Button == ui.MouseButtonRight {
d.userCanceled(ctx)
return true
}
case *ui.MouseMoveEvent:
if d.cancel.IsOver() {
d.active = "cancel"
}
if d.confirm.IsOver() {
d.active = "confirm"
}
d.updateActive()
case *ui.KeyDownEvent:
switch e.Key {
case ui.KeyEscape:
d.userCanceled(ctx)
return true
case ui.KeyEnter:
switch d.active {
case "cancel":
d.userCanceled(ctx)
case "confirm":
d.userConfirmed(ctx)
}
return true
case ui.KeyLeft:
d.toggleActive()
case ui.KeyRight:
d.toggleActive()
case ui.KeyTab:
d.toggleActive()
}
}
return false
}
type dialogBase struct {
ui.StackPanel
caption ui.Label
content ui.Proxy
}
func (d *dialogBase) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 {
return d.StackPanel.DesiredSize(ctx, size)
}
func (d *dialogBase) Init(caption string, content ui.Control) {
d.caption.Text = caption
d.content.Content = content
d.Children = []ui.Control{&d.caption, &d.content}
}

View File

@ -1,58 +0,0 @@
package tins2020
import (
"github.com/veandco/go-sdl2/sdl"
)
type Container struct {
ControlBase
Children []Control
}
func NewContainer() *Container {
return &Container{}
}
func (c *Container) AddChild(child Control) {
c.Children = append(c.Children, child)
}
func (c *Container) Arrange(ctx *Context, bounds Rectangle) {
c.ControlBase.Arrange(ctx, bounds)
for _, child := range c.Children {
child.Arrange(ctx, bounds)
}
}
func (c *Container) Handle(ctx *Context, event sdl.Event) bool {
if c.ControlBase.Handle(ctx, event) {
return true
}
for _, child := range c.Children {
if child.Handle(ctx, event) {
return true
}
}
return false
}
func (c *Container) Init(ctx *Context) error {
c.ControlBase.Init(ctx)
for _, child := range c.Children {
err := child.Init(ctx)
if err != nil {
return err
}
}
return nil
}
func (c *Container) Render(ctx *Context) {
c.ControlBase.Render(ctx)
for _, child := range c.Children {
child.Render(ctx)
}
}

View File

@ -1,28 +1,34 @@
package tins2020 package tins2020
import "github.com/veandco/go-sdl2/sdl" import (
"opslag.de/schobers/zntg/ui"
)
// Content shortcuts events when a dialog is opened. // Content shortcuts events when a dialog is opened.
type Content struct { type Content struct {
Container ui.Proxy
dialogOverlayed bool content ui.ContainerBase
shortcut bool
} }
func NewContent(dialogs *Dialogs) *Content { func NewContent(dialogs *Dialogs) *Content {
content := &Content{} content := &Content{}
dialogs.DialogOpened().Register(func() { content.Proxy.Content = &content.content
content.dialogOverlayed = true dialogs.DialogOpened().AddHandlerEmpty(func(ui.Context) {
content.shortcut = true
}) })
dialogs.DialogClosed().Register(func() { dialogs.DialogClosed().AddHandlerEmpty(func(ui.Context) {
content.dialogOverlayed = false content.shortcut = false
}) })
return content return content
} }
func (c *Content) Handle(ctx *Context, event sdl.Event) bool { func (c *Content) AddChild(child ui.Control) { c.content.AddChild(child) }
if c.dialogOverlayed {
func (c *Content) Handle(ctx ui.Context, event ui.Event) bool {
if c.shortcut {
return false return false
} }
return c.Container.Handle(ctx, event) return c.Proxy.Handle(ctx, event)
} }

View File

@ -1,46 +0,0 @@
package tins2020
import (
rice "github.com/GeertJohan/go.rice"
"github.com/veandco/go-sdl2/sdl"
)
type Context struct {
Renderer *sdl.Renderer
Fonts Fonts
Resources Resources
Textures Textures
Settings Settings
MousePosition Point
ShouldQuit bool
}
func NewContext(res *rice.Box) (*Context, error) {
ctx := &Context{}
err := ctx.Settings.Init()
if err != nil {
return nil, err
}
err = ctx.Resources.Open(res)
if err != nil {
return nil, err
}
return ctx, nil
}
func (c *Context) Destroy() {
c.Fonts.Destroy()
c.Resources.Destroy()
c.Textures.Destroy()
c.Settings.Store()
}
func (c *Context) Init(renderer *sdl.Renderer) {
c.Renderer = renderer
c.Fonts.Init(c.Resources.Copy())
c.Textures.Init(renderer, c.Resources.Copy())
c.Renderer.SetDrawBlendMode(sdl.BLENDMODE_BLEND)
}
func (c *Context) Quit() { c.ShouldQuit = true }

View File

@ -1,82 +0,0 @@
package tins2020
import "github.com/veandco/go-sdl2/sdl"
type Control interface {
Init(*Context) error
Arrange(*Context, Rectangle)
Handle(*Context, sdl.Event) bool
Render(*Context)
}
type EventContextFn func(*Context)
type EventFn func()
type EventInterfaceFn func(interface{})
func EmptyEvent(fn EventFn) EventContextFn {
return func(*Context) { fn() }
}
type ControlBase struct {
Bounds Rectangle
FontName string
Foreground sdl.Color
IsDisabled bool
IsMouseOver bool
OnLeftMouseButtonClick EventContextFn
}
func (c *ControlBase) ActualForeground() sdl.Color {
var none sdl.Color
if c.Foreground == none {
return MustHexColor("#ffffff")
}
return c.Foreground
}
func (c *ControlBase) ActualFont(ctx *Context) *Font {
name := c.ActualFontName()
return ctx.Fonts.Font(name)
}
func (c *ControlBase) ActualFontName() string {
if c.FontName == "" {
return "default"
}
return c.FontName
}
func (b *ControlBase) Arrange(ctx *Context, bounds Rectangle) { b.Bounds = bounds }
func (b *ControlBase) Init(*Context) error { return nil }
func (b *ControlBase) Handle(ctx *Context, event sdl.Event) bool {
switch e := event.(type) {
case *sdl.MouseMotionEvent:
b.IsMouseOver = b.Bounds.IsPointInside(e.X, e.Y)
case *sdl.MouseButtonEvent:
if !b.IsDisabled && b.IsMouseOver && e.Button == sdl.BUTTON_LEFT && e.Type == sdl.MOUSEBUTTONDOWN {
return b.Invoke(ctx, b.OnLeftMouseButtonClick)
}
case *sdl.WindowEvent:
if e.Event == sdl.WINDOWEVENT_LEAVE {
b.IsMouseOver = false
}
}
return false
}
func (b *ControlBase) Invoke(ctx *Context, fn EventContextFn) bool {
if fn == nil {
return false
}
fn(ctx)
return true
}
func (b *ControlBase) Render(*Context) {}

View File

@ -1,42 +0,0 @@
package tins2020_test
import (
"math/rand"
"testing"
)
type native struct{ a, b, c, d int }
type similar struct{ a, b, c, d int }
func (s similar) toNative() native { return native{s.a, s.b, s.c, s.d} }
type wrapper struct{ native }
func (w wrapper) toNative() native { return w.native }
func nativeFunction(n native) int { return n.a + n.b + n.c + n.d }
func BenchmarkNative(b *testing.B) {
var sum int
for i := 0; i < b.N; i++ {
n := native{rand.Int(), rand.Int(), rand.Int(), rand.Int()}
sum += nativeFunction(n)
}
}
func BenchmarkSimilar(b *testing.B) {
var sum int
for i := 0; i < b.N; i++ {
s := similar{rand.Int(), rand.Int(), rand.Int(), rand.Int()}
sum += nativeFunction(s.toNative())
}
}
func BenchmarkWrapper(b *testing.B) {
var sum int
for i := 0; i < b.N; i++ {
w := wrapper{native{rand.Int(), rand.Int(), rand.Int(), rand.Int()}}
sum += nativeFunction(w.toNative())
}
}

137
dial.go Normal file
View File

@ -0,0 +1,137 @@
package tins2020
import (
"math"
"strconv"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
)
type Dial struct {
ui.ContainerBase
dialer Dialer
typing string // current digit
digitCount int // number of times the digit is pressed
digits []DialDigit // digits
}
func NewDial(dialer Dialer) *Dial {
dial := &Dial{dialer: dialer}
dial.digits = make([]DialDigit, 10)
for i := range dial.digits {
j := i
dial.digits[i].Value = strconv.Itoa(i)
dial.digits[i].ControlClicked().AddHandler(func(ctx ui.Context, _ ui.ControlClickedArgs) {
dial.userTyped(ctx, j)
})
dial.AddChild(&dial.digits[i])
}
return dial
}
func (d *Dial) userTyped(ctx ui.Context, i int) {
d.digits[i].Blink()
digit := strconv.Itoa(i)
if len(d.typing) == 0 || digit != d.typing {
d.typing = digit
d.digitCount = 1
} else {
d.digitCount++
}
if !d.dialer.CanUserType(i) {
d.typing = ""
d.digitCount = 0
d.dialer.UserGaveWrongInput()
} else if d.digitCount == i || d.digitCount == 10 {
d.typing = ""
d.digitCount = 0
d.dialer.UserTyped(ctx, i)
}
}
func (d *Dial) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) {
d.ControlBase.Arrange(ctx, bounds, offset, parent)
center := bounds.Center()
size := bounds.Size()
distance := size.Y * .3
for i := range d.digits {
angle := (float32((10-i)%10)*0.16 + .2) * math.Pi
pos := geom.PtF32(distance*geom.Cos32(angle), .8*distance*geom.Sin32(angle))
digitCenter := center.Add(pos)
d.digits[i].Arrange(ctx, geom.RectRelF32(digitCenter.X-24, digitCenter.Y-24, 48, 48), offset, d)
}
}
func (d *Dial) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 {
return geom.PtF32(size.X, geom.NaN32())
}
func (d *Dial) Handle(ctx ui.Context, event ui.Event) bool {
if d.ContainerBase.Handle(ctx, event) {
return true
}
switch e := event.(type) {
case *ui.KeyDownEvent:
switch e.Key {
case ui.Key0:
d.userTyped(ctx, 0)
case ui.KeyPad0:
d.userTyped(ctx, 0)
case ui.Key1:
d.userTyped(ctx, 1)
case ui.KeyPad1:
d.userTyped(ctx, 1)
case ui.Key2:
d.userTyped(ctx, 2)
case ui.KeyPad2:
d.userTyped(ctx, 2)
case ui.Key3:
d.userTyped(ctx, 3)
case ui.KeyPad3:
d.userTyped(ctx, 3)
case ui.Key4:
d.userTyped(ctx, 4)
case ui.KeyPad4:
d.userTyped(ctx, 4)
case ui.Key5:
d.userTyped(ctx, 5)
case ui.KeyPad5:
d.userTyped(ctx, 5)
case ui.Key6:
d.userTyped(ctx, 6)
case ui.KeyPad6:
d.userTyped(ctx, 6)
case ui.Key7:
d.userTyped(ctx, 7)
case ui.KeyPad7:
d.userTyped(ctx, 7)
case ui.Key8:
d.userTyped(ctx, 8)
case ui.KeyPad8:
d.userTyped(ctx, 8)
case ui.Key9:
d.userTyped(ctx, 9)
case ui.KeyPad9:
d.userTyped(ctx, 9)
}
}
return false
}
func (d *Dial) Reset() {
d.typing = ""
d.digitCount = 0
}
func (d *Dial) Tick() {
for i := range d.digits {
d.digits[i].Tick()
}
}

34
dialdigit.go Normal file
View File

@ -0,0 +1,34 @@
package tins2020
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
)
type DialDigit struct {
ui.ControlBase
Value string
highlight int
}
func (d *DialDigit) Blink() {
d.highlight = 4
}
func (d *DialDigit) Render(ctx ui.Context) {
color := zntg.MustHexColor(`#FFFFFF`)
if d.highlight > 0 {
color = zntg.MustHexColor(`#15569F`)
}
bounds := d.Bounds()
ctx.Fonts().TextAlign("title", geom.PtF32(bounds.Center().X, bounds.Min.Y), color, d.Value, ui.AlignCenter)
}
func (d *DialDigit) Tick() {
if d.highlight > 0 {
d.highlight--
}
}

View File

@ -1,66 +1,98 @@
package tins2020 package tins2020
import (
"opslag.de/schobers/zntg/ui"
)
type Dialogs struct { type Dialogs struct {
Proxy ui.Proxy
intro Control intro ui.Overlay
settings Control research ui.Overlay
research Control settings ui.Overlay
nothing ui.Control
dialogClosed *Events closed ui.Events
dialogOpened *Events opened ui.Events
} }
const dialogsOverlayName = "dialogs"
func NewDialogs(game *Game) *Dialogs { func NewDialogs(game *Game) *Dialogs {
return &Dialogs{ intro := NewIntro()
intro: &Intro{}, research := NewResearch(game)
settings: &LargeDialog{}, settings := NewLargeDialog("Settings", &ui.Label{})
research: NewResearch(game),
dialogClosed: NewEvents(), dialogs := &Dialogs{
dialogOpened: NewEvents(), intro: intro,
settings: settings,
research: research,
nothing: &ui.ControlBase{},
}
intro.CloseRequested().AddHandlerEmpty(dialogs.Close)
research.CloseRequested().AddHandlerEmpty(dialogs.Close)
settings.CloseRequested().AddHandlerEmpty(dialogs.Close)
return dialogs
}
func (d *Dialogs) Init(ctx ui.Context) {
overlays := ctx.Overlays()
overlays.AddOnTop(dialogsOverlayName, d, false)
d.Content = d.nothing
}
func (d *Dialogs) showDialog(ctx ui.Context, control ui.Control) {
if control == nil {
ctx.Overlays().Hide(dialogsOverlayName)
d.closed.Notify(ctx, control)
d.Content = d.nothing
} else {
d.Content = control
ctx.Overlays().Show(dialogsOverlayName)
d.opened.Notify(ctx, control)
} }
} }
func (d *Dialogs) showDialog(ctx *Context, control Control) { func (d *Dialogs) Close(ctx ui.Context) {
d.SetContent(ctx, control) d.showDialog(ctx, nil)
control.(Dialog).ShowDialog(ctx, d.Close)
d.dialogOpened.Notify(nil)
} }
func (d *Dialogs) Arrange(ctx *Context, bounds Rectangle) { func (d *Dialogs) DialogClosed() ui.EventHandler { return &d.closed }
d.Proxy.Arrange(ctx, bounds) func (d *Dialogs) DialogOpened() ui.EventHandler { return &d.opened }
func (d *Dialogs) Hidden() {
d.Proxy.Hidden()
} }
func (d *Dialogs) DialogClosed() EventHandler { return d.dialogClosed } func (d *Dialogs) AskConfirmation(ctx ui.Context, caption, question string, confirm, cancel ui.EventFn) {
func (d *Dialogs) DialogOpened() EventHandler { return d.dialogOpened } dialog := newConfirmationDialog(caption, question)
dialog.userDecided.AddHandler(func(ctx ui.Context, state interface{}) {
func (d *Dialogs) Init(ctx *Context) error { decision := state.(bool)
err := d.intro.Init(ctx) if decision {
if err != nil { confirm(ctx, nil)
return err } else {
cancel(ctx, nil)
} }
err = d.settings.Init(ctx) d.Close(ctx)
if err != nil { })
return err d.showDialog(ctx, dialog)
}
err = d.research.Init(ctx)
return nil
} }
func (d *Dialogs) Close() { func (d *Dialogs) ShowIntro(ctx ui.Context) {
d.SetContent(nil, nil)
d.dialogClosed.Notify(nil)
}
func (d *Dialogs) ShowIntro(ctx *Context) {
d.showDialog(ctx, d.intro) d.showDialog(ctx, d.intro)
} }
func (d *Dialogs) ShowResearch(ctx *Context) { func (d *Dialogs) Shown() {
d.Proxy.Shown()
}
func (d *Dialogs) ShowResearch(ctx ui.Context) {
d.showDialog(ctx, d.research) d.showDialog(ctx, d.research)
} }
func (d *Dialogs) ShowSettings(ctx *Context) { func (d *Dialogs) ShowSettings(ctx ui.Context) {
d.showDialog(ctx, d.settings) d.showDialog(ctx, d.settings)
} }

View File

@ -1,24 +0,0 @@
package tins2020
type Drageable struct {
start *Point
dragged bool
}
func (d *Drageable) Cancel() { d.start = nil }
func (d *Drageable) IsDragging() bool { return d.start != nil }
func (d *Drageable) HasDragged() bool { return d.dragged }
func (d *Drageable) Move(p Point) Point {
delta := p.Sub(*d.start)
d.start = &p
d.dragged = true
return delta
}
func (d *Drageable) Start(p Point) {
d.start = &p
d.dragged = false
}

View File

@ -1,33 +0,0 @@
package tins2020
func NewEvents() *Events {
return &Events{events: map[int]EventInterfaceFn{}}
}
type Events struct {
nextID int
events map[int]EventInterfaceFn
}
type EventHandler interface {
Register(EventFn) int
RegisterItf(EventInterfaceFn) int
Unregister(int)
}
func (h *Events) Notify(state interface{}) {
for _, event := range h.events {
event(state)
}
}
func (h *Events) Register(fn EventFn) int { return h.RegisterItf(func(interface{}) { fn() }) }
func (h *Events) RegisterItf(fn EventInterfaceFn) int {
id := h.nextID
h.nextID++
h.events[id] = fn
return id
}
func (h *Events) Unregister(id int) { delete(h.events, id) }

View File

@ -64,8 +64,8 @@ func NewConeflowerTraits() FlowerTraits {
Spread: 0.0005, Spread: 0.0005,
Life: 0.99993, Life: 0.99993,
Resistance: FlowerResistance{ Resistance: FlowerResistance{
Cold: 0.7, Cold: 0.6,
Hot: 0.8, Hot: 0.9,
Dry: 0.8, Dry: 0.8,
Wet: 0.5, Wet: 0.5,
}, },

109
fonts.go
View File

@ -1,109 +0,0 @@
package tins2020
import (
"fmt"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"opslag.de/schobers/fs/vfs"
)
type TextAlignment int
const (
TextAlignmentLeft TextAlignment = iota
TextAlignmentCenter
TextAlignmentRight
)
type Font struct {
*ttf.Font
}
func (f *Font) Render(renderer *sdl.Renderer, text string, color sdl.Color) (*Texture, error) {
surface, err := f.RenderUTF8Blended(text, color)
if err != nil {
return nil, err
}
defer surface.Free()
texture, err := NewTextureFromSurface(renderer, surface)
if err != nil {
return nil, err
}
return texture, nil
}
func (f *Font) RenderCopyAlign(renderer *sdl.Renderer, text string, pos Point, color sdl.Color, align TextAlignment) error {
texture, err := f.Render(renderer, text, color)
if err != nil {
return err
}
defer texture.Destroy()
size := texture.Size()
switch align {
case TextAlignmentLeft:
texture.Copy(renderer, Pt(pos.X, pos.Y-size.Y))
case TextAlignmentCenter:
texture.Copy(renderer, Pt(pos.X-(size.X/2), pos.Y-size.Y))
case TextAlignmentRight:
texture.Copy(renderer, Pt(pos.X-size.X, pos.Y-size.Y))
}
return nil
}
func (f *Font) RenderCopy(renderer *sdl.Renderer, text string, pos Point, color sdl.Color) error {
return f.RenderCopyAlign(renderer, text, pos, color, TextAlignmentLeft)
}
type Fonts struct {
dir vfs.CopyDir
fonts map[string]*Font
}
func (f *Fonts) Init(dir vfs.CopyDir) {
f.dir = dir
f.fonts = map[string]*Font{}
}
func (f *Fonts) Font(name string) *Font { return f.fonts[name] }
type FontDescriptor struct {
Name string
Path string
Size int
}
func (f *Fonts) LoadDesc(fonts ...FontDescriptor) error {
for _, desc := range fonts {
err := f.Load(desc.Name, desc.Path, desc.Size)
if err != nil {
return fmt.Errorf("error loading '%s'; error: %v", desc.Name, err)
}
}
return nil
}
func (f *Fonts) Load(name, path string, size int) error {
fontPath, err := f.dir.Retrieve(path)
if err != nil {
return err
}
font, err := ttf.OpenFont(fontPath, size)
if err != nil {
return err
}
if font, ok := f.fonts[name]; ok {
font.Close()
}
f.fonts[name] = &Font{font}
return nil
}
func (f *Fonts) Destroy() {
if f.fonts == nil {
return
}
for _, f := range f.fonts {
f.Close()
}
}

View File

@ -1,46 +0,0 @@
package tins2020
import (
"fmt"
"time"
"github.com/veandco/go-sdl2/sdl"
)
type FPS struct {
ControlBase
Show *bool
start time.Time
stamp time.Duration
slot int
ticks []int
total int
}
func (f *FPS) Init(*Context) error {
f.start = time.Now()
f.stamp = 0
f.ticks = make([]int, 51)
return nil
}
func (f *FPS) Render(ctx *Context) {
if f.Show == nil || !*f.Show {
return
}
elapsed := time.Since(f.start)
stamp := elapsed / (20 * time.Millisecond)
for f.stamp < stamp {
f.total += f.ticks[f.slot]
f.slot = (f.slot + 1) % len(f.ticks)
f.total -= f.ticks[f.slot]
f.ticks[f.slot] = 0
f.stamp++
}
f.ticks[f.slot]++
font := ctx.Fonts.Font("debug")
font.RenderCopy(ctx.Renderer, fmt.Sprintf("FPS: %d", f.total), Pt(5, 17), sdl.Color{R: 255, G: 255, B: 255, A: 255})
}

123
game.go
View File

@ -4,6 +4,10 @@ import (
"log" "log"
"math/rand" "math/rand"
"time" "time"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
) )
type Game struct { type Game struct {
@ -16,10 +20,10 @@ type Game struct {
Terrain *Map Terrain *Map
tool Tool tool Tool
centerChanged *Events centerChanged ui.Events
toolChanged *Events toolChanged ui.Events
speedChanged *Events speedChanged ui.Events
simulation Animation simulation zntg.Animation
} }
type GameSpeed string type GameSpeed string
@ -35,21 +39,17 @@ const fastSimulationInterval = 20 * time.Millisecond
func NewGame() *Game { func NewGame() *Game {
game := &Game{ game := &Game{
centerChanged: NewEvents(), simulation: zntg.Animation{Interval: time.Millisecond * 10},
speedChanged: NewEvents(),
toolChanged: NewEvents(),
simulation: NewAnimation(time.Millisecond * 10),
} }
game.Reset()
return game return game
} }
func (g *Game) selectTool(t Tool) { func (g *Game) selectTool(ctx ui.Context, t Tool) {
g.tool = t g.tool = t
g.toolChanged.Notify(t) g.toolChanged.Notify(ctx, t)
} }
func (g *Game) setSpeed(speed GameSpeed) { func (g *Game) setSpeed(ctx ui.Context, speed GameSpeed) {
if speed == g.Speed { if speed == g.Speed {
return return
} }
@ -57,35 +57,35 @@ func (g *Game) setSpeed(speed GameSpeed) {
g.SpeedBeforePause = g.Speed g.SpeedBeforePause = g.Speed
} }
g.Speed = speed g.Speed = speed
g.speedChanged.Notify(speed) g.speedChanged.Notify(ctx, speed)
switch speed { switch speed {
case GameSpeedPaused: case GameSpeedPaused:
g.simulation.Pause() g.simulation.Pause()
case GameSpeedNormal: case GameSpeedNormal:
g.simulation.SetInterval(simulationInterval) g.simulation.Interval = simulationInterval
g.simulation.Run() g.simulation.Start()
case GameSpeedFast: case GameSpeedFast:
g.simulation.SetInterval(fastSimulationInterval) g.simulation.Interval = fastSimulationInterval
g.simulation.Run() g.simulation.Start()
} }
} }
func (g *Game) tick() { func (g *Game) tick() {
randomNeighbor := func(pos Point) Point { randomNeighbor := func(pos geom.Point) geom.Point {
switch rand.Intn(4) { switch rand.Intn(4) {
case 0: case 0:
return Pt(pos.X-1, pos.Y) return geom.Pt(pos.X-1, pos.Y)
case 1: case 1:
return Pt(pos.X, pos.Y-1) return geom.Pt(pos.X, pos.Y-1)
case 2: case 2:
return Pt(pos.X+1, pos.Y) return geom.Pt(pos.X+1, pos.Y)
case 3: case 3:
return Pt(pos.X, pos.Y+1) return geom.Pt(pos.X, pos.Y+1)
} }
return pos return pos
} }
flowers := map[Point]Flower{} flowers := map[geom.Point]Flower{}
for pos, flower := range g.Terrain.Flowers { for pos, flower := range g.Terrain.Flowers {
if rand.Float32() < flower.Traits.Spread { if rand.Float32() < flower.Traits.Spread {
dst := randomNeighbor(pos) dst := randomNeighbor(pos)
@ -102,13 +102,13 @@ func (g *Game) tick() {
g.Terrain.Flowers = flowers g.Terrain.Flowers = flowers
} }
func (g *Game) CancelTool() { func (g *Game) CancelTool(ctx ui.Context) {
g.selectTool(nil) g.selectTool(ctx, nil)
} }
func (g *Game) CenterChanged() EventHandler { return g.centerChanged } func (g *Game) CenterChanged() ui.EventHandler { return &g.centerChanged }
func (g *Game) Dig(tile Point) { func (g *Game) Dig(tile geom.Point) {
id := g.Terrain.DigFlower(tile) id := g.Terrain.DigFlower(tile)
desc, ok := g.Herbarium.Find(id) desc, ok := g.Herbarium.Find(id)
if !ok { if !ok {
@ -125,14 +125,14 @@ func (g *Game) Dig(tile Point) {
} }
} }
func (g *Game) New() { func (g *Game) New(ctx ui.Context) {
g.Pause() g.Pause(ctx)
g.Reset() g.Reset(ctx)
} }
func (g *Game) Load() { func (g *Game) Load(ctx ui.Context) {
g.CancelTool() g.CancelTool(ctx)
g.Pause() g.Pause(ctx)
var state GameState var state GameState
err := state.Deserialize(SaveGameName()) err := state.Deserialize(SaveGameName())
@ -155,22 +155,20 @@ func (g *Game) Load() {
Variant: NewRandomNoiseMap(state.Terrain.Variant), Variant: NewRandomNoiseMap(state.Terrain.Variant),
PlaceX: NewRandomNoiseMap(state.Terrain.PlaceX), PlaceX: NewRandomNoiseMap(state.Terrain.PlaceX),
PlaceY: NewRandomNoiseMap(state.Terrain.PlaceY), PlaceY: NewRandomNoiseMap(state.Terrain.PlaceY),
Flowers: map[Point]Flower{}, Flowers: map[geom.Point]Flower{},
} }
for _, flower := range state.Terrain.Flowers { for _, flower := range state.Terrain.Flowers {
desc, _ := g.Herbarium.Find(flower.ID) desc, _ := g.Herbarium.Find(flower.ID)
g.Terrain.AddFlower(flower.Location, flower.ID, desc.Traits) g.Terrain.AddFlower(flower.Location, flower.ID, desc.Traits)
} }
g.Terrain.Center = state.View.Center g.Terrain.Center = state.View.Center
g.centerChanged.Notify(g.Terrain.Center) g.centerChanged.Notify(ctx, g.Terrain.Center)
g.setSpeed(ctx, state.Speed)
g.CancelTool()
g.setSpeed(state.Speed)
} }
func (g *Game) Pause() { g.setSpeed(GameSpeedPaused) } func (g *Game) Pause(ctx ui.Context) { g.setSpeed(ctx, GameSpeedPaused) }
func (g *Game) PlantFlower(id string, tile Point) { func (g *Game) PlantFlower(id string, tile geom.Point) {
if g.Terrain.HasFlower(tile) { if g.Terrain.HasFlower(tile) {
// TODO: notify user it tried to plant on tile with flower? // TODO: notify user it tried to plant on tile with flower?
return return
@ -189,7 +187,7 @@ func (g *Game) PlantFlower(id string, tile Point) {
g.Terrain.AddFlower(tile, id, flower.Traits) g.Terrain.AddFlower(tile, id, flower.Traits)
} }
func (g *Game) Reset() { func (g *Game) Reset(ctx ui.Context) {
g.Balance = 100 g.Balance = 100
g.Herbarium = NewHerbarium() g.Herbarium = NewHerbarium()
g.Terrain = &Map{ g.Terrain = &Map{
@ -198,17 +196,17 @@ func (g *Game) Reset() {
Variant: NewRandomNoiseMap(rand.Int63()), Variant: NewRandomNoiseMap(rand.Int63()),
PlaceX: NewRandomNoiseMap(rand.Int63()), PlaceX: NewRandomNoiseMap(rand.Int63()),
PlaceY: NewRandomNoiseMap(rand.Int63()), PlaceY: NewRandomNoiseMap(rand.Int63()),
Flowers: map[Point]Flower{}, Flowers: map[geom.Point]Flower{},
} }
g.CancelTool() g.CancelTool(ctx)
g.setSpeed(GameSpeedNormal) g.setSpeed(ctx, GameSpeedNormal)
} }
func (g *Game) Resume() { g.setSpeed(g.SpeedBeforePause) } func (g *Game) Resume(ctx ui.Context) { g.setSpeed(ctx, g.SpeedBeforePause) }
func (g *Game) Run() { g.setSpeed(GameSpeedNormal) } func (g *Game) Run(ctx ui.Context) { g.setSpeed(ctx, GameSpeedNormal) }
func (g *Game) RunFast() { g.setSpeed(GameSpeedFast) } func (g *Game) RunFast(ctx ui.Context) { g.setSpeed(ctx, GameSpeedFast) }
func (g *Game) Save() { func (g *Game) Save() {
state := g.State() state := g.State()
@ -218,15 +216,15 @@ func (g *Game) Save() {
} }
} }
func (g *Game) SelectPlantFlowerTool(id string) { func (g *Game) SelectPlantFlowerTool(ctx ui.Context, id string) {
g.selectTool(&PlantFlowerTool{FlowerID: id}) g.selectTool(ctx, &PlantFlowerTool{FlowerID: id})
} }
func (g *Game) SelectShovel() { func (g *Game) SelectShovel(ctx ui.Context) {
g.selectTool(&ShovelTool{}) g.selectTool(ctx, &ShovelTool{})
} }
func (g *Game) SpeedChanged() EventHandler { return g.speedChanged } func (g *Game) SpeedChanged() ui.EventHandler { return &g.speedChanged }
func (g *Game) State() GameState { func (g *Game) State() GameState {
var state GameState var state GameState
@ -255,22 +253,27 @@ func (g *Game) State() GameState {
return state return state
} }
func (g *Game) TogglePause() { func (g *Game) TogglePause(ctx ui.Context) {
if g.Speed == GameSpeedPaused { if g.Speed == GameSpeedPaused {
g.Resume() g.Resume(ctx)
} else { } else {
g.Pause() g.Pause(ctx)
} }
} }
func (g *Game) Tool() Tool { return g.tool } func (g *Game) Tool() Tool {
if g.tool == nil {
return &NoTool{}
}
return g.tool
}
func (g *Game) ToolChanged() EventHandler { return g.toolChanged } func (g *Game) ToolChanged() ui.EventHandler { return &g.toolChanged }
func (g *Game) UnlockNextFlower() { func (g *Game) UnlockNextFlower(ctx ui.Context) {
price := g.Herbarium.UnlockNext() price := g.Herbarium.UnlockNext()
g.Balance -= price g.Balance -= price
g.selectTool(nil) g.selectTool(ctx, nil)
} }
func (g *Game) Update() { func (g *Game) Update() {
@ -279,7 +282,7 @@ func (g *Game) Update() {
} }
} }
func (g *Game) UserClickedTile(pos Point) { func (g *Game) UserClickedTile(pos geom.Point) {
if g.tool == nil { if g.tool == nil {
return return
} }

View File

@ -1,19 +1,24 @@
package tins2020 package tins2020
import ( import (
"github.com/veandco/go-sdl2/sdl" "opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/play"
"opslag.de/schobers/zntg/ui"
) )
const fpsOverlayName = "fps"
type GameControls struct { type GameControls struct {
Container ui.ContainerBase
game *Game game *Game
dialogs *Dialogs dialogs *Dialogs
menu ButtonBar menu ui.StackPanel
top ButtonBar top ui.StackPanel
flowers ButtonBar flowers ui.StackPanel
otherTools ButtonBar otherTools ui.StackPanel
pause *IconButton pause *IconButton
run *IconButton run *IconButton
@ -27,6 +32,114 @@ func NewGameControls(game *Game, dialogs *Dialogs) *GameControls {
return &GameControls{game: game, dialogs: dialogs} return &GameControls{game: game, dialogs: dialogs}
} }
func (c *GameControls) askUserBeforeLoad(ctx ui.Context) {
c.dialogs.AskConfirmation(ctx,
"Do you want to load?",
"You will lose any progress you made during this session.",
func(ui.Context, interface{}) {
c.game.Load(ctx)
c.updateFlowerControls()
},
func(ui.Context, interface{}) {})
}
func (c *GameControls) askUserBeforeNew(ctx ui.Context) {
c.dialogs.AskConfirmation(ctx,
"Do you want to start a new game?",
"You will lose any progress you made during this session.",
func(ui.Context, interface{}) {
c.game.New(ctx)
c.updateFlowerControls()
},
func(ui.Context, interface{}) {})
}
func (c *GameControls) askUserBeforeSave(ctx ui.Context) {
c.dialogs.AskConfirmation(ctx,
"Do you want to save?",
"Saving will overwrite any previous save game.",
func(ui.Context, interface{}) { c.game.Save() },
func(ui.Context, interface{}) {})
}
func (c *GameControls) Init(ctx ui.Context) {
ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{}, false)
c.game.SpeedChanged().AddHandler(c.speedChanged)
c.game.ToolChanged().AddHandler(c.toolChanged)
c.dialogs.DialogOpened().AddHandlerEmpty(func(ctx ui.Context) { c.game.Pause(ctx) })
c.dialogs.DialogClosed().AddHandlerEmpty(func(ctx ui.Context) {
c.updateFlowerControls()
c.game.Resume(ctx)
})
c.flowers.Background = zntg.MustHexColor("#356DAD")
for _, id := range c.game.Herbarium.Flowers() {
c.flowers.Children = append(c.flowers.Children, c.createBuyFlowerButton(id))
}
c.top.Orientation = ui.OrientationHorizontal
c.pause = NewIconButtonConfigure("control-pause", func(ctx ui.Context) {
c.game.Pause(ctx)
}, func(b *IconButton) {
b.DisabledColor = ctx.Style().Palette.Secondary
b.Tooltip = "Pause game"
})
c.run = NewIconButtonConfigure("control-run", func(ctx ui.Context) {
c.game.Run(ctx)
}, func(b *IconButton) {
b.DisabledColor = ctx.Style().Palette.Secondary
b.Tooltip = "Run game at normal speed"
})
c.runFast = NewIconButtonConfigure("control-run-fast", func(ctx ui.Context) {
c.game.RunFast(ctx)
}, func(b *IconButton) {
b.DisabledColor = ctx.Style().Palette.Secondary
b.Tooltip = "Run game at fast speed"
})
c.speedChanged(nil, c.game.Speed)
c.top.Children = []ui.Control{c.pause, c.run, c.runFast}
c.menu.Background = zntg.MustHexColor("#356DAD")
c.menu.Children = []ui.Control{
NewIconButtonConfigure("control-settings", c.dialogs.ShowSettings, func(b *IconButton) {
b.Disabled = true
b.DisabledColor = zntg.MustHexColor("#AFAFAF")
}),
NewIconButtonConfigure("control-save", c.askUserBeforeSave, func(b *IconButton) {
b.Tooltip = "Save game (key: Ctrl+S)"
}),
NewIconButtonConfigure("control-load", c.askUserBeforeLoad, func(b *IconButton) {
b.Tooltip = "Load last saved game (key: Ctrl+L)"
}),
NewIconButtonConfigure("control-new", c.askUserBeforeNew, func(b *IconButton) {
b.Tooltip = "Start new game (key: Ctrl+N)"
}),
NewIconButtonConfigure("control-information", c.dialogs.ShowIntro, func(b *IconButton) {
b.Tooltip = "Show information/intro (key: Escape)"
}),
}
for i, child := range c.menu.Children {
c.menu.Children[i] = ui.FixedHeight(child, 96)
}
c.shovel = NewIconButtonConfigure("control-shovel", func(ctx ui.Context) { c.game.SelectShovel(ctx) }, func(b *IconButton) {
b.Tooltip = "Select harvest tool (key: H)"
})
c.research = NewIconButtonConfigure("control-research", c.dialogs.ShowResearch, func(b *IconButton) {
b.Tooltip = "Conduct research (key: R)"
})
c.otherTools.Children = []ui.Control{c.shovel, c.research}
for i, child := range c.otherTools.Children {
c.otherTools.Children[i] = ui.FixedHeight(child, 96)
}
c.AddChild(&c.menu)
c.AddChild(&c.top)
c.AddChild(&c.flowers)
c.AddChild(&c.otherTools)
}
func (c *GameControls) createBuyFlowerButton(id string) *BuyFlowerButton { func (c *GameControls) createBuyFlowerButton(id string) *BuyFlowerButton {
flower, _ := c.game.Herbarium.Find(id) flower, _ := c.game.Herbarium.Find(id)
return NewBuyFlowerButton( return NewBuyFlowerButton(
@ -34,176 +147,106 @@ func (c *GameControls) createBuyFlowerButton(id string) *BuyFlowerButton {
flower.IconTemplate.Disabled(), flower.IconTemplate.Disabled(),
id, id,
flower, flower,
EmptyEvent(func() { func(ctx ui.Context) {
c.game.SelectPlantFlowerTool(id) c.game.SelectPlantFlowerTool(ctx, id)
}), },
) )
} }
func (c *GameControls) speedChanged(state interface{}) { func (c *GameControls) speedChanged(_ ui.Context, state interface{}) {
speed := state.(GameSpeed) speed := state.(GameSpeed)
disable := func(b *IconButton, expected GameSpeed) { disable := func(b *IconButton, expected GameSpeed) {
b.IsDisabled = speed == expected b.Disabled = speed == expected
} }
disable(c.pause, GameSpeedPaused) disable(c.pause, GameSpeedPaused)
disable(c.run, GameSpeedNormal) disable(c.run, GameSpeedNormal)
disable(c.runFast, GameSpeedFast) disable(c.runFast, GameSpeedFast)
} }
func (c *GameControls) toolChanged(state interface{}) { func (c *GameControls) toolChanged(_ ui.Context, state interface{}) {
tool, _ := state.(Tool) tool, _ := state.(Tool)
var flowerID string var flowerID string
if tool, ok := tool.(*PlantFlowerTool); ok { if tool, ok := tool.(*PlantFlowerTool); ok {
flowerID = tool.FlowerID flowerID = tool.FlowerID
} }
for _, control := range c.flowers.Buttons { for _, control := range c.flowers.Children {
button := control.(*BuyFlowerButton) button := control.(*BuyFlowerButton)
button.IsActive = button.FlowerID == flowerID button.Active = button.FlowerID == flowerID
button.IsDisabled = !c.game.Herbarium.IsUnlocked(button.FlowerID) button.Disabled = !c.game.Herbarium.IsUnlocked(button.FlowerID)
} }
_, shovel := tool.(*ShovelTool) _, shovel := tool.(*ShovelTool)
c.shovel.IsActive = shovel c.shovel.Active = shovel
} }
func (c *GameControls) updateFlowerControls(ctx *Context) { func (c *GameControls) updateFlowerControls() {
for _, b := range c.flowers.Buttons { for _, b := range c.flowers.Children {
button := b.(*BuyFlowerButton) button := b.(*BuyFlowerButton)
flower, ok := c.game.Herbarium.Find(button.FlowerID) flower, ok := c.game.Herbarium.Find(button.FlowerID)
if ok { if ok {
button.Update(ctx, flower) button.Update(flower)
} }
} }
} }
func (c *GameControls) Arrange(ctx *Context, bounds Rectangle) { const buttonBarWidth = 96
c.Bounds = bounds
c.menu.Arrange(ctx, Rect(bounds.X, bounds.Y, buttonBarWidth, bounds.H)) func (c *GameControls) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) {
c.top.Arrange(ctx, RectAbs(bounds.X+bounds.W/2+8, bounds.Y, bounds.Right(), bounds.Y+64)) c.ContainerBase.Arrange(ctx, bounds, offset, parent)
c.flowers.Arrange(ctx, Rect(bounds.Right()-buttonBarWidth, bounds.Y, buttonBarWidth, bounds.H))
c.otherTools.Arrange(ctx, Rect(bounds.Right()-buttonBarWidth, bounds.Bottom()-2*buttonBarWidth, buttonBarWidth, 2*buttonBarWidth)) c.menu.Arrange(ctx, geom.RectRelF32(bounds.Min.X, bounds.Min.Y, buttonBarWidth, bounds.Dy()), offset, c)
c.top.Arrange(ctx, geom.RectF32(bounds.Min.X+bounds.Dx()/2+8, bounds.Min.Y, bounds.Max.X, bounds.Min.Y+64), offset, c)
c.flowers.Arrange(ctx, geom.RectRelF32(bounds.Max.X-buttonBarWidth, bounds.Min.Y, buttonBarWidth, bounds.Dy()), offset, c)
otherToolsSize := c.otherTools.DesiredSize(ctx, bounds.Size())
c.otherTools.Arrange(ctx, geom.RectRelF32(bounds.Max.X-buttonBarWidth, bounds.Max.Y-otherToolsSize.Y, buttonBarWidth, 2*buttonBarWidth), offset, c)
} }
func (c *GameControls) Init(ctx *Context) error { func (c *GameControls) Handle(ctx ui.Context, event ui.Event) bool {
c.game.SpeedChanged().RegisterItf(c.speedChanged) if c.ContainerBase.Handle(ctx, event) {
c.game.ToolChanged().RegisterItf(c.toolChanged)
c.dialogs.DialogOpened().Register(func() { c.game.Pause() })
c.dialogs.DialogClosed().Register(func() {
c.updateFlowerControls(ctx)
c.game.Resume()
})
c.flowers.Background = MustHexColor("#356dad")
c.flowers.ButtonLength = 64
for _, id := range c.game.Herbarium.Flowers() {
c.flowers.Buttons = append(c.flowers.Buttons, c.createBuyFlowerButton(id))
}
c.top.Orientation = OrientationHorizontal
c.pause = NewIconButtonConfigure("control-pause", EmptyEvent(func() {
c.game.Pause()
}), func(b *IconButton) {
b.IconDisabled = "control-pause-disabled"
b.Tooltip.Text = "Pause game"
})
c.run = NewIconButtonConfigure("control-run", EmptyEvent(func() {
c.game.Run()
}), func(b *IconButton) {
b.IconDisabled = "control-run-disabled"
b.Tooltip.Text = "Run game at normal speed"
})
c.runFast = NewIconButtonConfigure("control-run-fast", EmptyEvent(func() {
c.game.RunFast()
}), func(b *IconButton) {
b.IconDisabled = "control-run-fast-disabled"
b.Tooltip.Text = "Run game at fast speed"
})
c.speedChanged(c.game.Speed)
c.top.Buttons = []Control{c.pause, c.run, c.runFast}
c.menu.Background = MustHexColor("#356dad")
c.menu.Buttons = []Control{
NewIconButtonConfigure("control-settings", c.dialogs.ShowSettings, func(b *IconButton) {
b.IsDisabled = true
b.IconDisabled = "#afafaf"
}),
NewIconButtonConfigure("control-save", func(*Context) { c.game.Save() }, func(b *IconButton) {
b.Tooltip.Text = "Save game (overwrites previous save; no confirmation)"
}),
NewIconButtonConfigure("control-load", func(ctx *Context) {
c.game.Load()
c.updateFlowerControls(ctx)
}, func(b *IconButton) {
b.Tooltip.Text = "Load last saved game (no confirmation)"
}),
NewIconButtonConfigure("control-new", func(ctx *Context) {
c.game.New()
c.updateFlowerControls(ctx)
}, func(b *IconButton) {
b.Tooltip.Text = "Start new game (no confirmation)"
}),
NewIconButtonConfigure("control-information", c.dialogs.ShowIntro, func(b *IconButton) {
b.Tooltip.Text = "Show information/intro"
}),
}
c.shovel = NewIconButtonConfigure("control-shovel", func(*Context) { c.game.SelectShovel() }, func(b *IconButton) {
b.IconHeight = 32
b.Tooltip.Text = "Select harvest tool (key: H)"
})
c.research = NewIconButtonConfigure("control-research", c.dialogs.ShowResearch, func(b *IconButton) {
b.IconHeight = 32
b.Tooltip.Text = "Conduct research (key: R)"
})
c.otherTools.Buttons = []Control{c.shovel, c.research}
c.Container.AddChild(&c.menu)
c.Container.AddChild(&c.top)
c.Container.AddChild(&c.flowers)
c.Container.AddChild(&c.otherTools)
return c.Container.Init(ctx)
}
func (c *GameControls) Handle(ctx *Context, event sdl.Event) bool {
if c.Container.Handle(ctx, event) {
return true return true
} }
switch e := event.(type) { switch e := event.(type) {
case *sdl.KeyboardEvent: case *ui.KeyDownEvent:
if e.Type == sdl.KEYDOWN { switch e.Key {
switch e.Keysym.Sym { case ui.KeySpace:
case sdl.K_SPACE: c.game.TogglePause(ctx)
c.game.TogglePause() case ui.Key1:
case sdl.K_1: c.game.Run(ctx)
c.game.Run() case ui.Key2:
case sdl.K_2: c.game.RunFast(ctx)
c.game.RunFast() case ui.KeyH:
case sdl.K_h: c.game.SelectShovel(ctx)
c.game.SelectShovel() case ui.KeyR:
case sdl.K_r:
c.dialogs.ShowResearch(ctx) c.dialogs.ShowResearch(ctx)
case sdl.K_ESCAPE: case ui.KeyEscape:
if c.game.Tool() == nil { if c.game.Tool().Type() == "none" {
c.dialogs.ShowIntro(ctx) c.dialogs.ShowIntro(ctx)
} else { } else {
c.game.CancelTool() c.game.CancelTool(ctx)
} }
return true return true
case sdl.K_F3: case ui.KeyF4:
c.game.Debug = !c.game.Debug c.game.Debug = !c.game.Debug
ctx.Overlays().Toggle(fpsOverlayName)
}
if e.Modifiers == ui.KeyModifierControl {
switch e.Key {
case ui.KeyL:
c.askUserBeforeLoad(ctx)
case ui.KeyN:
c.askUserBeforeNew(ctx)
case ui.KeyS:
c.askUserBeforeSave(ctx)
} }
} }
} }
return false return false
} }
func (c *GameControls) Render(ctx *Context) { func (c *GameControls) Render(ctx ui.Context) {
topBar := MustHexColor("#0000007f") topBar := zntg.MustHexColor("#0000007F")
SetDrawColor(ctx.Renderer, topBar) ctx.Renderer().FillRectangle(geom.RectF32(c.menu.Bounds().Max.X, 0, c.flowers.Bounds().Min.X, 64), topBar)
ctx.Renderer.FillRect(RectAbs(c.menu.Bounds.Right(), 0, c.flowers.Bounds.X, 64).SDLPtr()) ctx.Fonts().TextAlign("balance", geom.PtF32(c.top.Bounds().Min.X-8, 4), zntg.MustHexColor("#4AC69A"), FmtMoney(c.game.Balance), ui.AlignRight)
ctx.Fonts.Font("balance").RenderCopyAlign(ctx.Renderer, FmtMoney(c.game.Balance), Pt(c.top.Bounds.X-8, 58), MustHexColor("#4AC69A"), TextAlignmentRight)
c.Container.Render(ctx) c.ContainerBase.Render(ctx)
} }

View File

@ -1,8 +1,13 @@
package tins2020 package tins2020
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
)
type FlowerState struct { type FlowerState struct {
ID string ID string
Location Point Location geom.Point
} }
type GameState struct { type GameState struct {
@ -32,7 +37,7 @@ type TerrainState struct {
} }
type ViewState struct { type ViewState struct {
Center Point Center geom.Point
} }
func (s *GameState) Serialize(name string) error { func (s *GameState) Serialize(name string) error {
@ -40,7 +45,7 @@ func (s *GameState) Serialize(name string) error {
if err != nil { if err != nil {
return err return err
} }
return EncodeJSON(path, &s) return zntg.EncodeJSON(path, &s)
} }
func (s *GameState) Deserialize(name string) error { func (s *GameState) Deserialize(name string) error {
@ -48,7 +53,7 @@ func (s *GameState) Deserialize(name string) error {
if err != nil { if err != nil {
return err return err
} }
return DecodeJSON(path, &s) return zntg.DecodeJSON(path, &s)
} }
func SaveGameName() string { return "savegame.json" } func SaveGameName() string { return "savegame.json" }

View File

@ -31,7 +31,7 @@ func (h *Herbarium) Reset() {
}) })
h.Add("loosestrife", FlowerDescriptor{ h.Add("loosestrife", FlowerDescriptor{
Name: "Loosestrife", Name: "Loosestrife",
Description: "A simple flower that will spread in temperate and wet climates.", Description: "A simple flower that will spread in temperate and damp climates.",
IconTemplate: "flower-loosestrife-%s", IconTemplate: "flower-loosestrife-%s",
BuyPrice: 100, BuyPrice: 100,
SellPrice: 20, SellPrice: 20,

View File

@ -1,125 +1,47 @@
package tins2020 package tins2020
import ( import (
"github.com/veandco/go-sdl2/sdl" "image/color"
)
type HoverEffect int "opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
const (
HoverEffectLigthen HoverEffect = iota
HoverEffectColor
) )
type IconButton struct { type IconButton struct {
ControlBase ui.Button
Icon string Active bool
IconDisabled string
IconHeight int32
IconScale Scale
IconWidth int32
IconActive HoverEffect
IconHover HoverEffect
Tooltip Tooltip
IsActive bool
} }
func NewIconButton(icon string, onClick EventContextFn) *IconButton { func NewIconButton(icon string, click ui.EventEmptyFn) *IconButton {
return &IconButton{ b := &IconButton{
ControlBase: ControlBase{ Button: ui.Button{
OnLeftMouseButtonClick: onClick,
},
Icon: icon, Icon: icon,
IconHeight: 48,
Type: ui.ButtonTypeText,
HoverColor: hoverTransparentColor,
},
} }
b.Font.Color = color.White
b.ButtonClicked().AddHandler(func(ctx ui.Context, _ ui.ControlClickedArgs) { click(ctx) })
return b
} }
func NewIconButtonConfigure(icon string, onClick EventContextFn, configure func(*IconButton)) *IconButton { func NewIconButtonConfigure(icon string, click ui.EventEmptyFn, configure func(*IconButton)) *IconButton {
button := NewIconButton(icon, onClick) button := NewIconButton(icon, click)
configure(button) configure(button)
return button return button
} }
func (b *IconButton) activeTexture(ctx *Context) *Texture { var hoverTransparentColor = zntg.MustHexColor(`#FFFFFF1F`)
if b.IsDisabled {
texture := ctx.Textures.Texture(b.IconDisabled)
if texture != nil {
return texture
}
texture = ctx.Textures.Texture(b.Icon) func (b *IconButton) Render(ctx ui.Context) {
if len(b.IconDisabled) == 0 { b.RenderActive(ctx)
return texture b.Button.Render(ctx)
}
color, err := HexColor(b.IconDisabled)
if err == nil {
texture.SetColor(color)
}
return texture
}
return ctx.Textures.Texture(b.Icon)
} }
func (b *IconButton) Arrange(ctx *Context, bounds Rectangle) { func (b *IconButton) RenderActive(ctx ui.Context) {
b.ControlBase.Arrange(ctx, bounds) if b.Active || (!b.Disabled && b.IsOver()) {
b.Tooltip.Arrange(ctx, bounds) ctx.Renderer().FillRectangle(b.Bounds(), hoverTransparentColor)
}
func (b *IconButton) Handle(ctx *Context, event sdl.Event) bool {
if b.ControlBase.Handle(ctx, event) {
return true
}
if b.Tooltip.Handle(ctx, event) {
return true
}
return false
}
func (b *IconButton) Init(ctx *Context) error {
if err := b.ControlBase.Init(ctx); err != nil {
return err
}
if err := b.Tooltip.Init(ctx); err != nil {
return err
}
return nil
}
func (b *IconButton) Render(ctx *Context) {
iconTexture := b.activeTexture(ctx)
hover := b.IsMouseOver && !b.IsDisabled
if (hover && b.IconHover == HoverEffectColor) || (b.IsActive && b.IconActive == HoverEffectColor) {
iconTexture.SetColor(MustHexColor("#15569F"))
}
if b.IconScale == ScaleCenter {
size := iconTexture.Size()
if b.IconWidth != 0 {
size = Pt(b.IconWidth, b.IconWidth*size.Y/size.X)
} else if b.IconHeight != 0 {
size = Pt(b.IconHeight*size.X/size.Y, b.IconHeight)
}
iconTexture.CopyResize(ctx.Renderer, Rect(b.Bounds.X+(b.Bounds.W-size.X)/2, b.Bounds.Y+(b.Bounds.H-size.Y)/2, size.X, size.Y))
} else {
iconTexture.CopyResize(ctx.Renderer, b.Bounds)
}
if (hover && b.IconHover == HoverEffectLigthen) || (b.IsActive && b.IconActive == HoverEffectLigthen) {
SetDrawColor(ctx.Renderer, TransparentWhite)
ctx.Renderer.FillRect(b.Bounds.SDLPtr())
}
iconTexture.SetColor(White)
if len(b.Tooltip.Text) > 0 && b.IsMouseOver {
b.Tooltip.Render(ctx)
} }
} }
type Scale int
const (
ScaleCenter Scale = iota
ScaleStretch
)

View File

@ -1,62 +0,0 @@
package img
import (
"errors"
"image/color"
"regexp"
)
var hexColorRE = regexp.MustCompile(`^#?([0-9A-Fa-f]{2})-?([0-9A-Fa-f]{2})-?([0-9A-Fa-f]{2})-?([0-9A-Fa-f]{2})?$`)
func HexColor(s string) (color.RGBA, error) {
match := hexColorRE.FindStringSubmatch(s)
if match == nil {
return color.RGBA{}, errors.New("invalid color format")
}
values, err := HexToInts(match[1:]...)
if err != nil {
return color.RGBA{}, err
}
a := 255
if len(match[4]) > 0 {
a = values[3]
}
return color.RGBA{R: uint8(values[0]), G: uint8(values[1]), B: uint8(values[2]), A: uint8(a)}, nil
}
func MustHexColor(s string) color.RGBA {
color, err := HexColor(s)
if err != nil {
panic(err)
}
return color
}
func HexToInt(s string) (int, error) {
var i int
for _, c := range s {
i *= 16
if c >= '0' && c <= '9' {
i += int(c - '0')
} else if c >= 'A' && c <= 'F' {
i += int(c - 'A' + 10)
} else if c >= 'a' && c <= 'f' {
i += int(c - 'a' + 10)
} else {
return 0, errors.New("hex digit not supported")
}
}
return i, nil
}
func HexToInts(s ...string) ([]int, error) {
ints := make([]int, len(s))
for i, s := range s {
value, err := HexToInt(s)
if err != nil {
return nil, err
}
ints[i] = value
}
return ints, nil
}

View File

@ -1,29 +0,0 @@
package img
import (
"image"
"image/png"
"os"
)
func EncodePNG(path string, im image.Image) error {
dst, err := os.Create(path)
if err != nil {
return err
}
defer dst.Close()
return png.Encode(dst, im)
}
func DecodeImage(path string) (image.Image, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
im, _, err := image.Decode(f)
if err != nil {
return nil, err
}
return im, nil
}

View File

@ -1,13 +1,19 @@
package tins2020 package tins2020
type Intro struct { import (
LargeDialog "image/color"
welcome Paragraph "opslag.de/schobers/zntg/ui"
)
type Intro struct {
ui.Paragraph
} }
func (i *Intro) Init(ctx *Context) error { func NewIntro() *LargeDialog {
i.welcome.Text = i := &Intro{}
i.Font.Color = color.White
i.Text =
"Welcome to Botanim!\n\n" + "Welcome to Botanim!\n\n" +
"In Botanim you play the role of botanist and your goal is to cultivate flowers in an open landscape.\n\n" + "In Botanim you play the role of botanist and your goal is to cultivate flowers in an open landscape.\n\n" +
"Flowers can only grow (well) in certain climates based on two properties: humidity and temperature. Watch out for existing vegetation to get an idea how humid the land is and check the appearance of the tile to see how hot it is. When well placed your planted flower will spread soon but an odd choice might kill your flower almost instantly. So choose carefully. When the flower spread significantly you can harvest flowers again to collect more money.\n\n" + "Flowers can only grow (well) in certain climates based on two properties: humidity and temperature. Watch out for existing vegetation to get an idea how humid the land is and check the appearance of the tile to see how hot it is. When well placed your planted flower will spread soon but an odd choice might kill your flower almost instantly. So choose carefully. When the flower spread significantly you can harvest flowers again to collect more money.\n\n" +
@ -21,7 +27,5 @@ func (i *Intro) Init(ctx *Context) error {
" - W, A, S, D keys or CTRL + left mouse button or middle mouse button: pans landscape\n" + " - W, A, S, D keys or CTRL + left mouse button or middle mouse button: pans landscape\n" +
"\n" + "\n" +
"Have fun playing!" "Have fun playing!"
i.SetContent(&i.welcome) return NewLargeDialog("Botanim", i)
return i.LargeDialog.Init(ctx)
} }

47
io.go
View File

@ -1,50 +1,11 @@
package tins2020 package tins2020
import ( import (
"encoding/json" "opslag.de/schobers/zntg"
"os"
"path/filepath"
) )
func DecodeJSON(path string, v interface{}) error { const appName = "tins2020_botanim"
f, err := os.Open(path)
if err != nil {
return err
}
defer f.Close()
err = json.NewDecoder(f).Decode(v)
if err != nil {
return err
}
return nil
}
func EncodeJSON(path string, v interface{}) error { func UserDir() (string, error) { return zntg.UserDir(appName) }
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return json.NewEncoder(f).Encode(v)
}
func UserDir() (string, error) { func UserFile(name string) (string, error) { return zntg.UserFile(appName, name) }
config, err := os.UserConfigDir()
if err != nil {
return "", err
}
dir := filepath.Join(config, "tins2020_botanim")
err = os.MkdirAll(dir, 0777)
if err != nil {
return "", err
}
return dir, nil
}
func UserFile(name string) (string, error) {
dir, err := UserDir()
if err != nil {
return "", err
}
return filepath.Join(dir, name), nil
}

View File

@ -1,85 +0,0 @@
package tins2020
import (
"strings"
)
type Label struct {
ControlBase
Text string
Alignment TextAlignment
}
func (l *Label) Render(ctx *Context) {
font := ctx.Fonts.Font(l.ActualFontName())
color := l.ActualForeground()
bottom := l.Bounds.Y + l.Bounds.H
switch l.Alignment {
case TextAlignmentCenter:
font.RenderCopyAlign(ctx.Renderer, l.Text, Pt(l.Bounds.X+l.Bounds.W/2, bottom), color, TextAlignmentCenter)
case TextAlignmentLeft:
font.RenderCopyAlign(ctx.Renderer, l.Text, Pt(l.Bounds.X, bottom), color, TextAlignmentLeft)
case TextAlignmentRight:
font.RenderCopyAlign(ctx.Renderer, l.Text, Pt(l.Bounds.X+l.Bounds.W, bottom), color, TextAlignmentRight)
}
}
type Paragraph struct {
Label
}
func (p *Paragraph) Render(ctx *Context) {
font := ctx.Fonts.Font(p.ActualFontName())
color := p.ActualForeground()
fontHeight := int32(font.Height())
lines := strings.Split(p.Text, "\n")
measure := func(s string) int32 {
w, _, _ := font.SizeUTF8(s)
return int32(w)
}
spaces := func(s string) []int {
var spaces []int
offset := 0
for {
space := strings.Index(s[offset:], " ")
if space == -1 {
return spaces
}
offset += space
spaces = append(spaces, offset)
offset++
}
}
fit := func(s string) string {
if measure(s) < p.Bounds.W {
return s
}
spaces := spaces(s)
for split := len(spaces) - 1; split >= 0; split-- {
clipped := s[:spaces[split]]
if measure(clipped) < p.Bounds.W {
return clipped
}
}
return s
}
offset := p.Bounds.Y
for _, line := range lines {
if len(line) == 0 {
offset += fontHeight
continue
}
for len(line) > 0 {
offset += fontHeight
clipped := fit(line)
line = strings.TrimLeft(line[len(clipped):], " ")
font.RenderCopy(ctx.Renderer, clipped, Pt(p.Bounds.X, offset), color)
}
}
}

View File

@ -1,108 +1,100 @@
package tins2020 package tins2020
import "github.com/veandco/go-sdl2/sdl" import (
"image/color"
type DialogBase struct { "opslag.de/schobers/geom"
Container "opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
)
content Proxy const titleBarHeight = 64
onShow *Events
close EventFn
}
type Dialog interface {
CloseDialog()
OnShow() EventHandler
ShowDialog(*Context, EventFn)
}
func (d *DialogBase) CloseDialog() {
close := d.close
if close != nil {
close()
}
}
func (d *DialogBase) Init(ctx *Context) error {
d.AddChild(&d.content)
return d.Container.Init(ctx)
}
func (d *DialogBase) OnShow() EventHandler {
if d.onShow == nil {
d.onShow = NewEvents()
}
return d.onShow
}
func (d *DialogBase) SetContent(control Control) {
d.content.Proxied = control
}
func (d *DialogBase) ShowDialog(ctx *Context, close EventFn) {
d.close = close
if d.onShow != nil {
d.onShow.Notify(ctx)
}
}
type LargeDialog struct { type LargeDialog struct {
DialogBase ui.StackPanel
title Label titleBar *LargeDialogTitleBar
close IconButton content ui.Proxy
closeRequested ui.Events
} }
func (d *LargeDialog) Arrange(ctx *Context, bounds Rectangle) { func NewLargeDialog(title string, content ui.Control) *LargeDialog {
const titleHeight = 64 dialog := &LargeDialog{}
d.ControlBase.Arrange(ctx, bounds)
d.title.Arrange(ctx, Rect(bounds.X, bounds.Y, bounds.W, titleHeight)) dialog.Orientation = ui.OrientationVertical
d.close.Arrange(ctx, Rect(bounds.W-64, 0, 64, 64)) dialog.titleBar = NewLargeDialogTitleBar(title, func(ctx ui.Context, state interface{}) {
d.content.Arrange(ctx, Rect(bounds.X+titleHeight, 96, bounds.W-2*titleHeight, bounds.H-titleHeight)) dialog.closeRequested.Notify(ctx, state)
})
dialog.content.Content = ui.Margins(content, titleBarHeight, 20, titleBarHeight, 0)
dialog.Children = []ui.Control{dialog.titleBar, &dialog.content}
return dialog
} }
func (d *LargeDialog) Init(ctx *Context) error { func (d *LargeDialog) CloseRequested() ui.EventHandler { return &d.closeRequested }
d.title.Text = "Botanim"
d.title.FontName = "title"
d.title.Alignment = TextAlignmentCenter
d.close = IconButton{ func (d *LargeDialog) Handle(ctx ui.Context, e ui.Event) bool {
Icon: "control-cancel", if d.StackPanel.Handle(ctx, e) {
IconHover: HoverEffectColor,
IconWidth: 32,
}
d.close.OnLeftMouseButtonClick = EmptyEvent(d.CloseDialog)
d.AddChild(&d.title)
d.AddChild(&d.close)
return d.DialogBase.Init(ctx)
}
func (d *LargeDialog) Handle(ctx *Context, event sdl.Event) bool {
if d.DialogBase.Handle(ctx, event) {
return true return true
} }
switch e := event.(type) { switch e := e.(type) {
case *sdl.KeyboardEvent: case *ui.KeyDownEvent:
if e.Type == sdl.KEYDOWN { switch e.Key {
switch e.Keysym.Sym { case ui.KeyEscape:
case sdl.K_ESCAPE: d.closeRequested.Notify(ctx, nil)
d.CloseDialog()
return true return true
case sdl.K_RETURN: case ui.KeyEnter:
d.CloseDialog() d.closeRequested.Notify(ctx, nil)
return true return true
} }
} }
}
return false return false
} }
func (d *LargeDialog) Render(ctx *Context) { func (d *LargeDialog) Hidden() { d.content.Hidden() }
SetDrawColor(ctx.Renderer, MustHexColor("#356DAD"))
ctx.Renderer.FillRect(d.Bounds.SDLPtr())
d.DialogBase.Render(ctx) func (d *LargeDialog) Render(ctx ui.Context) {
ctx.Renderer().Clear(zntg.MustHexColor("#356DAD"))
d.StackPanel.Render(ctx)
} }
func (d *LargeDialog) SetCaption(s string) { d.title.Text = s } func (d *LargeDialog) Shown() { d.content.Shown() }
type LargeDialogTitleBar struct {
ui.ContainerBase
title ui.Label
close ui.Button
}
func NewLargeDialogTitleBar(title string, closeRequested ui.EventFn) *LargeDialogTitleBar {
titleBar := &LargeDialogTitleBar{}
titleBar.Children = []ui.Control{&titleBar.title, &titleBar.close}
titleBar.close.ButtonClicked().AddHandler(func(ctx ui.Context, args ui.ControlClickedArgs) {
closeRequested(ctx, args)
})
titleBar.title.Font.Color = color.White
titleBar.title.Font.Name = "title"
titleBar.title.Text = title
titleBar.title.TextAlignment = ui.AlignCenter
titleBar.close.Icon = "control-cancel"
titleBar.close.IconHeight = 32
titleBar.close.Type = ui.ButtonTypeIcon
titleBar.close.HoverColor = zntg.MustHexColor(`#ABCAED`)
return titleBar
}
func (b *LargeDialogTitleBar) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) {
b.ControlBase.Arrange(ctx, bounds, offset, parent)
b.title.Arrange(ctx, bounds, offset, parent)
height := bounds.Dy()
b.close.Arrange(ctx, geom.RectRelF32(bounds.Max.X-height, bounds.Min.Y, height, height), offset, parent)
}
func (b *LargeDialogTitleBar) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 {
return geom.PtF32(size.X, titleBarHeight)
}

24
map.go
View File

@ -1,5 +1,7 @@
package tins2020 package tins2020
import "opslag.de/schobers/geom"
type Map struct { type Map struct {
Temp NoiseMap Temp NoiseMap
Humid NoiseMap Humid NoiseMap
@ -7,32 +9,32 @@ type Map struct {
PlaceX NoiseMap // displacement map of props PlaceX NoiseMap // displacement map of props
PlaceY NoiseMap PlaceY NoiseMap
Center Point Center geom.Point
Flowers map[Point]Flower Flowers map[geom.Point]Flower
} }
func (m *Map) AddFlower(pos Point, id string, traits FlowerTraits) { func (m *Map) AddFlower(pos geom.Point, id string, traits FlowerTraits) {
m.Flowers[pos] = m.NewFlower(pos, id, traits) m.Flowers[pos] = m.NewFlower(pos, id, traits)
} }
func (m *Map) FlowersOnAdjacentTiles(pos Point) int { func (m *Map) FlowersOnAdjacentTiles(pos geom.Point) int {
var count int var count int
if _, ok := m.Flowers[Pt(pos.X+1, pos.Y)]; ok { if _, ok := m.Flowers[geom.Pt(pos.X+1, pos.Y)]; ok {
count++ count++
} }
if _, ok := m.Flowers[Pt(pos.X-1, pos.Y)]; ok { if _, ok := m.Flowers[geom.Pt(pos.X-1, pos.Y)]; ok {
count++ count++
} }
if _, ok := m.Flowers[Pt(pos.X, pos.Y+1)]; ok { if _, ok := m.Flowers[geom.Pt(pos.X, pos.Y+1)]; ok {
count++ count++
} }
if _, ok := m.Flowers[Pt(pos.X, pos.Y-1)]; ok { if _, ok := m.Flowers[geom.Pt(pos.X, pos.Y-1)]; ok {
count++ count++
} }
return count return count
} }
func (m *Map) DigFlower(pos Point) string { func (m *Map) DigFlower(pos geom.Point) string {
flower, ok := m.Flowers[pos] flower, ok := m.Flowers[pos]
if !ok { if !ok {
return "" return ""
@ -41,12 +43,12 @@ func (m *Map) DigFlower(pos Point) string {
return flower.ID return flower.ID
} }
func (m *Map) HasFlower(pos Point) bool { func (m *Map) HasFlower(pos geom.Point) bool {
_, ok := m.Flowers[pos] _, ok := m.Flowers[pos]
return ok return ok
} }
func (m *Map) NewFlower(pos Point, id string, traits FlowerTraits) Flower { func (m *Map) NewFlower(pos geom.Point, id string, traits FlowerTraits) Flower {
flower := Flower{ flower := Flower{
ID: id, ID: id,
Traits: traits, Traits: traits,

View File

@ -14,7 +14,7 @@ func clipNormalized(x float64) float64 {
type NoiseMap interface { type NoiseMap interface {
Seed() int64 Seed() int64
Value(x, y int32) float64 Value(x, y int) float64
} }
func NewNoiseMap(seed int64) NoiseMap { func NewNoiseMap(seed int64) NoiseMap {
@ -33,7 +33,7 @@ type noiseMap struct {
} }
// Value generates the noise value for an x/y pair. // Value generates the noise value for an x/y pair.
func (m noiseMap) Value(x, y int32) float64 { func (m noiseMap) Value(x, y int) float64 {
value := m.noise.Noise2D(float64(x)*.01, float64(y)*.01, m.alpha, m.beta, m.harmonics)*.565 + .5 value := m.noise.Noise2D(float64(x)*.01, float64(y)*.01, m.alpha, m.beta, m.harmonics)*.565 + .5
return clipNormalized(value) return clipNormalized(value)
} }
@ -49,7 +49,7 @@ type randomNoiseMap struct {
} }
// Value generates the noise value for an x/y pair. // Value generates the noise value for an x/y pair.
func (m randomNoiseMap) Value(x, y int32) float64 { func (m randomNoiseMap) Value(x, y int) float64 {
value := m.Noise2D(float64(x)*.53, float64(y)*.53, 1.01, 2, 2)*.5 + .5 value := m.Noise2D(float64(x)*.53, float64(y)*.53, 1.01, 2, 2)*.5 + .5
return clipNormalized(value) return clipNormalized(value)
} }

View File

@ -1,42 +0,0 @@
package tins2020
import "github.com/veandco/go-sdl2/sdl"
type Point struct {
sdl.Point
}
func (p Point) Add(q Point) Point { return Pt(p.X+q.X, p.Y+q.Y) }
func (p Point) In(r Rectangle) bool { return r.IsPointInsidePt(p) }
func (p Point) Sub(q Point) Point { return Pt(p.X-q.X, p.Y-q.Y) }
func (p Point) ToPtF() PointF { return PtF(float32(p.X), float32(p.Y)) }
type PointF struct {
sdl.FPoint
}
func (p PointF) Add(q PointF) PointF {
return PtF(p.X+q.X, p.Y+q.Y)
}
func (p PointF) Mul(f float32) PointF {
return PtF(f*p.X, f*p.Y)
}
func (p PointF) Sub(q PointF) PointF { return PtF(p.X-q.X, p.Y-q.Y) }
func Pt(x, y int32) Point { return Point{sdl.Point{X: x, Y: y}} }
func PtF(x, y float32) PointF { return PointF{sdl.FPoint{X: x, Y: y}} }
func PtPtr(x, y int32) *Point {
p := Pt(x, y)
return &p
}
func PtFPtr(x, y float32) *PointF {
p := PtF(x, y)
return &p
}

View File

@ -1,130 +0,0 @@
package tins2020
import (
"log"
"github.com/veandco/go-sdl2/sdl"
)
func mapToTile(q PointF) Point {
return Pt(int32(Round32(q.X)), int32(Round32(q.Y)))
}
type projection struct {
center PointF
zoom float32
zoomInv float32
windowInteractRect Rectangle
windowVisibleRect Rectangle
tileScreenDelta PointF
tileScreenDeltaInv PointF
tileScreenOffset Point
tileScreenSize Point
tileFitScreenSize Point
windowCenter Point
}
func newProjection() projection {
return projection{zoom: 1, tileScreenDelta: PtF(64, 32), tileScreenDeltaInv: PtF(1./64, 1./32)}
}
func (p *projection) mapToScreen(x, y int32) Point {
return p.mapToScreenF(float32(x), float32(y))
}
func (p *projection) mapToScreenF(x, y float32) Point {
translated := PtF(x-p.center.X, y-p.center.Y)
return Pt(p.windowCenter.X+int32((translated.X-translated.Y)*64*p.zoom), p.windowCenter.Y+int32((translated.X+translated.Y)*32*p.zoom))
}
func (p *projection) screenToMap(x, y int32) PointF {
pos := p.screenToMapRel(x-p.windowCenter.X, y-p.windowCenter.Y)
return p.center.Add(pos)
}
func (p *projection) screenToMapInt(x, y int32) Point {
pos := p.screenToMap(x, y)
return mapToTile(pos)
}
func (p *projection) screenToMapRel(x, y int32) PointF {
normX := p.zoomInv * float32(x)
normY := p.zoomInv * float32(y)
return PtF(.5*(p.tileScreenDeltaInv.X*normX+p.tileScreenDeltaInv.Y*normY), .5*(-p.tileScreenDeltaInv.X*normX+p.tileScreenDeltaInv.Y*normY))
}
func (p *projection) screenToTileFitRect(pos Point) Rectangle {
return Rect(pos.X-p.tileFitScreenSize.X, pos.Y-p.tileFitScreenSize.Y, 2*p.tileFitScreenSize.X, 2*p.tileFitScreenSize.Y)
}
func (p *projection) screenToTileRect(pos Point) Rectangle {
return Rect(pos.X-p.tileScreenOffset.X, pos.Y-p.tileScreenOffset.Y, p.tileScreenSize.X, p.tileScreenSize.Y)
}
func (p *projection) update(renderer *sdl.Renderer) {
p.zoomInv = 1 / p.zoom
p.tileScreenOffset = Pt(int32(p.zoom*64), int32(p.zoom*112))
p.tileScreenSize = Pt(int32(p.zoom*128), int32(p.zoom*160))
p.tileFitScreenSize = Pt(int32(p.zoom*64), int32(p.zoom*32))
windowW, windowH, err := renderer.GetOutputSize()
if err != nil {
log.Fatal(err)
}
p.windowCenter = Pt(windowW/2, windowH/2)
p.windowInteractRect = RectAbs(buttonBarWidth, 64, windowW-buttonBarWidth, windowH)
p.windowVisibleRect = RectAbs(buttonBarWidth, 0, windowW-buttonBarWidth, windowH+p.tileScreenSize.Y) // Adding a tile height to the bottom for trees that stick out from the cells below.
}
func (p *projection) visibleTiles(action func(int32, int32, Point)) {
visible := p.windowVisibleRect
topLeft := p.screenToMap(visible.X, visible.Y)
topRight := p.screenToMap(visible.X+visible.W, visible.Y)
bottomLeft := p.screenToMap(visible.X, visible.Y+visible.H)
bottomRight := p.screenToMap(visible.X+visible.W, visible.Y+visible.H)
minY, maxY := int32(Floor32(topRight.Y)), int32(Ceil32(bottomLeft.Y))
minX, maxX := int32(Floor32(topLeft.X)), int32(Ceil32(bottomRight.X))
for y := minY; y <= maxY; y++ {
for x := minX; x <= maxX; x++ {
pos := p.mapToScreen(x, y)
rectFit := p.screenToTileFitRect(pos)
if rectFit.X+rectFit.W < visible.X || rectFit.Y+rectFit.H < visible.Y {
continue
}
if rectFit.X > visible.X+visible.W || rectFit.Y > visible.Y+visible.H {
break
}
action(x, y, pos)
}
}
}
func (p *projection) Pan(ctx *Context, delta PointF) {
p.center = p.center.Add(delta.Mul(p.zoomInv))
p.update(ctx.Renderer)
}
func (p *projection) SetZoom(ctx *Context, center PointF, zoom float32) {
if p.zoom == zoom {
return
}
p.center = center.Sub(center.Sub(p.center).Mul(p.zoom / zoom))
p.zoom = zoom
p.update(ctx.Renderer)
}
func (p *projection) ZoomOut(ctx *Context, center PointF) {
if p.zoom <= .25 {
return
}
p.SetZoom(ctx, center, .5*p.zoom)
}
func (p *projection) ZoomIn(ctx *Context, center PointF) {
if p.zoom >= 2 {
return
}
p.SetZoom(ctx, center, 2*p.zoom)
}

View File

@ -1,48 +0,0 @@
package tins2020
import "github.com/veandco/go-sdl2/sdl"
var _ Control = &Proxy{}
type Proxy struct {
Proxied Control
bounds Rectangle
}
func (p *Proxy) Arrange(ctx *Context, bounds Rectangle) {
p.bounds = bounds
if p.Proxied == nil {
return
}
p.Proxied.Arrange(ctx, bounds)
}
func (p *Proxy) Handle(ctx *Context, event sdl.Event) bool {
if p.Proxied == nil {
return false
}
return p.Proxied.Handle(ctx, event)
}
func (p *Proxy) Init(ctx *Context) error {
if p.Proxied == nil {
return nil
}
return p.Proxied.Init(ctx)
}
func (p *Proxy) Render(ctx *Context) {
if p.Proxied == nil {
return
}
p.Proxied.Render(ctx)
}
func (p *Proxy) SetContent(ctx *Context, content Control) {
p.Proxied = content
if content == nil {
return
}
content.Arrange(ctx, p.bounds)
}

32
rect.go
View File

@ -1,32 +0,0 @@
package tins2020
import "github.com/veandco/go-sdl2/sdl"
type Rectangle struct {
sdl.Rect
}
func RectAbs(x1, y1, x2, y2 int32) Rectangle {
if x1 > x2 {
x1, x2 = x2, x1
}
if y1 > y2 {
y1, y2 = y2, y1
}
return Rectangle{sdl.Rect{X: x1, Y: y1, W: x2 - x1, H: y2 - y1}}
}
func Rect(x, y, w, h int32) Rectangle { return Rectangle{sdl.Rect{X: x, Y: y, W: w, H: h}} }
func (r Rectangle) Bottom() int32 { return r.Y + r.H }
func (r Rectangle) IsPointInside(x, y int32) bool {
return x >= r.X && x < r.X+r.W && y >= r.Y && y < r.Y+r.H
}
func (r Rectangle) IsPointInsidePt(p Point) bool { return r.IsPointInside(p.X, p.Y) }
func (r Rectangle) Right() int32 { return r.X + r.W }
func (r Rectangle) SDL() sdl.Rect { return r.Rect }
func (r Rectangle) SDLPtr() *sdl.Rect { return &r.Rect }

View File

@ -2,205 +2,76 @@ package tins2020
import ( import (
"fmt" "fmt"
"math"
"math/rand" "math/rand"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/veandco/go-sdl2/sdl" "opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
) )
type Research struct { type Research struct {
Container ui.StackPanel
game *Game game *Game
botanist Specialist botanist Specialist
farmer Specialist farmer Specialist
typing string description ui.Paragraph
digitCount int specialists ui.Paragraph
dial *Dial
input ui.Label
close func() animate zntg.Animation
description Paragraph closeRequested ui.Events
specialists Paragraph
input Label
digits []Digit
animate Animation
} }
func NewResearch(game *Game) Control { type Dialer interface {
research := &Research{ CanUserType(int) bool
game: game, UserGaveWrongInput()
animate: NewAnimation(20 * time.Millisecond), UserTyped(ui.Context, int)
} }
dialog := &LargeDialog{}
dialog.SetCaption("Research") func NewResearch(game *Game) *LargeDialog {
dialog.SetContent(research) research := &Research{game: game}
dialog.OnShow().RegisterItf(func(state interface{}) { research.animate.Interval = 20 * time.Millisecond
research.onShow(state.(*Context)) research.animate.Start()
})
research.close = func() { dialog.CloseDialog() } research.description.Text = "Call a specialist to conduct research with."
research.dial = NewDial(research)
research.Children = []ui.Control{&research.description, &research.specialists, research.dial, &research.input}
dialog := NewLargeDialog("Research", ui.Stretch(research))
research.closeRequested.AddHandlerEmpty(func(ctx ui.Context) { dialog.closeRequested.Notify(ctx, nil) })
return dialog return dialog
} }
type Digit struct {
ControlBase
Value string
highlight int
}
func (d *Digit) Blink() {
d.highlight = 4
}
func (d *Digit) Render(ctx *Context) {
font := ctx.Fonts.Font("title")
color := White
if d.highlight > 0 {
color = MustHexColor("#15569F")
}
font.RenderCopyAlign(ctx.Renderer, d.Value, Pt(d.Bounds.X+d.Bounds.W/2, d.Bounds.Y+int32(font.Height())), color, TextAlignmentCenter)
}
func (d *Digit) Tick() {
if d.highlight > 0 {
d.highlight--
}
}
type Specialist struct { type Specialist struct {
Cost int Cost int
Number string Number string
} }
func (r *Research) Init(ctx *Context) error { func (r *Research) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) {
r.AddChild(&r.description) r.input.TextAlignment = ui.AlignCenter
r.AddChild(&r.specialists) r.StackPanel.Arrange(ctx, bounds, offset, parent)
r.AddChild(&r.input)
r.description.Text = "Call a specialist to conduct research with."
r.digits = make([]Digit, 10)
for i := range r.digits {
j := i
r.digits[i].Value = strconv.Itoa(i)
r.digits[i].OnLeftMouseButtonClick = func(*Context) {
r.userTyped(j)
}
r.AddChild(&r.digits[i])
}
return nil
} }
func (r *Research) Arrange(ctx *Context, bounds Rectangle) { func (r *Research) CanUserType(digit int) bool {
r.Container.Arrange(ctx, bounds) typing := strconv.Itoa(digit)
r.specialists.Arrange(ctx, Rect(r.Bounds.X, r.Bounds.Y+40, r.Bounds.W, r.Bounds.H-40)) return strings.HasPrefix(r.botanist.Number, r.input.Text+typing)
r.input.Arrange(ctx, Rect(r.Bounds.X, r.Bounds.X+r.Bounds.H-48, r.Bounds.W, 24))
r.input.Alignment = TextAlignmentCenter
center := Pt(r.Bounds.X+r.Bounds.W/2, r.Bounds.Y+r.Bounds.H/2)
distance := float64(bounds.H) * .3
for i := range r.digits {
angle := (float64((10-i)%10)*0.16 + .2) * math.Pi
pos := Pt(int32(distance*math.Cos(angle)), int32(.8*distance*math.Sin(angle)))
digitCenter := center.Add(pos)
r.digits[i].Arrange(ctx, Rect(digitCenter.X-24, digitCenter.Y-24, 48, 48))
}
} }
func (r *Research) userTyped(i int) { func (r *Research) Hidden() {}
r.digits[i].Blink()
digit := strconv.Itoa(i)
if len(r.typing) == 0 || digit != r.typing {
r.typing = digit
r.digitCount = 1
} else {
r.digitCount++
}
if !strings.HasPrefix(r.botanist.Number, r.input.Text+r.typing) { func (r *Research) Render(ctx ui.Context) {
r.input.Text = "" r.animate.AnimateFn(r.dial.Tick)
r.typing = "" r.StackPanel.Render(ctx)
r.digitCount = 0
} else if r.digitCount == i || r.digitCount == 10 {
r.input.Text += digit
r.typing = ""
r.digitCount = 0
if r.input.Text == r.botanist.Number {
r.game.UnlockNextFlower()
r.close()
r.input.Text = ""
}
}
} }
func (r *Research) Handle(ctx *Context, event sdl.Event) bool { func (r *Research) Shown() {
if r.Container.Handle(ctx, event) {
return true
}
switch e := event.(type) {
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
switch e.Keysym.Sym {
case sdl.K_0:
r.userTyped(0)
case sdl.K_KP_0:
r.userTyped(0)
case sdl.K_1:
r.userTyped(1)
case sdl.K_KP_1:
r.userTyped(1)
case sdl.K_2:
r.userTyped(2)
case sdl.K_KP_2:
r.userTyped(2)
case sdl.K_3:
r.userTyped(3)
case sdl.K_KP_3:
r.userTyped(3)
case sdl.K_4:
r.userTyped(4)
case sdl.K_KP_4:
r.userTyped(4)
case sdl.K_5:
r.userTyped(5)
case sdl.K_KP_5:
r.userTyped(5)
case sdl.K_6:
r.userTyped(6)
case sdl.K_KP_6:
r.userTyped(6)
case sdl.K_7:
r.userTyped(7)
case sdl.K_KP_7:
r.userTyped(7)
case sdl.K_8:
r.userTyped(8)
case sdl.K_KP_8:
r.userTyped(8)
case sdl.K_9:
r.userTyped(9)
case sdl.K_KP_9:
r.userTyped(9)
}
}
}
return false
}
func (r *Research) Render(ctx *Context) {
for i := range r.digits {
r.digits[i].Tick()
}
r.Container.Render(ctx)
}
func (r *Research) onShow(ctx *Context) {
generateNumber := func() string { generateNumber := func() string {
var number string var number string
for i := 0; i < 3; i++ { for i := 0; i < 3; i++ {
@ -208,8 +79,9 @@ func (r *Research) onShow(ctx *Context) {
} }
return number return number
} }
r.digitCount = 0
r.input.Text = "" r.input.Text = ""
r.dial.Reset()
var specialists string var specialists string
defer func() { defer func() {
@ -233,3 +105,16 @@ func (r *Research) onShow(ctx *Context) {
specialists += fmt.Sprintf("Botanist: no. %s (unlocks next flower; $ %d)\n", r.botanist.Number, r.botanist.Cost) specialists += fmt.Sprintf("Botanist: no. %s (unlocks next flower; $ %d)\n", r.botanist.Number, r.botanist.Cost)
specialists += "Farmer: no. **unavailable** (fertilizes land; $ ---)\n" specialists += "Farmer: no. **unavailable** (fertilizes land; $ ---)\n"
} }
func (r *Research) UserGaveWrongInput() {
r.input.Text = ""
}
func (r *Research) UserTyped(ctx ui.Context, digit int) {
r.input.Text += strconv.Itoa(digit)
if r.input.Text == r.botanist.Number {
r.game.UnlockNextFlower(ctx)
r.input.Text = ""
r.closeRequested.Notify(ctx, nil)
}
}

View File

@ -4,6 +4,8 @@ import (
"bufio" "bufio"
"fmt" "fmt"
"strings" "strings"
"opslag.de/schobers/zntg/ui"
) )
type ResourceLoader struct { type ResourceLoader struct {
@ -14,8 +16,8 @@ func NewResourceLoader() *ResourceLoader {
return &ResourceLoader{} return &ResourceLoader{}
} }
func (l *ResourceLoader) parseResourcesFile(res *Resources, name string) error { func (l *ResourceLoader) parseResourcesFile(res ui.Resources, name string) error {
f, err := res.Fs().Open(name) f, err := res.OpenResource(name)
if err != nil { if err != nil {
return err return err
} }
@ -36,7 +38,7 @@ func (l *ResourceLoader) parseResourcesFile(res *Resources, name string) error {
return nil return nil
} }
func (l *ResourceLoader) LoadFromFile(res *Resources, name string, action func(string, string) error) error { func (l *ResourceLoader) LoadFromFile(res ui.Resources, name string, action func(string, string) error) error {
err := l.parseResourcesFile(res, name) err := l.parseResourcesFile(res, name)
if err != nil { if err != nil {
return err return err

View File

@ -1,43 +0,0 @@
package tins2020
import (
rice "github.com/GeertJohan/go.rice"
"github.com/spf13/afero"
"opslag.de/schobers/fs/ricefs"
"opslag.de/schobers/fs/vfs"
)
type Resources struct {
box *rice.Box
fs afero.Fs
copy vfs.CopyDir
}
func (r *Resources) Box() *rice.Box {
return r.box
}
func (r *Resources) Destroy() {
r.copy.Destroy()
}
func (r *Resources) Fs() afero.Fs {
return r.fs
}
func (r *Resources) Open(box *rice.Box) error {
r.box = box
r.fs = vfs.NewOsFsFallback(r.box.Name(), ricefs.NewFs(box))
copy, err := vfs.NewCopyDir(r.fs)
if err != nil {
return err
}
r.copy = copy
return nil
}
func (r *Resources) Copy() vfs.CopyDir { return r.copy }
func (r *Resources) Retrieve(name string) (string, error) {
return r.copy.Retrieve(name)
}

View File

@ -7,33 +7,54 @@ else
version="$1" version="$1"
fi fi
version_safe=${version//\./_}
echo "Creating ${version} release" echo "Creating ${version} release"
rm -rf build/linux* rm -rf build/linux*
rm -rf build/macosx*
rm -rf build/windows* rm -rf build/windows*
mkdir -p build/linux
mkdir -p build/windows
go generate ../cmd/tins2020
go build -tags static -ldflags "-s -w" -o build/linux/botanim ../cmd/tins2020
cp ../README.md build/linux
CGO_ENABLED=1 CC=x86_64-w64-mingw32-gcc GOOS=windows GOARCH=amd64 go build -tags static -ldflags "-s -w" -o build/windows/botanim.exe ../cmd/tins2020
cp ../README.md build/windows
mkdir -p build/release mkdir -p build/release
cd build go generate ../cmd/tins2020
cd linux mkdir -p build/linux
zip -9 -q ../release/botanim_${version}_linux_amd64.zip * go build -tags static -ldflags "-s -w" -o build/linux/botanim ../cmd/tins2020
echo "Created Linux release: build/release/botanim_${version}_linux_amd64.zip" cp ../README.md build/linux
cd .. cd build/linux
zip -9 -q ../release/botanim_${version_safe}_linux_amd64.zip *
echo "Created Linux release: build/release/botanim_${version_safe}_linux_amd64.zip"
cd ../..
cd windows mkdir -p build/linux-allegro
zip -9 -q ../release/botanim_${version}_windows_amd64.zip * go build -tags static,allegro -ldflags "-s -w" -o build/linux-allegro/botanim ../cmd/tins2020
echo "Created Windows release: build/release/botanim_${version}_windows_amd64.zip" cp ../README.md build/linux-allegro
cd .. cd build/linux-allegro
zip -9 -q ../release/botanim_allegro_${version_safe}_linux_amd64.zip *
echo "Created Linux (Allegro) release: build/release/botanim_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/botanim ../cmd/tins2020
cp ../README.md build/macosx
cd build/macosx
zip -9 -q ../release/botanim_${version_safe}_macosx_amd64.zip *
echo "Created Mac OS X release: build/release/botanim_${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/botanim.exe ../cmd/tins2020
cp ../README.md build/windows
cd build/windows
zip -9 -q ../release/botanim_${version_safe}_windows_amd64.zip *
echo "Created Windows release: build/release/botanim_${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/botanim.exe ../cmd/tins2020
cp ../README.md build/windows-allegro
cd build/windows-allegro
zip -9 -q ../release/botanim_allegro_${version_safe}_windows_amd64.zip *
echo "Created Windows (Allegro) release: build/release/botanim_allegro_${version_safe}_windows_amd64.zip"
cd ../..

View File

@ -1,6 +1,11 @@
package tins2020 package tins2020
import "os" import (
"os"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
)
type Settings struct { type Settings struct {
Window WindowSettings Window WindowSettings
@ -18,7 +23,7 @@ func (s *Settings) Init() error {
if _, err := os.Stat(path); os.IsNotExist(err) { if _, err := os.Stat(path); os.IsNotExist(err) {
return nil return nil
} }
return DecodeJSON(path, s) return zntg.DecodeJSON(path, s)
} }
func (s *Settings) Store() error { func (s *Settings) Store() error {
@ -26,11 +31,11 @@ func (s *Settings) Store() error {
if err != nil { if err != nil {
return err return err
} }
return EncodeJSON(path, s) return zntg.EncodeJSON(path, s)
} }
type WindowSettings struct { type WindowSettings struct {
Location *Point Location *geom.Point
Size *Point Size *geom.Point
VSync *bool VSync *bool
} }

View File

@ -3,129 +3,165 @@ package tins2020
import ( import (
"fmt" "fmt"
"github.com/veandco/go-sdl2/sdl" "opslag.de/schobers/zntg/play"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
) )
type terrainRenderer struct { type terrainRenderer struct {
ui.ControlBase
game *Game game *Game
hover *Point hover *geom.Point
project projection viewBounds geom.RectangleF32
interactBounds geom.RectangleF32
isometric *play.IsometricProjection
drag Drageable drag ui.Dragable
} }
func NewTerrainRenderer(game *Game) Control { func NewTerrainRenderer(game *Game) ui.Control {
return &terrainRenderer{game: game, project: newProjection()} renderer := &terrainRenderer{game: game, isometric: play.NewIsometricProjection(geom.PtF32(128, 64), geom.RectF32(0, 0, 100, 100))}
}
func (r *terrainRenderer) Arrange(ctx *Context, _ Rectangle) { renderer.game.CenterChanged().AddHandler(func(ctx ui.Context, state interface{}) {
r.project.update(ctx.Renderer) center := state.(geom.Point)
} renderer.isometric.MoveCenterTo(center.ToF32())
func (r *terrainRenderer) Init(ctx *Context) error {
r.game.CenterChanged().RegisterItf(func(state interface{}) {
center := state.(Point)
r.project.center = center.ToPtF()
r.project.update(ctx.Renderer)
}) })
r.project.update(ctx.Renderer) return renderer
return nil
} }
func isControlKeyDown() bool { func (r *terrainRenderer) Arrange(ctx ui.Context, bounds geom.RectangleF32, _ geom.PointF32, _ ui.Control) {
state := sdl.GetKeyboardState() r.viewBounds = geom.RectF32(buttonBarWidth, 0, bounds.Dx()-buttonBarWidth, bounds.Dy())
return state[sdl.SCANCODE_LCTRL] == 1 || state[sdl.SCANCODE_RCTRL] == 1 || state[sdl.SCANCODE_LGUI] == 1 || state[sdl.SCANCODE_RGUI] == 1 r.isometric.SetViewBounds(r.viewBounds)
r.interactBounds = r.viewBounds
r.interactBounds.Min.Y += 64
} }
func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) bool { func isControlKeyDown(ctx ui.Context) bool {
modifiers := ctx.KeyModifiers()
return modifiers&(ui.KeyModifierControl|ui.KeyModifierOSCommand) != 0
}
func (r *terrainRenderer) Handle(ctx ui.Context, event ui.Event) bool {
switch e := event.(type) { switch e := event.(type) {
case *sdl.MouseButtonEvent: case *ui.MouseButtonDownEvent:
if r.project.windowInteractRect.IsPointInside(e.X, e.Y) { pos := e.Pos()
if e.Type == sdl.MOUSEBUTTONDOWN { if pos.In(r.interactBounds) {
controlKeyDown := isControlKeyDown() controlKeyDown := isControlKeyDown(ctx)
if e.Button == sdl.BUTTON_MIDDLE || (e.Button == sdl.BUTTON_LEFT && controlKeyDown) { if e.Button == ui.MouseButtonMiddle || (e.Button == ui.MouseButtonLeft && controlKeyDown) {
if !r.drag.IsDragging() { if _, ok := r.drag.IsDragging(); !ok {
r.drag.Start(Pt(e.X, e.Y)) r.drag.Start(pos)
} }
} }
if e.Button == sdl.BUTTON_LEFT && !controlKeyDown { if e.Button == ui.MouseButtonLeft && !controlKeyDown {
pos := r.project.screenToMapInt(e.X, e.Y) pos := r.isometric.ViewToTileInt(pos)
r.game.UserClickedTile(pos) r.game.UserClickedTile(pos)
} }
if e.Button == sdl.BUTTON_RIGHT { if e.Button == ui.MouseButtonRight {
if e.Type == sdl.MOUSEBUTTONDOWN { r.game.CancelTool(ctx)
r.game.CancelTool()
} }
} }
} case *ui.MouseButtonUpEvent:
if e.Type == sdl.MOUSEBUTTONUP { if _, ok := r.drag.IsDragging(); ok {
if r.drag.IsDragging() { r.game.Terrain.Center = r.isometric.TileInt(r.isometric.Center())
r.game.Terrain.Center = mapToTile(r.project.center)
r.drag.Cancel() r.drag.Cancel()
} }
} case *ui.MouseMoveEvent:
} pos := e.Pos()
case *sdl.MouseMotionEvent: if pos.In(r.interactBounds) {
if r.project.windowInteractRect.IsPointInside(e.X, e.Y) { hover := r.isometric.ViewToTileInt(pos)
hover := r.project.screenToMapInt(e.X, e.Y)
r.hover = &hover r.hover = &hover
} else { } else {
r.hover = nil r.hover = nil
} }
if r.drag.IsDragging() { if _, ok := r.drag.IsDragging(); ok {
delta := r.drag.Move(Pt(e.X, e.Y)) delta, _ := r.drag.Move(pos)
r.project.center = r.project.center.Sub(r.project.screenToMapRel(delta.X, delta.Y)) r.isometric.Pan(delta.Invert())
r.project.update(ctx.Renderer)
} }
case *sdl.MouseWheelEvent:
if r.hover != nil { if r.hover != nil {
if e.Y < 0 { if e.MouseWheel < 0 {
r.project.ZoomOut(ctx, r.hover.ToPtF()) r.isometric.ZoomOut(r.hover.ToF32())
} else { } else if e.MouseWheel > 0 {
r.project.ZoomIn(ctx, r.hover.ToPtF()) r.isometric.ZoomIn(r.hover.ToF32())
} }
} }
case *sdl.WindowEvent: case *ui.MouseLeaveEvent:
if e.Event == sdl.WINDOWEVENT_LEAVE {
r.hover = nil r.hover = nil
r.project.update(ctx.Renderer) case *ui.KeyDownEvent:
switch e.Key {
case ui.KeyPadPlus:
r.isometric.ZoomIn(r.isometric.Center())
case ui.KeyMinus:
r.isometric.ZoomOut(r.isometric.Center())
case ui.KeyPadMinus:
r.isometric.ZoomOut(r.isometric.Center())
case ui.KeyW:
r.isometric.PanTile(geom.PtF32(-1, -1))
case ui.KeyA:
r.isometric.PanTile(geom.PtF32(-1, 1))
case ui.KeyS:
r.isometric.PanTile(geom.PtF32(1, 1))
case ui.KeyD:
r.isometric.PanTile(geom.PtF32(1, -1))
} }
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
switch e.Keysym.Sym {
case sdl.K_PLUS:
r.project.ZoomIn(ctx, r.project.center)
case sdl.K_KP_PLUS:
r.project.ZoomIn(ctx, r.project.center)
case sdl.K_MINUS:
r.project.ZoomOut(ctx, r.project.center)
case sdl.K_KP_MINUS:
r.project.ZoomOut(ctx, r.project.center)
case sdl.K_w:
r.project.Pan(ctx, PtF(-1, -1))
case sdl.K_a:
r.project.Pan(ctx, PtF(-1, 1))
case sdl.K_s:
r.project.Pan(ctx, PtF(1, 1))
case sdl.K_d:
r.project.Pan(ctx, PtF(1, -1))
} }
tool := r.game.Tool().Type()
if r.hover != nil {
if tool != "none" {
ctx.Renderer().SetMouseCursor(ui.MouseCursorPointer)
}
if tool == "plant-flower" {
terrain := r.game.Terrain
temp := func() string {
temp := terrain.Temp.Value(r.hover.X, r.hover.Y)
switch {
case temp < .3:
return "very cold"
case temp < .4:
return "cold"
case temp > .7:
return "very hot"
case temp > .6:
return "hot"
default:
return "moderate"
}
}()
humid := func() string {
humid := terrain.Humid.Value(r.hover.X, r.hover.Y)
switch {
case humid < .3:
return " and very arid"
case humid < .4:
return " and arid"
case humid > .7:
return " and very damp"
case humid > .6:
return " and damp"
default:
return ""
}
}()
ctx.ShowTooltip(fmt.Sprintf("It is %s%s over here", temp, humid))
} }
} }
return false return false
} }
func (r *terrainRenderer) Render(ctx *Context) { func (r *terrainRenderer) Render(ctx ui.Context) {
zoom := r.isometric.Zoom()
terrain := r.game.Terrain terrain := r.game.Terrain
toTileTexture := func(x, y int32) *Texture { toTileTexture := func(tile geom.Point) ui.Texture {
temp := terrain.Temp.Value(x, y) temp := terrain.Temp.Value(tile.X, tile.Y)
if temp < .35 { if temp < .35 {
return ctx.Textures.Texture("tile-snow") return ctx.Textures().ScaledByName("tile-snow", zoom)
} }
if temp > .65 { if temp > .65 {
return ctx.Textures.Texture("tile-dirt") return ctx.Textures().ScaledByName("tile-dirt", zoom)
} }
return ctx.Textures.Texture("tile-grass") return ctx.Textures().ScaledByName("tile-grass", zoom)
} }
variantToInt := func(variant float64) int { variantToInt := func(variant float64) int {
@ -144,14 +180,13 @@ func (r *terrainRenderer) Render(ctx *Context) {
return -1 return -1
} }
variantToTexture := func(format string, variant float64) *Texture { variantToTexture := func(format string, variant float64) ui.Texture {
textName := fmt.Sprintf(format, variantToInt(variant)) textName := fmt.Sprintf(format, variantToInt(variant))
return ctx.Textures.Texture(textName) return ctx.Textures().ScaledByName(textName, zoom)
} }
stretch := func(x, from, to float64) float64 { return (x - from) * 1 / (to - from) } stretch := func(x, from, to float64) float64 { return (x - from) * 1 / (to - from) }
toPropTexture := func(temp, humid, variant float64) *Texture { toPropTexture := func(temp, humid, variant float64) ui.Texture {
if temp < .35 { if temp < .35 {
if humid < .2 { if humid < .2 {
return nil return nil
@ -181,12 +216,12 @@ func (r *terrainRenderer) Render(ctx *Context) {
return variantToTexture("bush-large-%d", stretch(variant, .8, 1)*multiplier) return variantToTexture("bush-large-%d", stretch(variant, .8, 1)*multiplier)
} }
toItemTexture := func(x, y int32) *Texture { toItemTexture := func(x, y int) ui.Texture {
variant := terrain.Variant.Value(x, y) variant := terrain.Variant.Value(x, y)
flower, ok := terrain.Flowers[Pt(x, y)] flower, ok := terrain.Flowers[geom.Pt(x, y)]
if ok { if ok {
desc, _ := r.game.Herbarium.Find(flower.ID) desc, _ := r.game.Herbarium.Find(flower.ID)
return ctx.Textures.Texture(desc.IconTemplate.Variant(variantToInt(variant))) return ctx.Textures().ScaledByName(desc.IconTemplate.Variant(variantToInt(variant)), zoom)
} }
temp := terrain.Temp.Value(x, y) temp := terrain.Temp.Value(x, y)
humid := terrain.Humid.Value(x, y) humid := terrain.Humid.Value(x, y)
@ -197,24 +232,35 @@ func (r *terrainRenderer) Render(ctx *Context) {
// vertical (tile): [96,160) = 64 // vertical (tile): [96,160) = 64
// vertical (total): [0,160) = 160 // vertical (total): [0,160) = 160
r.project.visibleTiles(func(x, y int32, pos Point) { topLeft := geom.PtF32(-64*zoom, -112*zoom)
text := toTileTexture(x, y) bottomRight := geom.PtF32(64*zoom, 48*zoom)
rect := r.project.screenToTileRect(pos) textureRect := func(center geom.PointF32) geom.RectangleF32 {
text.CopyResize(ctx.Renderer, rect) return geom.RectangleF32{Min: center.Add(topLeft), Max: center.Add(bottomRight)}
}
hoverTexture := ctx.Textures().ScaledByName("tile-hover", zoom)
if r.hover != nil && x == r.hover.X && y == r.hover.Y { r.isometric.EnumerateInt(func(tile geom.Point, view geom.PointF32) {
ctx.Textures.Texture("tile-hover").CopyResize(ctx.Renderer, rect) text := toTileTexture(tile)
rect := textureRect(view)
ctx.Renderer().DrawTexture(text, rect)
// if r.game.Debug {
// ctx.Renderer().FillRectangle(view.Add2D(-1, -1).RectRel2D(2, 2), color.White)
// ctx.Fonts().TextAlign("debug", view, color.White, fmt.Sprintf("%d, %d", tile.X, tile.Y), ui.AlignCenter)
// }
if r.hover != nil && tile.X == r.hover.X && tile.Y == r.hover.Y {
ctx.Renderer().DrawTexture(hoverTexture, rect)
} }
}) })
r.project.visibleTiles(func(x, y int32, pos Point) { r.isometric.EnumerateInt(func(tile geom.Point, view geom.PointF32) {
text := toItemTexture(x, y) text := toItemTexture(tile.X, tile.Y)
if text == nil { if text == nil {
return return
} }
placeX, placeY := terrain.PlaceX.Value(x, y), terrain.PlaceY.Value(x, y) placeX, placeY := terrain.PlaceX.Value(tile.X, tile.Y), terrain.PlaceY.Value(tile.X, tile.Y)
pos = r.project.mapToScreenF(float32(x)-.2+float32(.9*placeX-.45), float32(y)-.2+float32(.9*placeY-.45)) displaced := r.isometric.TileToView(tile.ToF32().Add2D(-.2+.9*float32(placeX)-.45, -.2+.9*float32(placeY)-.45))
text.CopyResize(ctx.Renderer, r.project.screenToTileRect(pos)) rect := textureRect(displaced)
ctx.Renderer().DrawTexture(text, rect)
}) })
} }

View File

@ -1,116 +0,0 @@
package tins2020
import (
"errors"
"fmt"
"github.com/veandco/go-sdl2/img"
"github.com/veandco/go-sdl2/sdl"
"opslag.de/schobers/fs/vfs"
)
type Texture struct {
texture *sdl.Texture
size Point
}
func NewTextureFromSurface(renderer *sdl.Renderer, surface *sdl.Surface) (*Texture, error) {
texture, err := renderer.CreateTextureFromSurface(surface)
if err != nil {
return nil, err
}
return &Texture{texture: texture, size: Pt(surface.W, surface.H)}, nil
}
func (t *Texture) Size() Point { return t.size }
// func (t *Texture) Rect() Rectangle { return t.rect }
// func (t *Texture) SDLRectPtr() *sdl.Rect { return t.rect.SDLPtr() }
func (t *Texture) Copy(renderer *sdl.Renderer, dst Point) {
t.CopyResize(renderer, Rect(dst.X, dst.Y, t.size.X, t.size.Y))
}
func (t *Texture) CopyPart(renderer *sdl.Renderer, src Rectangle, dst Point) {
t.CopyPartResize(renderer, src, Rect(dst.X, dst.Y, src.W, src.H))
}
func (t *Texture) CopyPartResize(renderer *sdl.Renderer, src Rectangle, dst Rectangle) {
renderer.Copy(t.texture, src.SDLPtr(), dst.SDLPtr())
}
func (t *Texture) CopyResize(renderer *sdl.Renderer, dst Rectangle) {
t.CopyPartResize(renderer, RectAbs(0, 0, t.size.X, t.size.Y), dst)
}
func (t *Texture) SetColor(color sdl.Color) {
t.texture.SetColorMod(color.R, color.G, color.B)
}
// func (t *Texture) CopyF(renderer *sdl.Renderer, dst *sdl.FRect) {
// renderer.CopyF(t.texture, t.rect, dst) // Depends on SDL >=2.0.10
// }
func (t *Texture) Destroy() { t.texture.Destroy() }
type Textures struct {
dir vfs.CopyDir
renderer *sdl.Renderer
textures map[string]*Texture
}
func (t *Textures) Init(renderer *sdl.Renderer, dir vfs.CopyDir) {
t.dir = dir
t.renderer = renderer
t.textures = map[string]*Texture{}
}
func (t *Textures) Load(name, path string, other ...string) error {
err := t.load(name, path)
if err != nil {
return err
}
if len(other) == 0 {
return nil
}
if len(other)%2 != 0 {
return errors.New("expected name/path pairs")
}
for i := 0; i < len(other); i += 2 {
err = t.load(other[i], other[i+1])
if err != nil {
return fmt.Errorf("error loading '%s'; error: %v", other[i], err)
}
}
return nil
}
func (t *Textures) load(name, path string) error {
texturePath, err := t.dir.Retrieve(path)
if err != nil {
return err
}
surface, err := img.Load(texturePath)
if err != nil {
return err
}
defer surface.Free()
texture, err := NewTextureFromSurface(t.renderer, surface)
if err != nil {
return err
}
t.textures[name] = texture
return nil
}
func (t *Textures) Texture(name string) *Texture { return t.textures[name] }
func (t *Textures) Destroy() {
if t.textures == nil {
return
}
for _, t := range t.textures {
t.Destroy()
}
}

View File

@ -1,8 +1,10 @@
package tins2020 package tins2020
import "opslag.de/schobers/geom"
type Tool interface { type Tool interface {
Type() string Type() string
ClickedTile(*Game, Point) ClickedTile(*Game, geom.Point)
} }
type PlantFlowerTool struct { type PlantFlowerTool struct {
@ -11,14 +13,20 @@ type PlantFlowerTool struct {
func (t *PlantFlowerTool) Type() string { return "plant-flower" } func (t *PlantFlowerTool) Type() string { return "plant-flower" }
func (t *PlantFlowerTool) ClickedTile(game *Game, tile Point) { func (t *PlantFlowerTool) ClickedTile(game *Game, tile geom.Point) {
game.PlantFlower(t.FlowerID, tile) game.PlantFlower(t.FlowerID, tile)
} }
type NoTool struct{}
func (t *NoTool) Type() string { return "none" }
func (t *NoTool) ClickedTile(*Game, geom.Point) {}
type ShovelTool struct{} type ShovelTool struct{}
func (t *ShovelTool) Type() string { return "shovel" } func (t *ShovelTool) Type() string { return "shovel" }
func (t *ShovelTool) ClickedTile(game *Game, tile Point) { func (t *ShovelTool) ClickedTile(game *Game, tile geom.Point) {
game.Dig(tile) game.Dig(tile)
} }

View File

@ -1,72 +0,0 @@
package tins2020
import "github.com/veandco/go-sdl2/sdl"
type Tooltip struct {
ControlBase
Text string
}
const tooltipBorderThickness = 1
const tooltipHorizontalPadding = 6
const tooltipVerticalPadding = 2
const tooltipMouseDistance = 12
func (t *Tooltip) ActualFontName() string {
if t.FontName == "" {
return "small"
}
return t.FontName
}
func (t *Tooltip) Handle(ctx *Context, event sdl.Event) bool {
if len(t.Text) == 0 {
return false
}
font := ctx.Fonts.Font(t.ActualFontName())
if font == nil {
font = ctx.Fonts.Font("default")
}
windowW, windowH, err := ctx.Renderer.GetOutputSize()
if err != nil {
return false
}
labelW, labelH, err := font.SizeUTF8(t.Text)
if err != nil {
return false
}
mouse := ctx.MousePosition
width := int32(labelW) + 2*tooltipBorderThickness + 2*tooltipHorizontalPadding
height := int32(labelH) + 2*tooltipBorderThickness + 2*tooltipVerticalPadding
left := mouse.X + tooltipMouseDistance
top := mouse.Y + tooltipMouseDistance
if left+width > windowW {
left = mouse.X - tooltipMouseDistance - width
}
if top+height > windowH {
top = mouse.Y - tooltipMouseDistance - height
}
t.Bounds = Rect(left, top, width, height)
return false
}
func (t *Tooltip) Render(ctx *Context) {
almostBlack := MustHexColor("#0000007f")
SetDrawColor(ctx.Renderer, almostBlack)
ctx.Renderer.FillRect(t.Bounds.SDLPtr())
almostWhite := MustHexColor("ffffff7f")
SetDrawColor(ctx.Renderer, almostWhite)
ctx.Renderer.DrawRect(t.Bounds.SDLPtr())
font := ctx.Fonts.Font(t.ActualFontName())
bottomLeft := Pt(t.Bounds.X+tooltipBorderThickness+tooltipHorizontalPadding, t.Bounds.Y+t.Bounds.H-tooltipBorderThickness-tooltipVerticalPadding)
font.RenderCopy(ctx.Renderer, t.Text, bottomLeft, White)
}