Compare commits

...

4 Commits

Author SHA1 Message Date
c926ae2ef9 Added bonusses when flower is adjacent to more flowers 2020-05-14 08:39:42 +02:00
013fe4bdb6 Added tooltips. 2020-05-14 08:29:23 +02:00
f57a9dd845 Extended README. 2020-05-14 07:47:45 +02:00
76ac685cbb Added panning with keyboard.
Changed D for digging into H for harvesting.
2020-05-14 07:47:29 +02:00
15 changed files with 223 additions and 61 deletions

View File

@ -23,13 +23,21 @@ In Botanim you play the role of botanist and your goal is to cultivate flowers i
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. 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.
**Controls:** **Controls:**
- D: Selects shovel - H: Selects shovel
- R: Selects research - R: Selects research
- Spacebar: pauses game - Spacebar: pauses game
- 1: runs game at normal speed - 1: runs game at normal speed
- 2: runs game extra fast - 2: runs game extra fast
- Mouse wheel or plus/minus: zooms landscape - Mouse wheel or plus/minus: zooms landscape
- CTRL + left mouse button or middle mouse button: pans 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! Have fun playing!

View File

@ -21,7 +21,7 @@ type BuyFlowerButton struct {
func NewBuyFlowerButton(icon, iconDisabled, flowerID string, flower FlowerDescriptor, onClick EventContextFn) *BuyFlowerButton { func NewBuyFlowerButton(icon, iconDisabled, flowerID string, flower FlowerDescriptor, onClick EventContextFn) *BuyFlowerButton {
return &BuyFlowerButton{ return &BuyFlowerButton{
IconButton: *NewIconButtonConfig(icon, onClick, func(b *IconButton) { IconButton: *NewIconButtonConfigure(icon, onClick, func(b *IconButton) {
b.IconDisabled = iconDisabled b.IconDisabled = iconDisabled
b.IsDisabled = !flower.Unlocked b.IsDisabled = !flower.Unlocked
}), }),

View File

@ -136,6 +136,8 @@ func run() error {
app.Arrange(ctx, tins2020.RectAbs(0, 0, w, h)) app.Arrange(ctx, tins2020.RectAbs(0, 0, w, h))
ctx.Settings.Window.Size = tins2020.PtPtr(w, h) ctx.Settings.Window.Size = tins2020.PtPtr(w, h)
} }
case *sdl.MouseMotionEvent:
ctx.MousePosition = tins2020.Pt(e.X, e.Y)
} }
app.Handle(ctx, event) app.Handle(ctx, event)
} }

View File

