Lots of UI work.

Added more icons and placed buttons on the bars.
Implemented pause/run/fast.
This commit is contained in:
Sander Schobers 2020-05-10 17:16:18 +02:00
parent 44aef25d34
commit b14f79a61a
28 changed files with 522 additions and 167 deletions

56
animation.go Normal file
View File

@ -0,0 +1,56 @@
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()
}
func (a *Animation) SetInterval(interval time.Duration) {
a.interval = interval
}

57
buttonbar.go Normal file
View File

@ -0,0 +1,57 @@
package tins2020
import "github.com/veandco/go-sdl2/sdl"
type ButtonBar struct {
Container
Background sdl.Color
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)
switch b.Orientation {
case OrientationHorizontal:
length := bounds.H
offset := bounds.X
for i := range b.Buttons {
b.Buttons[i].Arrange(ctx, RectSize(offset, bounds.Y, length, length))
offset += length
}
default:
length := bounds.W
offset := bounds.Y
for i := range b.Buttons {
b.Buttons[i].Arrange(ctx, RectSize(bounds.X, offset, length, length))
offset += length
}
}
}
func (b *ButtonBar) Handle(ctx *Context, event sdl.Event) {
b.Container.Handle(ctx, event)
}
func (b *ButtonBar) Render(ctx *Context) {
ctx.Renderer.SetDrawColor(b.Background.R, b.Background.G, b.Background.B, b.Background.A)
ctx.Renderer.FillRect(b.Bounds.SDLPtr())
b.Container.Render(ctx)
}
type Orientation int
const (
OrientationVertical Orientation = iota
OrientationHorizontal
)

95
buyflowerbutton.go Normal file
View File

