Compare commits
No commits in common. "master" and "botanium_1_0_0" have entirely different histories.
master
...
botanium_1
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
.vscode/launch.json
|
||||
|
||||
cmd/tins2020/*rice-box.*
|
||||
scripts/build
|
||||
|
80
README.md
80
README.md
@ -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
57
animation.go
Normal 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
59
buttonbar.go
Normal 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
|
||||
)
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
}
|
@ -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
30
color.go
Normal 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
33
color_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -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
58
container.go
Normal 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)
|
||||
}
|
||||
}
|
26
content.go
26
content.go
@ -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
45
context.go
Normal 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
59
control.go
Normal 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
137
dial.go
@ -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()
|
||||
}
|
||||
}
|
34
dialdigit.go
34
dialdigit.go
@ -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--
|
||||
}
|
||||
}
|
110
dialogs.go
110
dialogs.go
@ -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
24
drageable.go
Normal 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
33
eventhandler.go
Normal 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) }
|
@ -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
109
fonts.go
Normal 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
46
fpsrenderer.go
Normal 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})
|
||||
}
|
131
game.go
131
game.go
@ -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(ctx ui.Context) {
|
||||
g.Pause(ctx)
|
||||
g.Reset(ctx)
|
||||
func (g *Game) New() {
|
||||
g.Pause()
|
||||
g.Reset()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
288
gamecontrols.go
288
gamecontrols.go
@ -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)
|
||||
}
|
||||
|
13
gamestate.go
13
gamestate.go
@ -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" }
|
||||
|
@ -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,
|
||||
|
100
iconbutton.go
100
iconbutton.go
@ -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) RenderActive(ctx ui.Context) {
|
||||
if b.Active || (!b.Disabled && b.IsOver()) {
|
||||
ctx.Renderer().FillRectangle(b.Bounds(), hoverTransparentColor)
|
||||
func (b *IconButton) activeTexture(ctx *Context) *Texture {
|
||||
if b.IsDisabled {
|
||||
texture := ctx.Textures.Texture(b.IconDisabled)
|
||||
if texture != nil {
|
||||
return texture
|
||||
}
|
||||
|
||||
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
62
img/color.go
Normal 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
29
img/io.go
Normal 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
|
||||
}
|
26
intro.go
26
intro.go
@ -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
47
io.go
@ -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
104
label.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
159
largedialog.go
159
largedialog.go
@ -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
31
map.go
@ -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,
|
||||
|
@ -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
42
point.go
Normal 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
125
projection.go
Normal 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
48
proxy.go
Normal 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
32
rect.go
Normal 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 }
|
215
research.go
215
research.go
@ -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) *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) })
|
||||
func NewResearch(game *Game) Control {
|
||||
research := &Research{
|
||||
game: game,
|
||||
animate: NewAnimation(20 * time.Millisecond),
|
||||
}
|
||||
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])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *Research) CanUserType(digit int) bool {
|
||||
typing := strconv.Itoa(digit)
|
||||
return strings.HasPrefix(r.botanist.Number, r.input.Text+typing)
|
||||
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
|
||||
|
||||
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) Hidden() {}
|
||||
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++
|
||||
}
|
||||
|
||||
func (r *Research) Render(ctx ui.Context) {
|
||||
r.animate.AnimateFn(r.dial.Tick)
|
||||
r.StackPanel.Render(ctx)
|
||||
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) Shown() {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -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
39
resources.go
Normal 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)
|
||||
}
|
@ -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 ../..
|
15
settings.go
15
settings.go
@ -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
|
||||
}
|
||||
|
@ -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
116
textures.go
Normal 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()
|
||||
}
|
||||
}
|
14
tools.go
14
tools.go
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user