Compare commits

..

No commits in common. "master" and "botanium_1_0_0" have entirely different histories.

49 changed files with 2053 additions and 1482 deletions

1
.gitignore vendored
View File

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

View File

@ -1,64 +1,7 @@
# TINS 2020
## Content
- [Introduction](##Introduction)
- [Additional Rules](##Additional-Rules)
* [Implementation](###Implementation)
* [Definition](###Definition)
- [Building](##Building)
- [Sources](##Sources)
- [Licenses (third party)](##Licenses)
* [Go-SDL2](###Go-SDL2)
* [SDL 2.0](###SDL-2.0)
* [Fira Mono font](###Fira-Mono-font)
* [Open Sans font](###Open-Sans-font)
## Introduction
**Welcome to Botanim!**
In Botanim you play the role of botanist and your goal is to cultivate flowers in an open landscape.
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.
**Controls:**
- H: Selects harvest tool
- R: Selects research
- Spacebar: pauses game
- 1: runs game at normal speed
- 2: runs game extra fast
- Mouse wheel or plus/minus: zooms landscape
- W, A, S, D keys or CTRL + left mouse button or middle mouse button: pans landscape
** On screen **
On the left side of the playing screen you'll find several buttons:
- Settings: disabled.
- Save: saves the game instantly, only single slot.
- Load: loads previously saved game instantly.
- New: starts a new game with a different terrain.
- Information: shows the intro/information screen (again, also accessible with the Escape key).
Have fun playing!
## Additional Rules
### Implementation
**genre rule #130**
This rule is the most obvious. The goal of the game is to grow flowers so given enough time and a bit of luck your screen will be full of flowers.
**artistical rule #60**
The least obvious and not finished within the time contraint: when researching you'll have to dial a phone number like you used to do with an old rotary dial (works by repeating the number on the keyboard an exact number of times).
**technical rule #10**
The terrain is based on (several: humidity, temperature, placement of props, variants of props) perlin noise functions.
**technical rule #68**
The description of the flowers will scroll below the icon of the "buy flower" buttons when hovered.
### Definition
### Genre requirement
**** There will be 1 genre rule
@ -72,7 +15,7 @@ Comments: This rule just reminds me of my favourite platformer, Super Mario Worl
**artistical rule #60**
make fun of old-fashioned things.
Comments: e.g. have a mode in which the game plays in black and white, featuring an old granny in the corner saying "I didn't pay my colour television license for this", or occasionally "I was in a war" Some more inspirations [here](https://www.youtube.com/watch?v=67OPI3cXAaE) and [here](https://twitter.com/Bill_Gross/status/920406104911233024)
Comments:E.g. have a mode in which the game plays in black and white, featuring an old granny in the corner saying "I didn't pay my colour television license for this", or occasionally "I was in a war" Some more inspirations [here](https://www.youtube.com/watch?v=67OPI3cXAaE) and [here](https://twitter.com/Bill_Gross/status/920406104911233024)
### Technical requirement
**** There will be 2 technical rules
@ -102,31 +45,16 @@ Comments: Lots of wiggle room here. For example, you could revert the last techn
## Building
Prerequisites:
- [SDL 2.0](https://www.libsdl.org/) (SDL2, SDL2_image, SDL2_ttf and SDL2_gfx development libraries are required; for [more information](https://github.com/veandco/go-sdl2)).
- [SDL 2.0](https://www.libsdl.org/) (SDL2, SDL2_image, SDL2_ttf and SDL2_gfx development libraries are required).
- [Go](https://golang.org/dl/) 1.12 or later.
- GCC or a compatible compiler.
- [Git](https://git-scm.com/download).
- GCC.
- Git.
With all prequisites installed you can run:
```
go get -u opslag.de/schobers/tins2020/cmd/tins2020
```
This will create the binary `$HOME/go/bin/tins2020`. Additionally you can embed the resources and build a static release with the following commands (this assumes `$HOME/go/bin` is available in the `PATH`):
```
go generate opslag.de/schobers/tins2020/cmd/tins2020
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
Can be found at https://opslag.de/schobers/tins2020 (Git repository).

57
animation.go Normal file
View File

@ -0,0 +1,57 @@
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
}

59
buttonbar.go Normal file
View File

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

View File

@ -1,37 +0,0 @@
package main
import (
"io"
"os"
"path/filepath"
rice "github.com/GeertJohan/go.rice"
)
func copyFile(path string, file *rice.File) error {
dir := filepath.Dir(path)
os.MkdirAll(dir, 0777)
dst, err := os.Create(path)
if err != nil {
return err
}
defer dst.Close()
_, err = io.Copy(dst, file)
return err
}
func copyBoxToDisk(box *rice.Box) error {
return box.Walk("", func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
src, err := box.Open(path)
if err != nil {
return err
}
return copyFile(filepath.Join(box.Name(), path), src)
})
}

View File

@ -1,17 +1,11 @@
package main
import (
"flag"
"image/color"
"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"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
"opslag.de/schobers/tins2020"
)
@ -25,158 +19,136 @@ func main() {
}
}
func openResources(box *rice.Box) ui.Resources {
embedded := riceres.New(box)
return ui.NewFallbackResources(ui.NewPathResources(nil, box.Name()), embedded)
}
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 logSDLVersion() {
var version sdl.Version
sdl.GetVersion(&version)
log.Printf("SDL version: %d.%d.%d", version.Major, version.Minor, version.Patch)
}
func run() error {
var extract bool
flag.BoolVar(&extract, "extract-resources", false, "extracts all resources to the current working directory")
flag.Parse()
box := rice.MustFindBox(`res`)
if extract {
return copyBoxToDisk(box)
if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil {
return err
}
res := openResources(box)
defer sdl.Quit()
ptPtr := func(x, y int) *geom.Point {
p := geom.Pt(x, y)
return &p
logSDLVersion()
if err := ttf.Init(); err != nil {
return err
}
defer ttf.Quit()
settings := &tins2020.Settings{}
err := settings.Init()
ctx, err := tins2020.NewContext(rice.MustFindBox("res"))
if err != nil {
return err
}
defer settings.Store()
defer ctx.Destroy()
var location *geom.PointF32
if settings.Window.Location != nil {
location = &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)}
if ctx.Settings.Window.Location == nil {
ctx.Settings.Window.Location = tins2020.PtPtr(sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED)
}
if settings.Window.Size == nil {
settings.Window.Size = ptPtr(800, 600)
if ctx.Settings.Window.Size == nil {
ctx.Settings.Window.Size = tins2020.PtPtr(800, 600)
}
if settings.Window.VSync == nil {
if ctx.Settings.Window.VSync == nil {
vsync := true
settings.Window.VSync = &vsync
ctx.Settings.Window.VSync = &vsync
}
renderer, err := ui.NewRenderer("Botanim - TINS 2020", settings.Window.Size.X, settings.Window.Size.Y, ui.NewRendererOptions{
Location: location,
Resizable: true,
VSync: *settings.Window.VSync,
})
if *ctx.Settings.Window.VSync {
sdl.SetHint(sdl.HINT_RENDER_VSYNC, "1")
}
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()
renderer.SetResourceProvider(func() ui.Resources { return res })
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 {
return err
}
game := tins2020.NewGame()
app := &app{
game: game,
dialogs: tins2020.NewDialogs(game),
settings: settings,
app := tins2020.NewContainer()
overlays := tins2020.NewContainer()
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
}
style := ui.DefaultStyle()
style.Palette = &ui.Palette{
Background: zntg.MustHexColor(`#356DAD`),
Disabled: zntg.MustHexColor(`#DEDEDE`),
Primary: zntg.MustHexColor(`#356DAD`),
PrimaryDark: zntg.MustHexColor(`#15569F`),
PrimaryLight: zntg.MustHexColor(`#ABCAED`),
Secondary: zntg.MustHexColor(`#4AC69A`),
SecondaryDark: zntg.MustHexColor(`#0AA36D`),
SecondaryLight: zntg.MustHexColor(`#A6EED4`),
Text: color.White,
TextOnPrimary: color.White,
TextOnSecondary: color.White,
TextNegative: zntg.MustHexColor(`#F3590E`),
TextPositive: zntg.MustHexColor(`#65D80D`),
dialogs.ShowIntro(ctx)
w, h := window.GetSize()
app.Arrange(ctx, tins2020.Rect(0, 0, w, h))
for {
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch e := event.(type) {
case *sdl.QuitEvent:
ctx.Quit()
break
case *sdl.WindowEvent:
switch e.Event {
case sdl.WINDOWEVENT_MOVED:
x, y := window.GetPosition()
ctx.Settings.Window.Location = tins2020.PtPtr(x, y)
case sdl.WINDOWEVENT_SIZE_CHANGED:
w, h := window.GetSize()
app.Arrange(ctx, tins2020.Rect(0, 0, w, h))
ctx.Settings.Window.Size = tins2020.PtPtr(w, h)
}
return ui.Run(renderer, style, app)
}
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

@ -1,16 +0,0 @@
// +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

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

30
color.go Normal file
View File

@ -0,0 +1,30 @@
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))
}

33
color_test.go Normal file
View File

@ -0,0 +1,33 @@
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)
}
}
}

View File

@ -1,140 +0,0 @@
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}
}

58
container.go Normal file
View File

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

45
context.go Normal file
View File

@ -0,0 +1,45 @@
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
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 }

59
control.go Normal file
View File

@ -0,0 +1,59 @@
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
IsDisabled bool
IsMouseOver bool
OnLeftMouseButtonClick EventContextFn
}
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) {}

137
dial.go
View File

@ -1,137 +0,0 @@
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()
}
}

View File

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

24
drageable.go Normal file
View File

@ -0,0 +1,24 @@
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
}

33
eventhandler.go Normal file
View File

@ -0,0 +1,33 @@
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,
Life: 0.99993,
Resistance: FlowerResistance{
Cold: 0.6,
Hot: 0.9,
Cold: 0.7,
Hot: 0.8,
Dry: 0.8,
Wet: 0.5,
},

109
fonts.go Normal file
View File

@ -0,0 +1,109 @@
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()
}
}

46
fpsrenderer.go Normal file
View File

@ -0,0 +1,46 @@
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})
}