@ -0,0 +1,95 @@
package tins2020
import (
"fmt"
"time"
"github.com/veandco/go-sdl2/sdl"
)
type BuyFlowerButton struct {
IconButton
Name string
Price int
Description string
hoverAnimation *Animation
hoverOffset int32
hoverTexture *Texture
priceTexture *Texture
}
func NewBuyFlowerButton(icon, iconDisabled, name string, price int, description string, isDisabled bool, onClick EventFn) *BuyFlowerButton {
return &BuyFlowerButton{
IconButton: *NewIconButtonConfig(icon, onClick, func(b *IconButton) {
b.IconDisabled = iconDisabled
b.IsDisabled = isDisabled
}),
Name: name,
Price: price,
Description: description,
}
}
func (b *BuyFlowerButton) animate() {
b.hoverOffset++
if b.hoverOffset > b.hoverTexture.Size().X+b.Bounds.W {
b.hoverOffset = b.priceTexture.Size().X
}
}
func (b *BuyFlowerButton) Init(ctx *Context) error {
text := fmt.Sprintf("%s - %s - %s", FmtMoney(b.Price), b.Name, b.Description)
font := ctx.Fonts.Font("small")
color := MustHexColor("#ffffff")
texture, err := font.Render(ctx.Renderer, text, color)
if err != nil {
return err
}
b.hoverTexture = texture
texture, err = font.Render(ctx.Renderer, FmtMoney(b.Price), color)
if err != nil {
return err
}
b.priceTexture = texture
return nil
}
func (b *BuyFlowerButton) Handle(ctx *Context, event sdl.Event) {
b.IconButton.Handle(ctx, event)
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
}
}
func (b *BuyFlowerButton) Render(ctx *Context) {
iconTexture := b.activeTexture(ctx)
mouseOverTexture := ctx.Textures.Texture("control-hover")
pos := Pt(b.Bounds.X, b.Bounds.Y)
iconTexture.CopyResize(ctx.Renderer, RectSize(pos.X, pos.Y-40, buttonBarWidth, 120))
if b.IsMouseOver && !b.IsDisabled {
mouseOverTexture.Copy(ctx.Renderer, pos)
}
if b.hoverAnimation != nil {
b.hoverAnimation.AnimateFn(b.animate)
}
if b.IsMouseOver {
left := buttonBarWidth - 8 - b.hoverOffset
top := pos.Y + buttonBarWidth - 20
if left < 0 {
part := Rect(-left, 0, b.hoverTexture.Size().X, b.hoverTexture.Size().Y)
b.hoverTexture.CopyPart(ctx.Renderer, part, Pt(pos.X, top))
} else {
b.hoverTexture.Copy(ctx.Renderer, Pt(pos.X+left, top))
}
} else {
b.priceTexture.Copy(ctx.Renderer, Pt(pos.X+buttonBarWidth-8-b.priceTexture.Size().X, pos.Y+buttonBarWidth-20))
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -1,4 +1,15 @@
game-control-hover: images/game_control_hover.png control-hover: images/game_control_hover.png
control-shovel: images/genericItem_color_022.png
control-research: images/genericItem_color_111.png
control-settings: images/gear.png
control-save: images/save.png
control-load: images/basket.png
control-quit: images/power.png
control-pause: images/pause.png
control-run: images/forward.png
control-run-fast: images/fastForward.png
tile-dirt: images/tile_dirt.png tile-dirt: images/tile_dirt.png
tile-grass: images/tile_grass.png tile-grass: images/tile_grass.png

View File

@ -67,9 +67,10 @@ func run() error {
ctx.Init(renderer) ctx.Init(renderer)
err = ctx.Fonts.LoadDesc( err = ctx.Fonts.LoadDesc(
tins2020.FontDescriptor{Name: "debug", Path: "fonts/OpenSans-Regular.ttf", Size: 12}, tins2020.FontDescriptor{Name: "debug", Path: "fonts/FiraMono-Regular.ttf", Size: 12},
tins2020.FontDescriptor{Name: "default", Path: "fonts/FiraMono-Regular.ttf", Size: 16}, tins2020.FontDescriptor{Name: "default", Path: "fonts/OpenSans-Regular.ttf", Size: 16},
tins2020.FontDescriptor{Name: "small", Path: "fonts/FiraMono-Regular.ttf", Size: 12}, tins2020.FontDescriptor{Name: "small", Path: "fonts/OpenSans-Regular.ttf", Size: 12},
tins2020.FontDescriptor{Name: "balance", Path: "fonts/OpenSans-Bold.ttf", Size: 40},
) )
if err != nil { if err != nil {
return err return err
@ -86,7 +87,7 @@ func run() error {
app := tins2020.NewContainer() app := tins2020.NewContainer()
overlays := tins2020.NewContainer() overlays := tins2020.NewContainer()
gameControls := tins2020.NewGameControls() gameControls := tins2020.NewGameControls(game)
overlays.AddChild(gameControls) overlays.AddChild(gameControls)
overlays.AddChild(&tins2020.FPS{}) overlays.AddChild(&tins2020.FPS{})
content := tins2020.NewContainer() content := tins2020.NewContainer()
@ -98,6 +99,9 @@ func run() error {
return err return err
} }
w, h := window.GetSize()
app.Arrange(ctx, tins2020.Rect(0, 0, w, h))
for { for {
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
switch e := event.(type) { switch e := event.(type) {
@ -111,6 +115,7 @@ func run() error {
ctx.Settings.Window.Location = tins2020.PtPtr(x, y) ctx.Settings.Window.Location = tins2020.PtPtr(x, y)
case sdl.WINDOWEVENT_SIZE_CHANGED: case sdl.WINDOWEVENT_SIZE_CHANGED:
w, h := window.GetSize() w, h := window.GetSize()
app.Arrange(ctx, tins2020.Rect(0, 0, w, h))
ctx.Settings.Window.Size = tins2020.PtPtr(w, h) ctx.Settings.Window.Size = tins2020.PtPtr(w, h)
} }
case *sdl.KeyboardEvent: case *sdl.KeyboardEvent:

View File

@ -25,6 +25,14 @@ func HexColor(s string) (sdl.Color, error) {
return sdl.Color{R: uint8(values[0]), G: uint8(values[1]), B: uint8(values[2]), A: uint8(a)}, nil return sdl.Color{R: uint8(values[0]), G: uint8(values[1]), B: uint8(values[2]), A: uint8(a)}, nil
} }
func MustHexColor(s string) sdl.Color {
color, err := HexColor(s)
if err != nil {
panic(err)
}
return color
}
func HexToInt(s string) (int, error) { func HexToInt(s string) (int, error) {
var i int var i int
for _, c := range s { for _, c := range s {

View File

@ -5,6 +5,8 @@ import (
) )
type Container struct { type Container struct {
ControlBase
Children []Control Children []Control
} }
@ -16,19 +18,23 @@ func (c *Container) AddChild(child Control) {
c.Children = append(c.Children, child) 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) { func (c *Container) Handle(ctx *Context, event sdl.Event) {
c.ControlBase.Handle(ctx, event)
for _, child := range c.Children { for _, child := range c.Children {
child.Handle(ctx, event) child.Handle(ctx, event)
} }
} }
func (c *Container) Render(ctx *Context) {
for _, child := range c.Children {
child.Render(ctx)
}
}
func (c *Container) Init(ctx *Context) error { func (c *Container) Init(ctx *Context) error {
c.ControlBase.Init(ctx)
for _, child := range c.Children { for _, child := range c.Children {
err := child.Init(ctx) err := child.Init(ctx)
if err != nil { if err != nil {
@ -37,3 +43,11 @@ func (c *Container) Init(ctx *Context) error {
} }
return nil return nil
} }
func (c *Container) Render(ctx *Context) {
c.ControlBase.Render(ctx)
for _, child := range c.Children {
child.Render(ctx)
}
}

View File

@ -4,23 +4,47 @@ import "github.com/veandco/go-sdl2/sdl"
type Control interface { type Control interface {
Init(*Context) error Init(*Context) error
Arrange(*Context, Rectangle)
Handle(*Context, sdl.Event) Handle(*Context, sdl.Event)
Render(*Context) Render(*Context)
} }
type EventFn func(*Context)
type EmptyEventFn func()
func EmptyEvent(fn EmptyEventFn) EventFn {
return func(*Context) { fn() }
}
type ControlBase struct { type ControlBase struct {
Bounds Rectangle Bounds Rectangle
IsMouseOver bool IsMouseOver bool
OnLeftMouseButtonClick EventFn
} }
func (b *ControlBase) Arrange(ctx *Context, bounds Rectangle) { b.Bounds = bounds }
func (b *ControlBase) Init(*Context) error { return nil } func (b *ControlBase) Init(*Context) error { return nil }
func (b *ControlBase) Handle(ctx *Context, event sdl.Event) { func (b *ControlBase) Handle(ctx *Context, event sdl.Event) {
switch e := event.(type) { switch e := event.(type) {
case *sdl.MouseMotionEvent: case *sdl.MouseMotionEvent:
b.IsMouseOver = b.Bounds.IsPointInside(e.X, e.Y) b.IsMouseOver = b.Bounds.IsPointInside(e.X, e.Y)
case *sdl.MouseButtonEvent:
if b.IsMouseOver && e.Button == sdl.BUTTON_LEFT && e.Type == sdl.MOUSEBUTTONDOWN {
b.Invoke(ctx, b.OnLeftMouseButtonClick)
}
} }
} }
func (b *ControlBase) Invoke(ctx *Context, fn EventFn) {
if fn == nil {
return
}
fn(ctx)
}
func (b *ControlBase) Render(*Context) {} func (b *ControlBase) Render(*Context) {}

View File

@ -20,7 +20,7 @@ type Font struct {
*ttf.Font *ttf.Font
} }
func (f *Font) Render(renderer *sdl.Renderer, text string, pos Point, color sdl.Color) (*Texture, error) { func (f *Font) Render(renderer *sdl.Renderer, text string, color sdl.Color) (*Texture, error) {
surface, err := f.RenderUTF8Blended(text, color) surface, err := f.RenderUTF8Blended(text, color)
if err != nil { if err != nil {
return nil, err return nil, err
@ -34,31 +34,25 @@ func (f *Font) Render(renderer *sdl.Renderer, text string, pos Point, color sdl.
} }
func (f *Font) RenderCopyAlign(renderer *sdl.Renderer, text string, pos Point, color sdl.Color, align TextAlignment) error { func (f *Font) RenderCopyAlign(renderer *sdl.Renderer, text string, pos Point, color sdl.Color, align TextAlignment) error {
texture, err := f.Render(renderer, text, pos, color) texture, err := f.Render(renderer, text, color)
if err != nil { if err != nil {
return err return err
} }
defer texture.Destroy() defer texture.Destroy()
rect := texture.Rect() size := texture.Size()
switch align { switch align {
case TextAlignmentLeft: case TextAlignmentLeft:
texture.Copy(renderer, RectSize(pos.X, pos.Y, rect.W, rect.H).SDLPtr()) texture.Copy(renderer, Pt(pos.X, pos.Y-size.Y))
case TextAlignmentCenter: case TextAlignmentCenter:
texture.Copy(renderer, RectSize(pos.X-(rect.W/2), pos.Y, rect.W, rect.H).SDLPtr()) texture.Copy(renderer, Pt(pos.X-(size.X/2), pos.Y-size.Y))
case TextAlignmentRight: case TextAlignmentRight:
texture.Copy(renderer, RectSize(pos.X-rect.W, pos.Y, rect.W, rect.H).SDLPtr()) texture.Copy(renderer, Pt(pos.X-size.X, pos.Y-size.Y))
} }
return nil return nil
} }
func (f *Font) RenderCopy(renderer *sdl.Renderer, text string, pos Point, color sdl.Color) error { func (f *Font) RenderCopy(renderer *sdl.Renderer, text string, pos Point, color sdl.Color) error {
texture, err := f.Render(renderer, text, pos, color) return f.RenderCopyAlign(renderer, text, pos, color, TextAlignmentLeft)
if err != nil {
return err
}
defer texture.Destroy()
texture.Copy(renderer, texture.RectOffset(pos))
return nil
} }
type Fonts struct { type Fonts struct {

View File

@ -37,5 +37,5 @@ func (f *FPS) Render(ctx *Context) {
f.ticks[f.slot]++ f.ticks[f.slot]++
font := ctx.Fonts.Font("debug") font := ctx.Fonts.Font("debug")
font.RenderCopy(ctx.Renderer, fmt.Sprintf("FPS: %d", f.total), Pt(5, 5), sdl.Color{R: 255, G: 255, B: 255, A: 255}) font.RenderCopy(ctx.Renderer, fmt.Sprintf("FPS: %d", f.total), Pt(5, 17), sdl.Color{R: 255, G: 255, B: 255, A: 255})
} }

52
game.go
View File

@ -6,13 +6,21 @@ import (
) )
type Game struct { type Game struct {
Money int Balance int
Speed GameSpeed
Terrain *Map Terrain *Map
start time.Time simulation Animation
lastUpdate time.Duration
} }
type GameSpeed string
const (
GameSpeedNormal GameSpeed = "normal"
GameSpeedFast = "fast"
GameSpeedPaused = "paused"
)
type Map struct { type Map struct {
Temp NoiseMap Temp NoiseMap
Humid NoiseMap Humid NoiseMap
@ -36,6 +44,9 @@ func (m *Map) NewFlower(pos Point, traits FlowerTraits) Flower {
return flower return flower
} }
const simulationInterval = 120 * time.Millisecond
const fastSimulationInterval = 40 * time.Millisecond
func NewGame() *Game { func NewGame() *Game {
terrain := &Map{ terrain := &Map{
Temp: NewNoiseMap(rand.Int63()), Temp: NewNoiseMap(rand.Int63()),
@ -47,18 +58,10 @@ func NewGame() *Game {
} }
terrain.AddFlower(Pt(0, 0), NewPoppyTraits()) terrain.AddFlower(Pt(0, 0), NewPoppyTraits())
return &Game{ return &Game{
Money: 100, Balance: 100,
Terrain: terrain, Terrain: terrain,
start: time.Now(), simulation: NewAnimation(time.Millisecond * 10),
}
}
func (g *Game) Update() {
update := time.Since(g.start)
for g.lastUpdate < update {
g.tick()
g.lastUpdate += time.Millisecond * 10
} }
} }
@ -92,3 +95,26 @@ func (g *Game) tick() {
} }
g.Terrain.Flowers = flowers g.Terrain.Flowers = flowers
} }
func (g *Game) Pause() {
g.Speed = GameSpeedPaused
g.simulation.Pause()
}
func (g *Game) Run() {
g.Speed = GameSpeedNormal
g.simulation.SetInterval(simulationInterval)
g.simulation.Run()
}
func (g *Game) RunFast() {
g.Speed = GameSpeedFast
g.simulation.SetInterval(fastSimulationInterval)
g.simulation.Run()
}
func (g *Game) Update() {
for g.simulation.Animate() {
g.tick()
}
}

View File

@ -1,114 +1,77 @@
package tins2020 package tins2020
import (
"log"
"github.com/veandco/go-sdl2/sdl"
)
type GameControls struct { type GameControls struct {
ControlBase Container
game *Game
menu ButtonBar menu ButtonBar
top ButtonBar
flowers ButtonBar flowers ButtonBar
pause *IconButton
run *IconButton
runFast *IconButton
} }
type ButtonBar struct { func NewGameControls(game *Game) *GameControls {
Top int32 return &GameControls{game: game}
Left int32
Bottom int32
Hover int
Buttons []Button
} }
type Button struct { func (c *GameControls) updateSpeedControls() {
Icon string
Disabled string
IsDisabled bool
} }
const buttonBarWidth = 96 func (c *GameControls) Arrange(ctx *Context, bounds Rectangle) {
c.Bounds = bounds
func (b *ButtonBar) Handle(ctx *Context, event sdl.Event) { c.menu.Arrange(ctx, RectSize(bounds.X, bounds.Y, buttonBarWidth, bounds.H))
switch e := event.(type) { c.top.Arrange(ctx, Rect(bounds.X+bounds.W/2+8, bounds.Y, bounds.Right(), bounds.Y+64))
case *sdl.MouseMotionEvent: c.flowers.Arrange(ctx, RectSize(bounds.Right()-buttonBarWidth, bounds.Y, buttonBarWidth, bounds.H))
if e.X > b.Left && e.X < b.Left+buttonBarWidth {
button := int(e.Y-b.Top) / buttonBarWidth
if button < 0 || button >= len(b.Buttons) || b.Buttons[button].IsDisabled {
button = -1
}
b.Hover = button
} else {
b.Hover = -1
}
}
} }
func (b *ButtonBar) Render(ctx *Context) { func (c *GameControls) buyPoppy(ctx *Context) {
ctx.Renderer.FillRect(Rect(b.Left, b.Top, b.Left+buttonBarWidth, b.Bottom).SDLPtr()) c.game.Balance -= 10
texture := func(b Button) *Texture {
if b.IsDisabled {
texture := ctx.Textures.Texture(b.Disabled)
if texture != nil {
return texture
}
}
return ctx.Textures.Texture(b.Icon)
}
hoverTexture := ctx.Textures.Texture("game-control-hover")
for i, button := range b.Buttons {
pos := Pt(b.Left, b.Top+int32(i)*buttonBarWidth)
texture := texture(button)
texture.Copy(ctx.Renderer, &sdl.Rect{X: pos.X, Y: pos.Y - 40, W: buttonBarWidth, H: 120})
if b.Hover == i {
hoverTexture.Copy(ctx.Renderer, hoverTexture.RectOffset(pos))
}
}
}
func NewGameControls() *GameControls {
return &GameControls{}
} }
func (c *GameControls) Init(ctx *Context) error { func (c *GameControls) Init(ctx *Context) error {
c.flowers.Hover = -1 c.flowers.Background = MustHexColor("#356dad") // brown alternative? #4ac69a
c.flowers.Buttons = []Button{ c.flowers.Buttons = []Control{
Button{Icon: "flower-poppy-1", Disabled: "flower-poppy-disabled"}, NewBuyFlowerButton("flower-poppy-1", "flower-poppy-disabled", "Poppy", 10, "A very generic flower that thrives in a moderate climate.", false, c.buyPoppy),
Button{Icon: "flower-red-c-1", Disabled: "flower-red-c-disabled", IsDisabled: true}, NewBuyFlowerButton("flower-red-c-1", "flower-poppy-disabled", "Unknown", 100, "Traits are not known yet.", true, nil),
} }
return c.updateBarPositions(ctx)
}
func (c *GameControls) Handle(ctx *Context, event sdl.Event) { c.top.Orientation = OrientationHorizontal
c.menu.Handle(ctx, event) c.pause = NewIconButton("control-pause", EmptyEvent(func() {
c.flowers.Handle(ctx, event) c.game.Pause()
c.updateSpeedControls()
}))
c.run = NewIconButton("control-run", EmptyEvent(func() {
c.game.Run()
c.updateSpeedControls()
}))
c.runFast = NewIconButton("control-run-fast", EmptyEvent(func() {
c.game.RunFast()
c.updateSpeedControls()
}))
c.top.Buttons = []Control{c.pause, c.run, c.runFast}
switch e := event.(type) { c.menu.Background = MustHexColor("#356dad")
case *sdl.WindowEvent: c.menu.Buttons = []Control{
switch e.Event { NewIconButton("control-settings", EmptyEvent(func() {})),
case sdl.WINDOWEVENT_RESIZED: NewIconButton("control-save", EmptyEvent(func() {})),
err := c.updateBarPositions(ctx) NewIconButton("control-load", EmptyEvent(func() {})),
if err != nil {
log.Fatal(err)
}
}
} }
c.Container.AddChild(&c.menu)
c.Container.AddChild(&c.top)
c.Container.AddChild(&c.flowers)
return c.Container.Init(ctx)
} }
func (c *GameControls) Render(ctx *Context) { func (c *GameControls) Render(ctx *Context) {
// ctx.Renderer.SetDrawColor(74, 198, 154, 255) // #4ac69a topBar := MustHexColor("#0000007f")
ctx.Renderer.SetDrawColor(53, 109, 173, 255) ctx.Renderer.SetDrawColor(topBar.R, topBar.G, topBar.B, topBar.A)
c.menu.Render(ctx) ctx.Renderer.FillRect(Rect(c.menu.Bounds.Right(), 0, c.flowers.Bounds.X, 64).SDLPtr())
c.flowers.Render(ctx) ctx.Fonts.Font("balance").RenderCopyAlign(ctx.Renderer, FmtMoney(c.game.Balance), Pt(c.top.Bounds.X-8, 58), MustHexColor("#79A6D9"), TextAlignmentRight)
}
func (c *GameControls) updateBarPositions(ctx *Context) error { c.Container.Render(ctx)
w, h, err := ctx.Renderer.GetOutputSize()
if err != nil {
return err
}
c.menu.Top, c.menu.Left, c.menu.Bottom = 0, 0, h
c.flowers.Top, c.flowers.Left, c.flowers.Bottom = 0, w-buttonBarWidth, h
return nil
} }

62
iconbutton.go Normal file
View File

@ -0,0 +1,62 @@
package tins2020
type IconButton struct {
ControlBase
Icon string
IconDisabled string
IconScale Scale
IconWidth int32
IsDisabled bool
}
type Scale int
const (
ScaleCenter Scale = iota
ScaleStretch
)
func NewIconButton(icon string, onClick EventFn) *IconButton {
return &IconButton{
ControlBase: ControlBase{
OnLeftMouseButtonClick: onClick,
},
Icon: icon,
}
}
func NewIconButtonConfig(icon string, onClick EventFn, configure func(*IconButton)) *IconButton {
button := NewIconButton(icon, onClick)
configure(button)
return button
}
func (b *IconButton) activeTexture(ctx *Context) *Texture {
if b.IsDisabled {
texture := ctx.Textures.Texture(b.IconDisabled)
if texture != nil {
return texture
}
}
return ctx.Textures.Texture(b.Icon)
}
func (b *IconButton) Render(ctx *Context) {
iconTexture := b.activeTexture(ctx)
mouseOverTexture := ctx.Textures.Texture("control-hover")
if b.IconScale == ScaleCenter {
size := iconTexture.Size()
if b.IconWidth != 0 {
size = Pt(b.IconWidth, b.IconWidth*size.Y/size.X)
}
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 b.IsMouseOver && !b.IsDisabled {
mouseOverTexture.CopyResize(ctx.Renderer, b.Bounds)
}
}

14
math.go
View File

@ -20,6 +20,13 @@ func Ceil32(x float32) float32 { return float32(math.Ceil(float64(x))) }
func Floor32(x float32) float32 { return float32(math.Floor(float64(x))) } func Floor32(x float32) float32 { return float32(math.Floor(float64(x))) }
func Max(a, b int32) int32 {
if a > b {
return a
}
return b
}
func Max32(a, b float32) float32 { func Max32(a, b float32) float32 {
if a > b { if a > b {
return a return a
@ -27,6 +34,13 @@ func Max32(a, b float32) float32 {
return b return b
} }
func Min(a, b int32) int32 {
if a < b {
return a
}
return b
}
func Min32(a, b float32) float32 { func Min32(a, b float32) float32 {
if a < b { if a < b {
return a return a

5
money.go Normal file
View File

@ -0,0 +1,5 @@
package tins2020
import "fmt"
func FmtMoney(amount int) string { return fmt.Sprintf("$ %d", amount) }

View File

@ -11,7 +11,8 @@ type projection struct {
zoom float32 zoom float32
zoomInv float32 zoomInv float32
windowRect Rectangle windowInteractRect Rectangle
windowVisibleRect Rectangle
tileScreenDelta PointF tileScreenDelta PointF
tileScreenDeltaInv PointF tileScreenDeltaInv PointF
tileScreenOffset Point tileScreenOffset Point
@ -44,12 +45,12 @@ func (p *projection) screenToMapRel(x, y int32) PointF {
return PtF(.5*(p.tileScreenDeltaInv.X*normX+p.tileScreenDeltaInv.Y*normY), .5*(-p.tileScreenDeltaInv.X*normX+p.tileScreenDeltaInv.Y*normY)) 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) *sdl.Rect { func (p *projection) screenToTileFitRect(pos Point) Rectangle {
return &sdl.Rect{X: pos.X - p.tileFitScreenSize.X, Y: pos.Y - p.tileFitScreenSize.Y, W: 2 * p.tileFitScreenSize.X, H: 2 * p.tileFitScreenSize.Y} 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) *sdl.Rect { func (p *projection) screenToTileRect(pos Point) Rectangle {
return &sdl.Rect{X: pos.X - p.tileScreenOffset.X, Y: pos.Y - p.tileScreenOffset.Y, W: p.tileScreenSize.X, H: p.tileScreenSize.Y} 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) { func (p *projection) update(renderer *sdl.Renderer) {
@ -64,24 +65,26 @@ func (p *projection) update(renderer *sdl.Renderer) {
log.Fatal(err) log.Fatal(err)
} }
p.windowCenter = Pt(windowW/2, windowH/2) p.windowCenter = Pt(windowW/2, windowH/2)
p.windowRect = RectSize(buttonBarWidth, 0, windowW-2*buttonBarWidth, windowH) 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)) { func (p *projection) visibleTiles(action func(int32, int32, Point)) {
topLeft := p.screenToMap(p.windowRect.X, p.windowRect.Y) visible := p.windowVisibleRect
topRight := p.screenToMap(p.windowRect.X+p.windowRect.W, p.windowRect.Y) topLeft := p.screenToMap(visible.X, visible.Y)
bottomLeft := p.screenToMap(p.windowRect.X, p.windowRect.Y+p.windowRect.H) topRight := p.screenToMap(visible.X+visible.W, visible.Y)
bottomRight := p.screenToMap(p.windowRect.X+p.windowRect.W, p.windowRect.Y+p.windowRect.H) 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)) minY, maxY := int32(Floor32(topRight.Y)), int32(Ceil32(bottomLeft.Y))
minX, maxX := int32(Floor32(topLeft.X)), int32(Ceil32(bottomRight.X)) minX, maxX := int32(Floor32(topLeft.X)), int32(Ceil32(bottomRight.X))
for y := minY; y <= maxY; y++ { for y := minY; y <= maxY; y++ {
for x := minX; x <= maxX; x++ { for x := minX; x <= maxX; x++ {
pos := p.mapToScreen(x, y) pos := p.mapToScreen(x, y)
rectFit := p.screenToTileFitRect(pos) rectFit := p.screenToTileFitRect(pos)
if rectFit.X+rectFit.W < p.windowRect.X || rectFit.Y+rectFit.H < p.windowRect.Y { if rectFit.X+rectFit.W < visible.X || rectFit.Y+rectFit.H < visible.Y {
continue continue
} }
if rectFit.X > p.windowRect.X+p.windowRect.W || rectFit.Y > p.windowRect.Y+p.windowRect.H { if rectFit.X > visible.X+visible.W || rectFit.Y > visible.Y+visible.H {
break break
} }
action(x, y, pos) action(x, y, pos)

16
rect.go
View File

@ -6,12 +6,6 @@ type Rectangle struct {
sdl.Rect sdl.Rect
} }
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 Rect(x1, y1, x2, y2 int32) Rectangle { func Rect(x1, y1, x2, y2 int32) Rectangle {
if x1 > x2 { if x1 > x2 {
x1, x2 = x2, x1 x1, x2 = x2, x1
@ -24,5 +18,15 @@ func Rect(x1, y1, x2, y2 int32) Rectangle {
func RectSize(x, y, w, h int32) Rectangle { return Rectangle{sdl.Rect{X: x, Y: y, W: w, H: h}} } 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) SDL() sdl.Rect { return r.Rect }
func (r Rectangle) SDLPtr() *sdl.Rect { return &r.Rect } func (r Rectangle) SDLPtr() *sdl.Rect { return &r.Rect }

View File

@ -24,6 +24,10 @@ func NewTerrainRenderer(terrain *Map) Control {
return &terrainRenderer{terrain: terrain, project: newProjection()} return &terrainRenderer{terrain: terrain, project: newProjection()}
} }
func (r *terrainRenderer) Arrange(ctx *Context, _ Rectangle) {
r.project.update(ctx.Renderer)
}
func (r *terrainRenderer) Init(ctx *Context) error { func (r *terrainRenderer) Init(ctx *Context) error {
r.project.update(ctx.Renderer) r.project.update(ctx.Renderer)
return nil return nil
@ -32,16 +36,18 @@ func (r *terrainRenderer) Init(ctx *Context) error {
func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) { func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) {
switch e := event.(type) { switch e := event.(type) {
case *sdl.MouseButtonEvent: case *sdl.MouseButtonEvent:
if e.Button == sdl.BUTTON_LEFT { if r.project.windowInteractRect.IsPointInside(e.X, e.Y) {
r.interact.mouseLeftDown = e.Type == sdl.MOUSEBUTTONDOWN if e.Button == sdl.BUTTON_LEFT {
if r.interact.mouseLeftDown && r.interact.mouseDrag == nil { r.interact.mouseLeftDown = e.Type == sdl.MOUSEBUTTONDOWN
r.interact.mouseDrag = PtPtr(e.X, e.Y) if r.interact.mouseLeftDown && r.interact.mouseDrag == nil {
} else if !r.interact.mouseLeftDown && r.interact.mouseDrag != nil { r.interact.mouseDrag = PtPtr(e.X, e.Y)
r.interact.mouseDrag = nil } else if !r.interact.mouseLeftDown && r.interact.mouseDrag != nil {
r.interact.mouseDrag = nil
}
} }
} }
case *sdl.MouseMotionEvent: case *sdl.MouseMotionEvent:
if r.project.windowRect.IsPointInside(e.X, e.Y) { if r.project.windowInteractRect.IsPointInside(e.X, e.Y) {
hover := r.project.screenToMap(e.X, e.Y) hover := r.project.screenToMap(e.X, e.Y)
r.hover = PtPtr(int32(Round32(hover.X)), int32(Round32(hover.Y))) r.hover = PtPtr(int32(Round32(hover.X)), int32(Round32(hover.Y)))
} else { } else {
@ -67,10 +73,6 @@ func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) {
r.project.update(ctx.Renderer) r.project.update(ctx.Renderer)
} }
} }
case *sdl.WindowEvent:
if e.Event == sdl.WINDOWEVENT_RESIZED {
r.project.update(ctx.Renderer)
}
} }
} }
@ -157,10 +159,10 @@ func (r *terrainRenderer) Render(ctx *Context) {
r.project.visibleTiles(func(x, y int32, pos Point) { r.project.visibleTiles(func(x, y int32, pos Point) {
text := toTileTexture(x, y) text := toTileTexture(x, y)
rect := r.project.screenToTileRect(pos) rect := r.project.screenToTileRect(pos)
text.Copy(ctx.Renderer, rect) text.CopyResize(ctx.Renderer, rect)
if r.hover != nil && x == r.hover.X && y == r.hover.Y { if r.hover != nil && x == r.hover.X && y == r.hover.Y {
ctx.Textures.Texture("tile-hover").Copy(ctx.Renderer, rect) ctx.Textures.Texture("tile-hover").CopyResize(ctx.Renderer, rect)
} }
}) })
@ -172,7 +174,7 @@ func (r *terrainRenderer) Render(ctx *Context) {
placeX, placeY := r.terrain.PlaceX.Value(x, y), r.terrain.PlaceY.Value(x, y) placeX, placeY := r.terrain.PlaceX.Value(x, y), r.terrain.PlaceY.Value(x, y)
pos = r.project.mapToScreenF(float32(x)-.2+float32(.9*placeX-.45), float32(y)-.2+float32(.9*placeY-.45)) pos = r.project.mapToScreenF(float32(x)-.2+float32(.9*placeX-.45), float32(y)-.2+float32(.9*placeY-.45))
text.Copy(ctx.Renderer, r.project.screenToTileRect(pos)) text.CopyResize(ctx.Renderer, r.project.screenToTileRect(pos))
}) })
// gfx.RectangleColor(ctx.Renderer, r.project.windowRect.X, r.project.windowRect.Y, r.project.windowRect.X+r.project.windowRect.W, r.project.windowRect.Y+r.project.windowRect.H, sdl.Color{R: 255, A: 255}) // gfx.RectangleColor(ctx.Renderer, r.project.windowRect.X, r.project.windowRect.Y, r.project.windowRect.X+r.project.windowRect.W, r.project.windowRect.Y+r.project.windowRect.H, sdl.Color{R: 255, A: 255})

View File

@ -11,7 +11,7 @@ import (
type Texture struct { type Texture struct {
texture *sdl.Texture texture *sdl.Texture
rect *sdl.Rect size Point
} }
func NewTextureFromSurface(renderer *sdl.Renderer, surface *sdl.Surface) (*Texture, error) { func NewTextureFromSurface(renderer *sdl.Renderer, surface *sdl.Surface) (*Texture, error) {
@ -19,21 +19,33 @@ func NewTextureFromSurface(renderer *sdl.Renderer, surface *sdl.Surface) (*Textu
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &Texture{texture: texture, rect: &sdl.Rect{X: 0, Y: 0, W: surface.W, H: surface.H}}, nil return &Texture{texture: texture, size: Pt(surface.W, surface.H)}, nil
} }
func (t *Texture) Rect() *sdl.Rect { return t.rect } func (t *Texture) Size() Point { return t.size }
func (t *Texture) RectOffset(offset Point) *sdl.Rect { // func (t *Texture) Rect() Rectangle { return t.rect }
return &sdl.Rect{X: offset.X, Y: offset.Y, W: t.rect.W, H: t.rect.H}
// 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) Copy(renderer *sdl.Renderer, target *sdl.Rect) { func (t *Texture) CopyPart(renderer *sdl.Renderer, src Rectangle, dst Point) {
renderer.Copy(t.texture, t.rect, target) t.CopyPartResize(renderer, src, RectSize(dst.X, dst.Y, src.W, src.H))
} }
// func (t *Texture) CopyF(renderer *sdl.Renderer, target *sdl.FRect) { func (t *Texture) CopyPartResize(renderer *sdl.Renderer, src Rectangle, dst Rectangle) {
// renderer.CopyF(t.texture, t.rect, target) // Depends on SDL >=2.0.10 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) 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() } func (t *Texture) Destroy() { t.texture.Destroy() }