@ -6,12 +6,13 @@ import (
) )
type Context struct { type Context struct {
Renderer *sdl.Renderer Renderer *sdl.Renderer
Fonts Fonts Fonts Fonts
Resources Resources Resources Resources
Textures Textures Textures Textures
Settings Settings Settings Settings
ShouldQuit bool MousePosition Point
ShouldQuit bool
} }
func NewContext(res *rice.Box) (*Context, error) { func NewContext(res *rice.Box) (*Context, error) {

View File

@ -22,12 +22,35 @@ func EmptyEvent(fn EventFn) EventContextFn {
type ControlBase struct { type ControlBase struct {
Bounds Rectangle Bounds Rectangle
FontName string
Foreground sdl.Color
IsDisabled bool IsDisabled bool
IsMouseOver bool IsMouseOver bool
OnLeftMouseButtonClick EventContextFn OnLeftMouseButtonClick EventContextFn
} }
func (c *ControlBase) ActualForeground() sdl.Color {
var none sdl.Color
if c.Foreground == none {
return MustHexColor("#ffffff")
}
return c.Foreground
}
func (c *ControlBase) ActualFont(ctx *Context) *Font {
name := c.ActualFontName()
return ctx.Fonts.Font(name)
}
func (c *ControlBase) ActualFontName() string {
if len(c.FontName) == 0 {
return "default"
}
return c.FontName
}
func (b *ControlBase) Arrange(ctx *Context, bounds Rectangle) { b.Bounds = bounds } 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 }

10
game.go
View File

@ -114,7 +114,15 @@ func (g *Game) Dig(tile Point) {
if !ok { if !ok {
return return
} }
g.Balance += desc.SellPrice 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() { func (g *Game) New() {

View File

@ -100,44 +100,61 @@ func (c *GameControls) Init(ctx *Context) error {
} }
c.top.Orientation = OrientationHorizontal c.top.Orientation = OrientationHorizontal
c.pause = NewIconButtonConfig("control-pause", EmptyEvent(func() { c.pause = NewIconButtonConfigure("control-pause", EmptyEvent(func() {
c.game.Pause() c.game.Pause()
}), func(b *IconButton) { }), func(b *IconButton) {
b.IconDisabled = "control-pause-disabled" b.IconDisabled = "control-pause-disabled"
b.Tooltip.Text = "Pause game"
}) })
c.run = NewIconButtonConfig("control-run", EmptyEvent(func() { c.run = NewIconButtonConfigure("control-run", EmptyEvent(func() {
c.game.Run() c.game.Run()
}), func(b *IconButton) { }), func(b *IconButton) {
b.IconDisabled = "control-run-disabled" b.IconDisabled = "control-run-disabled"
b.Tooltip.Text = "Run game at normal speed"
}) })
c.runFast = NewIconButtonConfig("control-run-fast", EmptyEvent(func() { c.runFast = NewIconButtonConfigure("control-run-fast", EmptyEvent(func() {
c.game.RunFast() c.game.RunFast()
}), func(b *IconButton) { }), func(b *IconButton) {
b.IconDisabled = "control-run-fast-disabled" b.IconDisabled = "control-run-fast-disabled"
b.Tooltip.Text = "Run game at fast speed"
}) })
c.speedChanged(c.game.Speed) c.speedChanged(c.game.Speed)
c.top.Buttons = []Control{c.pause, c.run, c.runFast} c.top.Buttons = []Control{c.pause, c.run, c.runFast}
c.menu.Background = MustHexColor("#356dad") c.menu.Background = MustHexColor("#356dad")
c.menu.Buttons = []Control{ c.menu.Buttons = []Control{
NewIconButtonConfig("control-settings", c.dialogs.ShowSettings, func(b *IconButton) { NewIconButtonConfigure("control-settings", c.dialogs.ShowSettings, func(b *IconButton) {
b.IsDisabled = true b.IsDisabled = true
b.IconDisabled = "#afafaf" b.IconDisabled = "#afafaf"
}), }),
NewIconButton("control-save", func(*Context) { c.game.Save() }), NewIconButtonConfigure("control-save", func(*Context) { c.game.Save() }, func(b *IconButton) {
NewIconButton("control-load", func(ctx *Context) { b.Tooltip.Text = "Save game (overwrites previous save; no confirmation)"
}),
NewIconButtonConfigure("control-load", func(ctx *Context) {
c.game.Load() c.game.Load()
c.updateFlowerControls(ctx) c.updateFlowerControls(ctx)
}, func(b *IconButton) {
b.Tooltip.Text = "Load last saved game (no confirmation)"
}), }),
NewIconButton("control-new", func(ctx *Context) { NewIconButtonConfigure("control-new", func(ctx *Context) {
c.game.New() c.game.New()
c.updateFlowerControls(ctx) c.updateFlowerControls(ctx)
}, func(b *IconButton) {
b.Tooltip.Text = "Start new game (no confirmation)"
}),
NewIconButtonConfigure("control-information", c.dialogs.ShowIntro, func(b *IconButton) {
b.Tooltip.Text = "Show information/intro"
}), }),
NewIconButton("control-information", c.dialogs.ShowIntro),
} }
c.shovel = NewIconButtonConfig("control-shovel", func(*Context) { c.game.SelectShovel() }, func(b *IconButton) { b.IconHeight = 32 }) c.shovel = NewIconButtonConfigure("control-shovel", func(*Context) { c.game.SelectShovel() }, func(b *IconButton) {
c.research = NewIconButtonConfig("control-research", c.dialogs.ShowResearch, func(b *IconButton) { b.IconHeight = 32 }) b.IconHeight = 32
b.Tooltip.Text = "Select harvest tool (key: H)"
})
c.research = NewIconButtonConfigure("control-research", c.dialogs.ShowResearch, func(b *IconButton) {
b.IconHeight = 32
b.Tooltip.Text = "Conduct research (key: R)"
})
c.otherTools.Buttons = []Control{c.shovel, c.research} c.otherTools.Buttons = []Control{c.shovel, c.research}
c.Container.AddChild(&c.menu) c.Container.AddChild(&c.menu)
@ -163,7 +180,7 @@ func (c *GameControls) Handle(ctx *Context, event sdl.Event) bool {
c.game.Run() c.game.Run()
case sdl.K_2: case sdl.K_2:
c.game.RunFast() c.game.RunFast()
case sdl.K_d: case sdl.K_h:
c.game.SelectShovel() c.game.SelectShovel()
case sdl.K_r: case sdl.K_r:
c.dialogs.ShowResearch(ctx) c.dialogs.ShowResearch(ctx)

View File

@ -1,5 +1,9 @@
package tins2020 package tins2020
import (
"github.com/veandco/go-sdl2/sdl"
)
type HoverEffect int type HoverEffect int
const ( const (
@ -19,6 +23,7 @@ type IconButton struct {
IconActive HoverEffect IconActive HoverEffect
IconHover HoverEffect IconHover HoverEffect
Tooltip Tooltip
IsActive bool IsActive bool
} }
@ -31,7 +36,7 @@ func NewIconButton(icon string, onClick EventContextFn) *IconButton {
} }
} }
func NewIconButtonConfig(icon string, onClick EventContextFn, configure func(*IconButton)) *IconButton { func NewIconButtonConfigure(icon string, onClick EventContextFn, configure func(*IconButton)) *IconButton {
button := NewIconButton(icon, onClick) button := NewIconButton(icon, onClick)
configure(button) configure(button)
return button return button
@ -57,6 +62,31 @@ func (b *IconButton) activeTexture(ctx *Context) *Texture {
return ctx.Textures.Texture(b.Icon) return ctx.Textures.Texture(b.Icon)
} }
func (b *IconButton) Arrange(ctx *Context, bounds Rectangle) {
b.ControlBase.Arrange(ctx, bounds)
b.Tooltip.Arrange(ctx, bounds)
}
func (b *IconButton) Handle(ctx *Context, event sdl.Event) bool {
if b.ControlBase.Handle(ctx, event) {
return true
}
if b.Tooltip.Handle(ctx, event) {
return true
}
return false
}
func (b *IconButton) Init(ctx *Context) error {
if err := b.ControlBase.Init(ctx); err != nil {
return err
}
if err := b.Tooltip.Init(ctx); err != nil {
return err
}
return nil
}
func (b *IconButton) Render(ctx *Context) { func (b *IconButton) Render(ctx *Context) {
iconTexture := b.activeTexture(ctx) iconTexture := b.activeTexture(ctx)
@ -81,6 +111,10 @@ func (b *IconButton) Render(ctx *Context) {
ctx.Renderer.FillRect(b.Bounds.SDLPtr()) ctx.Renderer.FillRect(b.Bounds.SDLPtr())
} }
iconTexture.SetColor(White) iconTexture.SetColor(White)
if len(b.Tooltip.Text) > 0 && b.IsMouseOver {
b.Tooltip.Render(ctx)
}
} }
type Scale int type Scale int

View File

@ -12,13 +12,13 @@ func (i *Intro) Init(ctx *Context) error {
"In Botanim you play the role of botanist and your goal is to cultivate flowers in an open landscape.\n\n" + "In Botanim you play the role of botanist and your goal is to cultivate flowers in an open landscape.\n\n" +
"Flowers can only grow (well) in certain climates based on two properties: humidity and temperature. Watch out for existing vegetation to get an idea how humid the land is and check the appearance of the tile to see how hot it is. When well placed your planted flower will spread soon but an odd choice might kill your flower almost instantly. So choose carefully. When the flower spread significantly you can dig up 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" + "Controls:\n" +
" - D: Selects shovel\n" + " - H: Selects shovel\n" +
" - R: Selects research\n" + " - R: Selects research\n" +
" - Spacebar: pauses game\n" + " - Spacebar: pauses game\n" +
" - 1: runs game at normal speed\n" + " - 1: runs game at normal speed\n" +
" - 2: runs game extra fast\n" + " - 2: runs game extra fast\n" +
" - Mouse wheel or plus/minus: zooms landscape\n" + " - Mouse wheel or plus/minus: zooms landscape\n" +
" - CTRL + left mouse button or middle mouse button: pans landscape\n" + " - W, A, S, D keys or CTRL + left mouse button or middle mouse button: pans landscape\n" +
"\n" + "\n" +
"Have fun playing!" "Have fun playing!"
i.SetContent(&i.welcome) i.SetContent(&i.welcome)

View File

@ -2,37 +2,18 @@ package tins2020
import ( import (
"strings" "strings"
"github.com/veandco/go-sdl2/sdl"
) )
type Label struct { type Label struct {
ControlBase ControlBase
FontColor sdl.Color
FontName string
Text string Text string
Alignment TextAlignment 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) { func (l *Label) Render(ctx *Context) {
font := ctx.Fonts.Font(l.fontName()) font := ctx.Fonts.Font(l.ActualFontName())
color := l.fontColor() color := l.ActualForeground()
bottom := l.Bounds.Y + l.Bounds.H bottom := l.Bounds.Y + l.Bounds.H
switch l.Alignment { switch l.Alignment {
case TextAlignmentCenter: case TextAlignmentCenter:
@ -49,8 +30,8 @@ type Paragraph struct {
} }
func (p *Paragraph) Render(ctx *Context) { func (p *Paragraph) Render(ctx *Context) {
font := ctx.Fonts.Font(p.fontName()) font := ctx.Fonts.Font(p.ActualFontName())
color := p.fontColor() color := p.ActualForeground()
fontHeight := int32(font.Height()) fontHeight := int32(font.Height())
lines := strings.Split(p.Text, "\n") lines := strings.Split(p.Text, "\n")

View File

@ -62,11 +62,10 @@ func (d *LargeDialog) Arrange(ctx *Context, bounds Rectangle) {
} }
func (d *LargeDialog) Init(ctx *Context) error { func (d *LargeDialog) Init(ctx *Context) error {
d.title = Label{ d.title.Text = "Botanim"
Text: "Botanim", d.title.FontName = "title"
FontName: "title", d.title.Alignment = TextAlignmentCenter
Alignment: TextAlignmentCenter,
}
d.close = IconButton{ d.close = IconButton{
Icon: "control-cancel", Icon: "control-cancel",
IconHover: HoverEffectColor, IconHover: HoverEffectColor,

17
map.go
View File

@ -15,6 +15,23 @@ func (m *Map) AddFlower(pos Point, id string, traits FlowerTraits) {
m.Flowers[pos] = m.NewFlower(pos, id, traits) m.Flowers[pos] = m.NewFlower(pos, id, traits)
} }
func (m *Map) FlowersOnAdjacentTiles(pos Point) int {
var count int
if _, ok := m.Flowers[Pt(pos.X+1, pos.Y)]; ok {
count++
}
if _, ok := m.Flowers[Pt(pos.X-1, pos.Y)]; ok {
count++
}
if _, ok := m.Flowers[Pt(pos.X, pos.Y+1)]; ok {
count++
}
if _, ok := m.Flowers[Pt(pos.X, pos.Y-1)]; ok {
count++
}
return count
}
func (m *Map) DigFlower(pos Point) string { func (m *Map) DigFlower(pos Point) string {
flower, ok := m.Flowers[pos] flower, ok := m.Flowers[pos]
if !ok { if !ok {

View File

@ -101,6 +101,20 @@ func (p *projection) visibleTiles(action func(int32, int32, Point)) {
} }
} }
func (p *projection) Pan(ctx *Context, delta PointF) {
p.center = p.center.Add(delta.Mul(p.zoomInv))
p.update(ctx.Renderer)
}
func (p *projection) SetZoom(ctx *Context, center PointF, zoom float32) {
if p.zoom == zoom {
return
}
p.center = center.Sub(center.Sub(p.center).Mul(p.zoom / zoom))
p.zoom = zoom
p.update(ctx.Renderer)
}
func (p *projection) ZoomOut(ctx *Context, center PointF) { func (p *projection) ZoomOut(ctx *Context, center PointF) {
if p.zoom <= .25 { if p.zoom <= .25 {
return return
@ -114,12 +128,3 @@ func (p *projection) ZoomIn(ctx *Context, center PointF) {
} }
p.SetZoom(ctx, center, 2*p.zoom) 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)
}

View File

@ -101,6 +101,14 @@ func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) bool {
r.project.ZoomOut(ctx, r.project.center) r.project.ZoomOut(ctx, r.project.center)
case sdl.K_KP_MINUS: case sdl.K_KP_MINUS:
r.project.ZoomOut(ctx, r.project.center) r.project.ZoomOut(ctx, r.project.center)
case sdl.K_w:
r.project.Pan(ctx, PtF(-1, -1))
case sdl.K_a:
r.project.Pan(ctx, PtF(-1, 1))
case sdl.K_s:
r.project.Pan(ctx, PtF(1, 1))
case sdl.K_d:
r.project.Pan(ctx, PtF(1, -1))
} }
} }
} }

59
tooltip.go Normal file
View File

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