133
game.go
View File

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

View File

@ -1,24 +1,19 @@
package tins2020
import (
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/play"
"opslag.de/schobers/zntg/ui"
"github.com/veandco/go-sdl2/sdl"
)
const fpsOverlayName = "fps"
type GameControls struct {
ui.ContainerBase
Container
game *Game
dialogs *Dialogs
menu ui.StackPanel
top ui.StackPanel
flowers ui.StackPanel
otherTools ui.StackPanel
menu ButtonBar
top ButtonBar
flowers ButtonBar
otherTools ButtonBar
pause *IconButton
run *IconButton
@ -32,114 +27,6 @@ func NewGameControls(game *Game, dialogs *Dialogs) *GameControls {
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 {
flower, _ := c.game.Herbarium.Find(id)
return NewBuyFlowerButton(
@ -147,106 +34,159 @@ func (c *GameControls) createBuyFlowerButton(id string) *BuyFlowerButton {
flower.IconTemplate.Disabled(),
id,
flower,
func(ctx ui.Context) {
c.game.SelectPlantFlowerTool(ctx, id)
},
EmptyEvent(func() {
c.game.SelectPlantFlowerTool(id)
}),
)
}
func (c *GameControls) speedChanged(_ ui.Context, state interface{}) {
func (c *GameControls) speedChanged(state interface{}) {
speed := state.(GameSpeed)
disable := func(b *IconButton, expected GameSpeed) {
b.Disabled = speed == expected
b.IsDisabled = speed == expected
}
disable(c.pause, GameSpeedPaused)
disable(c.run, GameSpeedNormal)
disable(c.runFast, GameSpeedFast)
}
func (c *GameControls) toolChanged(_ ui.Context, state interface{}) {
func (c *GameControls) toolChanged(state interface{}) {
tool, _ := state.(Tool)
var flowerID string
if tool, ok := tool.(*PlantFlowerTool); ok {
flowerID = tool.FlowerID
}
for _, control := range c.flowers.Children {
for _, control := range c.flowers.Buttons {
button := control.(*BuyFlowerButton)
button.Active = button.FlowerID == flowerID
button.Disabled = !c.game.Herbarium.IsUnlocked(button.FlowerID)
button.IsActive = button.FlowerID == flowerID
button.IsDisabled = !c.game.Herbarium.IsUnlocked(button.FlowerID)
}
_, shovel := tool.(*ShovelTool)
c.shovel.Active = shovel
c.shovel.IsActive = shovel
}
func (c *GameControls) updateFlowerControls() {
for _, b := range c.flowers.Children {
func (c *GameControls) updateFlowerControls(ctx *Context) {
for _, b := range c.flowers.Buttons {
button := b.(*BuyFlowerButton)
flower, ok := c.game.Herbarium.Find(button.FlowerID)
if ok {
button.Update(flower)
button.Update(ctx, flower)
}
}
}
const buttonBarWidth = 96
func (c *GameControls) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) {
c.ContainerBase.Arrange(ctx, bounds, offset, parent)
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) Arrange(ctx *Context, bounds Rectangle) {
c.Bounds = bounds
c.menu.Arrange(ctx, RectSize(bounds.X, bounds.Y, buttonBarWidth, bounds.H))
c.top.Arrange(ctx, Rect(bounds.X+bounds.W/2+8, bounds.Y, bounds.Right(), bounds.Y+64))
c.flowers.Arrange(ctx, RectSize(bounds.Right()-buttonBarWidth, bounds.Y, buttonBarWidth, bounds.H))
c.otherTools.Arrange(ctx, RectSize(bounds.Right()-buttonBarWidth, bounds.Bottom()-2*buttonBarWidth, buttonBarWidth, 2*buttonBarWidth))
}
func (c *GameControls) Handle(ctx ui.Context, event ui.Event) bool {
if c.ContainerBase.Handle(ctx, event) {
func (c *GameControls) Init(ctx *Context) error {
c.game.SpeedChanged().RegisterItf(c.speedChanged)
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 = NewIconButtonConfig("control-pause", EmptyEvent(func() {
c.game.Pause()
}), func(b *IconButton) {
b.IconDisabled = "control-pause-disabled"
})
c.run = NewIconButtonConfig("control-run", EmptyEvent(func() {
c.game.Run()
}), func(b *IconButton) {
b.IconDisabled = "control-run-disabled"
})
c.runFast = NewIconButtonConfig("control-run-fast", EmptyEvent(func() {
c.game.RunFast()
}), func(b *IconButton) {
b.IconDisabled = "control-run-fast-disabled"
})
c.speedChanged(c.game.Speed)
c.top.Buttons = []Control{c.pause, c.run, c.runFast}
c.menu.Background = MustHexColor("#356dad")
c.menu.Buttons = []Control{
NewIconButtonConfig("control-settings", c.dialogs.ShowSettings, func(b *IconButton) {
b.IsDisabled = true
b.IconDisabled = "#afafaf"
}),
NewIconButton("control-save", func(*Context) { c.game.Save() }),
NewIconButton("control-load", func(ctx *Context) {
c.game.Load()
c.updateFlowerControls(ctx)
}),
NewIconButton("control-new", func(ctx *Context) {
c.game.New()
c.updateFlowerControls(ctx)
}),
NewIconButton("control-information", c.dialogs.ShowIntro),
}
c.shovel = NewIconButtonConfig("control-shovel", func(*Context) { c.game.SelectShovel() }, func(b *IconButton) { b.IconHeight = 32 })
c.research = NewIconButtonConfig("control-research", c.dialogs.ShowResearch, func(b *IconButton) { b.IconHeight = 32 })
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
}
switch e := event.(type) {
case *ui.KeyDownEvent:
switch e.Key {
case ui.KeySpace:
c.game.TogglePause(ctx)
case ui.Key1:
c.game.Run(ctx)
case ui.Key2:
c.game.RunFast(ctx)
case ui.KeyH:
c.game.SelectShovel(ctx)
case ui.KeyR:
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
switch e.Keysym.Sym {
case sdl.K_SPACE:
c.game.TogglePause()
case sdl.K_1:
c.game.Run()
case sdl.K_2:
c.game.RunFast()
case sdl.K_d:
c.game.SelectShovel()
case sdl.K_r:
c.dialogs.ShowResearch(ctx)
case ui.KeyEscape:
if c.game.Tool().Type() == "none" {
case sdl.K_ESCAPE:
if c.game.Tool() == nil {
c.dialogs.ShowIntro(ctx)
} else {
c.game.CancelTool(ctx)
c.game.CancelTool()
}
return true
case ui.KeyF4:
case sdl.K_F3:
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
}
func (c *GameControls) Render(ctx ui.Context) {
topBar := zntg.MustHexColor("#0000007F")
ctx.Renderer().FillRectangle(geom.RectF32(c.menu.Bounds().Max.X, 0, c.flowers.Bounds().Min.X, 64), topBar)
ctx.Fonts().TextAlign("balance", geom.PtF32(c.top.Bounds().Min.X-8, 4), zntg.MustHexColor("#4AC69A"), FmtMoney(c.game.Balance), ui.AlignRight)
func (c *GameControls) Render(ctx *Context) {
topBar := MustHexColor("#0000007f")
SetDrawColor(ctx.Renderer, topBar)
ctx.Renderer.FillRect(Rect(c.menu.Bounds.Right(), 0, c.flowers.Bounds.X, 64).SDLPtr())
ctx.Fonts.Font("balance").RenderCopyAlign(ctx.Renderer, FmtMoney(c.game.Balance), Pt(c.top.Bounds.X-8, 58), MustHexColor("#4AC69A"), TextAlignmentRight)
c.ContainerBase.Render(ctx)
c.Container.Render(ctx)
}

View File

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

View File

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

View File

@ -1,47 +1,91 @@
package tins2020
import (
"image/color"
type HoverEffect int
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
const (
HoverEffectLigthen HoverEffect = iota
HoverEffectColor
)
type IconButton struct {
ui.Button
ControlBase
Active bool
Icon string
IconDisabled string
IconHeight int32
IconScale Scale
IconWidth int32
IconActive HoverEffect
IconHover HoverEffect
IsActive bool
}
func NewIconButton(icon string, click ui.EventEmptyFn) *IconButton {
b := &IconButton{
Button: ui.Button{
Icon: icon,
IconHeight: 48,
Type: ui.ButtonTypeText,
HoverColor: hoverTransparentColor,
func NewIconButton(icon string, onClick EventContextFn) *IconButton {
return &IconButton{
ControlBase: ControlBase{
OnLeftMouseButtonClick: onClick,
},
Icon: icon,
}
b.Font.Color = color.White
b.ButtonClicked().AddHandler(func(ctx ui.Context, _ ui.ControlClickedArgs) { click(ctx) })
return b
}
func NewIconButtonConfigure(icon string, click ui.EventEmptyFn, configure func(*IconButton)) *IconButton {
button := NewIconButton(icon, click)
func NewIconButtonConfig(icon string, onClick EventContextFn, configure func(*IconButton)) *IconButton {
button := NewIconButton(icon, onClick)
configure(button)
return button
}
var hoverTransparentColor = zntg.MustHexColor(`#FFFFFF1F`)
func (b *IconButton) Render(ctx ui.Context) {
b.RenderActive(ctx)
b.Button.Render(ctx)
func (b *IconButton) activeTexture(ctx *Context) *Texture {
if b.IsDisabled {
texture := ctx.Textures.Texture(b.IconDisabled)
if texture != nil {
return texture
}
func (b *IconButton) RenderActive(ctx ui.Context) {
if b.Active || (!b.Disabled && b.IsOver()) {
ctx.Renderer().FillRectangle(b.Bounds(), hoverTransparentColor)
texture = ctx.Textures.Texture(b.Icon)
if len(b.IconDisabled) == 0 {
return texture
}
color, err := HexColor(b.IconDisabled)
if err == nil {
texture.SetColor(color)
}
return texture
}
return ctx.Textures.Texture(b.Icon)
}
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, RectSize(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)
}
type Scale int
const (
ScaleCenter Scale = iota
ScaleStretch
)

62
img/color.go Normal file
View File

@ -0,0 +1,62 @@
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
}

29
img/io.go Normal file
View File

@ -0,0 +1,29 @@
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,31 +1,27 @@
package tins2020
import (
"image/color"
"opslag.de/schobers/zntg/ui"
)
type Intro struct {
ui.Paragraph
LargeDialog
welcome Paragraph
}
func NewIntro() *LargeDialog {
i := &Intro{}
i.Font.Color = color.White
i.Text =
func (i *Intro) Init(ctx *Context) error {
i.welcome.Text =
"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" +
"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 dig up flowers again to collect more money.\n\n" +
"Controls:\n" +
" - H: Selects harvest tool\n" +
" - D: Selects shovel\n" +
" - R: Selects research\n" +
" - Spacebar: pauses game\n" +
" - 1: runs game at normal speed\n" +
" - 2: runs game extra fast\n" +
" - Mouse wheel or plus/minus: zooms landscape\n" +
" - W, A, S, D keys or CTRL + left mouse button or middle mouse button: pans landscape\n" +
" - CTRL + left mouse button or middle mouse button: pans landscape\n" +
"\n" +
"Have fun playing!"
return NewLargeDialog("Botanim", i)
i.SetContent(&i.welcome)
return i.LargeDialog.Init(ctx)
}

47
io.go
View File

@ -1,11 +1,50 @@
package tins2020
import (
"opslag.de/schobers/zntg"
"encoding/json"
"os"
"path/filepath"
)
const appName = "tins2020_botanim"
func DecodeJSON(path string, v interface{}) error {
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 UserDir() (string, error) { return zntg.UserDir(appName) }
func EncodeJSON(path string, v interface{}) error {
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
return json.NewEncoder(f).Encode(v)
}
func UserFile(name string) (string, error) { return zntg.UserFile(appName, name) }
func UserDir() (string, error) {
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
}

104
label.go Normal file
View File

@ -0,0 +1,104 @@
package tins2020
import (
"strings"
"github.com/veandco/go-sdl2/sdl"
)
type Label struct {
ControlBase
FontColor sdl.Color
FontName string
Text string
Alignment TextAlignment
}
func (l *Label) fontColor() sdl.Color {
var none sdl.Color
if l.FontColor == none {
return MustHexColor("#ffffff")
}
return l.FontColor
}
func (l *Label) fontName() string {
if len(l.FontName) == 0 {
return "default"
}
return l.FontName
}
func (l *Label) Render(ctx *Context) {
font := ctx.Fonts.Font(l.fontName())
color := l.fontColor()
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.fontName())
color := p.fontColor()
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,100 +1,109 @@
package tins2020
import (
"image/color"
import "github.com/veandco/go-sdl2/sdl"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
)
type DialogBase struct {
Container
const titleBarHeight = 64
content Proxy
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 {
ui.StackPanel
DialogBase
titleBar *LargeDialogTitleBar
content ui.Proxy
closeRequested ui.Events
title Label
close IconButton
}
func NewLargeDialog(title string, content ui.Control) *LargeDialog {
dialog := &LargeDialog{}
dialog.Orientation = ui.OrientationVertical
dialog.titleBar = NewLargeDialogTitleBar(title, func(ctx ui.Context, state interface{}) {
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) Arrange(ctx *Context, bounds Rectangle) {
const titleHeight = 64
d.ControlBase.Arrange(ctx, bounds)
d.title.Arrange(ctx, RectSize(bounds.X, bounds.Y, bounds.W, titleHeight))
d.close.Arrange(ctx, RectSize(bounds.W-64, 0, 64, 64))
d.content.Arrange(ctx, RectSize(bounds.X+titleHeight, 96, bounds.W-2*titleHeight, bounds.H-titleHeight))
}
func (d *LargeDialog) CloseRequested() ui.EventHandler { return &d.closeRequested }
func (d *LargeDialog) Init(ctx *Context) error {
d.title = Label{
Text: "Botanim",
FontName: "title",
Alignment: TextAlignmentCenter,
}
d.close = IconButton{
Icon: "control-cancel",
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 ui.Context, e ui.Event) bool {
if d.StackPanel.Handle(ctx, e) {
func (d *LargeDialog) Handle(ctx *Context, event sdl.Event) bool {
if d.DialogBase.Handle(ctx, event) {
return true
}
switch e := e.(type) {
case *ui.KeyDownEvent:
switch e.Key {
case ui.KeyEscape:
d.closeRequested.Notify(ctx, nil)
switch e := event.(type) {
case *sdl.KeyboardEvent:
if e.Type == sdl.KEYDOWN {
switch e.Keysym.Sym {
case sdl.K_ESCAPE:
d.CloseDialog()
return true
case ui.KeyEnter:
d.closeRequested.Notify(ctx, nil)
case sdl.K_RETURN:
d.CloseDialog()
return true
}
}
}
return false
}
func (d *LargeDialog) Hidden() { d.content.Hidden() }
func (d *LargeDialog) Render(ctx *Context) {
SetDrawColor(ctx.Renderer, MustHexColor("#356DAD"))
ctx.Renderer.FillRect(d.Bounds.SDLPtr())
func (d *LargeDialog) Render(ctx ui.Context) {
ctx.Renderer().Clear(zntg.MustHexColor("#356DAD"))
d.StackPanel.Render(ctx)
d.DialogBase.Render(ctx)
}
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)
}
func (d *LargeDialog) SetCaption(s string) { d.title.Text = s }

31
map.go
View File

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

View File

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

42
point.go Normal file
View File

@ -0,0 +1,42 @@
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
}

125
projection.go Normal file
View File

@ -0,0 +1,125 @@
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 RectSize(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 RectSize(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 = Rect(buttonBarWidth, 64, windowW-buttonBarWidth, windowH)
p.windowVisibleRect = Rect(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) 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)
}
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)
}

48
proxy.go Normal file
View File

@ -0,0 +1,48 @@
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 Normal file
View File

@ -0,0 +1,32 @@
package tins2020
import "github.com/veandco/go-sdl2/sdl"
type Rectangle struct {
sdl.Rect
}
func Rect(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 RectSize(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,76 +2,197 @@ package tins2020
import (
"fmt"
"math"
"math/rand"
"strconv"
"strings"
"time"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
"opslag.de/schobers/zntg/ui"
"github.com/veandco/go-sdl2/sdl"
)
type Research struct {
ui.StackPanel
Container
game *Game
botanist Specialist
farmer Specialist
description ui.Paragraph
specialists ui.Paragraph
dial *Dial
input ui.Label
typing string
digitCount int
animate zntg.Animation
closeRequested ui.Events
close func()
description Paragraph
specialists Paragraph
input Label
digits []Digit
animate Animation
}
type Dialer interface {
CanUserType(int) bool
UserGaveWrongInput()
UserTyped(ui.Context, int)
func NewResearch(game *Game) Control {
research := &Research{
game: game,
animate: NewAnimation(20 * time.Millisecond),
}
func NewResearch(game *Game) *LargeDialog {
research := &Research{game: game}
research.animate.Interval = 20 * time.Millisecond
research.animate.Start()
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) })
dialog := &LargeDialog{}
dialog.SetCaption("Research")
dialog.SetContent(research)
dialog.OnShow().RegisterItf(func(state interface{}) {
research.onShow(state.(*Context))
})
research.close = func() { dialog.CloseDialog() }
return dialog
}
type Digit struct {
ControlBase
Value string
highlight int
}
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) Blink() {
d.highlight = 4
}
func (d *Digit) Tick() {
if d.highlight > 0 {
d.highlight--
}
}
type Specialist struct {
Cost int
Number string
}
func (r *Research) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) {
r.input.TextAlignment = ui.AlignCenter
r.StackPanel.Arrange(ctx, bounds, offset, parent)
func (r *Research) Init(ctx *Context) error {
r.AddChild(&r.description)
r.AddChild(&r.specialists)
r.AddChild(&r.input)
r.description.Text = "Call a specialist to conduct research with."
r.digits = make([]Digit, 10)
for i := range r.digits {
r.digits[i].Value = strconv.Itoa(i)
r.AddChild(&r.digits[i])
}
func (r *Research) CanUserType(digit int) bool {
typing := strconv.Itoa(digit)
return strings.HasPrefix(r.botanist.Number, r.input.Text+typing)
return nil
}
func (r *Research) Hidden() {}
func (r *Research) Arrange(ctx *Context, bounds Rectangle) {
r.Container.Arrange(ctx, bounds)
r.specialists.Arrange(ctx, RectSize(r.Bounds.X, r.Bounds.Y+40, r.Bounds.W, r.Bounds.H-40))
r.input.Arrange(ctx, RectSize(r.Bounds.X, r.Bounds.X+r.Bounds.H-48, r.Bounds.W, 24))
r.input.Alignment = TextAlignmentCenter
func (r *Research) Render(ctx ui.Context) {
r.animate.AnimateFn(r.dial.Tick)
r.StackPanel.Render(ctx)
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, RectSize(digitCenter.X-24, digitCenter.Y-24, 48, 48))
}
}
func (r *Research) Shown() {
func (r *Research) userTyped(i int) {
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) {
r.input.Text = ""
r.typing = ""
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 {
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 {
var number string
for i := 0; i < 3; i++ {
@ -79,9 +200,8 @@ func (r *Research) Shown() {
}
return number
}
r.digitCount = 0
r.input.Text = ""
r.dial.Reset()
var specialists string
defer func() {
@ -105,16 +225,3 @@ func (r *Research) Shown() {
specialists += fmt.Sprintf("Botanist: no. %s (unlocks next flower; $ %d)\n", r.botanist.Number, r.botanist.Cost)
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,8 +4,6 @@ import (
"bufio"
"fmt"
"strings"
"opslag.de/schobers/zntg/ui"
)
type ResourceLoader struct {
@ -16,8 +14,8 @@ func NewResourceLoader() *ResourceLoader {
return &ResourceLoader{}
}
func (l *ResourceLoader) parseResourcesFile(res ui.Resources, name string) error {
f, err := res.OpenResource(name)
func (l *ResourceLoader) parseResourcesFile(res *Resources, name string) error {
f, err := res.Fs().Open(name)
if err != nil {
return err
}
@ -38,7 +36,7 @@ func (l *ResourceLoader) parseResourcesFile(res ui.Resources, name string) error
return nil
}
func (l *ResourceLoader) LoadFromFile(res ui.Resources, name string, action func(string, string) error) error {
func (l *ResourceLoader) LoadFromFile(res *Resources, name string, action func(string, string) error) error {
err := l.parseResourcesFile(res, name)
if err != nil {
return err

39
resources.go Normal file
View File

@ -0,0 +1,39 @@
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) 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

@ -1,60 +0,0 @@
#!/bin/bash
if [ -z "$1" ]
then
version="0.0.0"
else
version="$1"
fi
version_safe=${version//\./_}
echo "Creating ${version} release"
rm -rf build/linux*
rm -rf build/macosx*
rm -rf build/windows*
mkdir -p build/release
go generate ../cmd/tins2020
mkdir -p build/linux
go build -tags static -ldflags "-s -w" -o build/linux/botanim ../cmd/tins2020
cp ../README.md build/linux
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 ../..
mkdir -p build/linux-allegro
go build -tags static,allegro -ldflags "-s -w" -o build/linux-allegro/botanim ../cmd/tins2020
cp ../README.md build/linux-allegro
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,11 +1,6 @@
package tins2020
import (
"os"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg"
)
import "os"
type Settings struct {
Window WindowSettings
@ -23,7 +18,7 @@ func (s *Settings) Init() error {
if _, err := os.Stat(path); os.IsNotExist(err) {
return nil
}
return zntg.DecodeJSON(path, s)
return DecodeJSON(path, s)
}
func (s *Settings) Store() error {
@ -31,11 +26,11 @@ func (s *Settings) Store() error {
if err != nil {
return err
}
return zntg.EncodeJSON(path, s)
return EncodeJSON(path, s)
}
type WindowSettings struct {
Location *geom.Point
Size *geom.Point
Location *Point
Size *Point
VSync *bool
}

View File

@ -3,165 +3,121 @@ package tins2020
import (
"fmt"
"opslag.de/schobers/zntg/play"
"opslag.de/schobers/geom"
"opslag.de/schobers/zntg/ui"
"github.com/veandco/go-sdl2/sdl"
)
type terrainRenderer struct {
ui.ControlBase
game *Game
hover *geom.Point
viewBounds geom.RectangleF32
interactBounds geom.RectangleF32
isometric *play.IsometricProjection
hover *Point
project projection
drag ui.Dragable
drag Drageable
}
func NewTerrainRenderer(game *Game) ui.Control {
renderer := &terrainRenderer{game: game, isometric: play.NewIsometricProjection(geom.PtF32(128, 64), geom.RectF32(0, 0, 100, 100))}
func NewTerrainRenderer(game *Game) Control {
return &terrainRenderer{game: game, project: newProjection()}
}
renderer.game.CenterChanged().AddHandler(func(ctx ui.Context, state interface{}) {
center := state.(geom.Point)
renderer.isometric.MoveCenterTo(center.ToF32())
func (r *terrainRenderer) Arrange(ctx *Context, _ Rectangle) {
r.project.update(ctx.Renderer)
}
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)
})
return renderer
r.project.update(ctx.Renderer)
return nil
}
func (r *terrainRenderer) Arrange(ctx ui.Context, bounds geom.RectangleF32, _ geom.PointF32, _ ui.Control) {
r.viewBounds = geom.RectF32(buttonBarWidth, 0, bounds.Dx()-buttonBarWidth, bounds.Dy())
r.isometric.SetViewBounds(r.viewBounds)
r.interactBounds = r.viewBounds
r.interactBounds.Min.Y += 64
func isControlKeyDown() bool {
state := sdl.GetKeyboardState()
return state[sdl.SCANCODE_LCTRL] == 1 || state[sdl.SCANCODE_RCTRL] == 1 || state[sdl.SCANCODE_LGUI] == 1 || state[sdl.SCANCODE_RGUI] == 1
}
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 {
func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) bool {
switch e := event.(type) {
case *ui.MouseButtonDownEvent:
pos := e.Pos()
if pos.In(r.interactBounds) {
controlKeyDown := isControlKeyDown(ctx)
if e.Button == ui.MouseButtonMiddle || (e.Button == ui.MouseButtonLeft && controlKeyDown) {
if _, ok := r.drag.IsDragging(); !ok {
r.drag.Start(pos)
case *sdl.MouseButtonEvent:
if r.project.windowInteractRect.IsPointInside(e.X, e.Y) {
if e.Type == sdl.MOUSEBUTTONDOWN {
controlKeyDown := isControlKeyDown()
if e.Button == sdl.BUTTON_MIDDLE || (e.Button == sdl.BUTTON_LEFT && controlKeyDown) {
if !r.drag.IsDragging() {
r.drag.Start(Pt(e.X, e.Y))
}
}
if e.Button == ui.MouseButtonLeft && !controlKeyDown {
pos := r.isometric.ViewToTileInt(pos)
if e.Button == sdl.BUTTON_LEFT && !controlKeyDown {
pos := r.project.screenToMapInt(e.X, e.Y)
r.game.UserClickedTile(pos)
}
if e.Button == ui.MouseButtonRight {
r.game.CancelTool(ctx)
if e.Button == sdl.BUTTON_RIGHT {
if e.Type == sdl.MOUSEBUTTONDOWN {
r.game.CancelTool()
}
}
case *ui.MouseButtonUpEvent:
if _, ok := r.drag.IsDragging(); ok {
r.game.Terrain.Center = r.isometric.TileInt(r.isometric.Center())
}
if e.Type == sdl.MOUSEBUTTONUP {
if r.drag.IsDragging() {
r.game.Terrain.Center = mapToTile(r.project.center)
r.drag.Cancel()
}
case *ui.MouseMoveEvent:
pos := e.Pos()
if pos.In(r.interactBounds) {
hover := r.isometric.ViewToTileInt(pos)
}
}
case *sdl.MouseMotionEvent:
if r.project.windowInteractRect.IsPointInside(e.X, e.Y) {
hover := r.project.screenToMapInt(e.X, e.Y)
r.hover = &hover
} else {
r.hover = nil
}
if _, ok := r.drag.IsDragging(); ok {
delta, _ := r.drag.Move(pos)
r.isometric.Pan(delta.Invert())
if r.drag.IsDragging() {
delta := r.drag.Move(Pt(e.X, e.Y))
r.project.center = r.project.center.Sub(r.project.screenToMapRel(delta.X, delta.Y))
r.project.update(ctx.Renderer)
}
case *sdl.MouseWheelEvent:
if r.hover != nil {
if e.MouseWheel < 0 {
r.isometric.ZoomOut(r.hover.ToF32())
} else if e.MouseWheel > 0 {
r.isometric.ZoomIn(r.hover.ToF32())
if e.Y < 0 {
r.project.ZoomOut(ctx, r.hover.ToPtF())
} else {
r.project.ZoomIn(ctx, r.hover.ToPtF())
}
}
case *ui.MouseLeaveEvent:
case *sdl.WindowEvent:
if e.Event == sdl.WINDOWEVENT_LEAVE {
r.hover = nil
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))
r.project.update(ctx.Renderer)
}
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)
}
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
}
func (r *terrainRenderer) Render(ctx ui.Context) {
zoom := r.isometric.Zoom()
func (r *terrainRenderer) Render(ctx *Context) {
terrain := r.game.Terrain
toTileTexture := func(tile geom.Point) ui.Texture {
temp := terrain.Temp.Value(tile.X, tile.Y)
toTileTexture := func(x, y int32) *Texture {
temp := terrain.Temp.Value(x, y)
if temp < .35 {
return ctx.Textures().ScaledByName("tile-snow", zoom)
return ctx.Textures.Texture("tile-snow")
}
if temp > .65 {
return ctx.Textures().ScaledByName("tile-dirt", zoom)
return ctx.Textures.Texture("tile-dirt")
}
return ctx.Textures().ScaledByName("tile-grass", zoom)
return ctx.Textures.Texture("tile-grass")
}
variantToInt := func(variant float64) int {
@ -180,13 +136,14 @@ func (r *terrainRenderer) Render(ctx ui.Context) {
return -1
}
variantToTexture := func(format string, variant float64) ui.Texture {
variantToTexture := func(format string, variant float64) *Texture {
textName := fmt.Sprintf(format, variantToInt(variant))
return ctx.Textures().ScaledByName(textName, zoom)
return ctx.Textures.Texture(textName)
}
stretch := func(x, from, to float64) float64 { return (x - from) * 1 / (to - from) }
toPropTexture := func(temp, humid, variant float64) ui.Texture {
toPropTexture := func(temp, humid, variant float64) *Texture {
if temp < .35 {
if humid < .2 {
return nil
@ -216,12 +173,12 @@ func (r *terrainRenderer) Render(ctx ui.Context) {
return variantToTexture("bush-large-%d", stretch(variant, .8, 1)*multiplier)
}
toItemTexture := func(x, y int) ui.Texture {
toItemTexture := func(x, y int32) *Texture {
variant := terrain.Variant.Value(x, y)
flower, ok := terrain.Flowers[geom.Pt(x, y)]
flower, ok := terrain.Flowers[Pt(x, y)]
if ok {
desc, _ := r.game.Herbarium.Find(flower.ID)
return ctx.Textures().ScaledByName(desc.IconTemplate.Variant(variantToInt(variant)), zoom)
return ctx.Textures.Texture(desc.IconTemplate.Variant(variantToInt(variant)))
}
temp := terrain.Temp.Value(x, y)
humid := terrain.Humid.Value(x, y)
@ -232,35 +189,24 @@ func (r *terrainRenderer) Render(ctx ui.Context) {
// vertical (tile): [96,160) = 64
// vertical (total): [0,160) = 160
topLeft := geom.PtF32(-64*zoom, -112*zoom)
bottomRight := geom.PtF32(64*zoom, 48*zoom)
textureRect := func(center geom.PointF32) geom.RectangleF32 {
return geom.RectangleF32{Min: center.Add(topLeft), Max: center.Add(bottomRight)}
}
hoverTexture := ctx.Textures().ScaledByName("tile-hover", zoom)
r.project.visibleTiles(func(x, y int32, pos Point) {
text := toTileTexture(x, y)
rect := r.project.screenToTileRect(pos)
text.CopyResize(ctx.Renderer, rect)
r.isometric.EnumerateInt(func(tile geom.Point, view geom.PointF32) {
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)
if r.hover != nil && x == r.hover.X && y == r.hover.Y {
ctx.Textures.Texture("tile-hover").CopyResize(ctx.Renderer, rect)
}
})
r.isometric.EnumerateInt(func(tile geom.Point, view geom.PointF32) {
text := toItemTexture(tile.X, tile.Y)
r.project.visibleTiles(func(x, y int32, pos Point) {
text := toItemTexture(x, y)
if text == nil {
return
}
placeX, placeY := terrain.PlaceX.Value(tile.X, tile.Y), terrain.PlaceY.Value(tile.X, tile.Y)
displaced := r.isometric.TileToView(tile.ToF32().Add2D(-.2+.9*float32(placeX)-.45, -.2+.9*float32(placeY)-.45))
rect := textureRect(displaced)
ctx.Renderer().DrawTexture(text, rect)
placeX, placeY := terrain.PlaceX.Value(x, y), terrain.PlaceY.Value(x, y)
pos = r.project.mapToScreenF(float32(x)-.2+float32(.9*placeX-.45), float32(y)-.2+float32(.9*placeY-.45))
text.CopyResize(ctx.Renderer, r.project.screenToTileRect(pos))
})
}

116
textures.go Normal file
View File

@ -0,0 +1,116 @@
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, RectSize(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, RectSize(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, Rect(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,10 +1,8 @@
package tins2020
import "opslag.de/schobers/geom"
type Tool interface {
Type() string
ClickedTile(*Game, geom.Point)
ClickedTile(*Game, Point)
}
type PlantFlowerTool struct {
@ -13,20 +11,14 @@ type PlantFlowerTool struct {
func (t *PlantFlowerTool) Type() string { return "plant-flower" }
func (t *PlantFlowerTool) ClickedTile(game *Game, tile geom.Point) {
func (t *PlantFlowerTool) ClickedTile(game *Game, tile Point) {
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{}
func (t *ShovelTool) Type() string { return "shovel" }
func (t *ShovelTool) ClickedTile(game *Game, tile geom.Point) {
func (t *ShovelTool) ClickedTile(game *Game, tile Point) {
game.Dig(tile)
}