Added game mechanic to buy/plant flowers.
This commit is contained in:
parent
cd5ca3f04f
commit
243e204f48
@ -10,22 +10,26 @@ import (
|
||||
type BuyFlowerButton struct {
|
||||
IconButton
|
||||
|
||||
FlowerID string
|
||||
Name string
|
||||
Price int
|
||||
Description string
|
||||
|
||||
IsActive bool
|
||||
|
||||
hoverAnimation *Animation
|
||||
hoverOffset int32
|
||||
hoverTexture *Texture
|
||||
priceTexture *Texture
|
||||
}
|
||||
|
||||
func NewBuyFlowerButton(icon, iconDisabled, name string, price int, description string, isDisabled bool, onClick EventFn) *BuyFlowerButton {
|
||||
func NewBuyFlowerButton(icon, iconDisabled, flowerID, name string, price int, description string, isDisabled bool, onClick EventContextFn) *BuyFlowerButton {
|
||||
return &BuyFlowerButton{
|
||||
IconButton: *NewIconButtonConfig(icon, onClick, func(b *IconButton) {
|
||||
b.IconDisabled = iconDisabled
|
||||
b.IsDisabled = isDisabled
|
||||
}),
|
||||
FlowerID: flowerID,
|
||||
Name: name,
|
||||
Price: price,
|
||||
Description: description,
|
||||
@ -72,7 +76,7 @@ func (b *BuyFlowerButton) Render(ctx *Context) {
|
||||
|
||||
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 {
|
||||
if (b.IsMouseOver && !b.IsDisabled) || b.IsActive {
|
||||
mouseOverTexture.Copy(ctx.Renderer, pos)
|
||||
}
|
||||
|
||||
|
@ -93,7 +93,7 @@ func run() error {
|
||||
content := tins2020.NewContainer()
|
||||
app.AddChild(content)
|
||||
app.AddChild(overlays)
|
||||
content.AddChild(tins2020.NewTerrainRenderer(game.Terrain))
|
||||
content.AddChild(tins2020.NewTerrainRenderer(game))
|
||||
err = app.Init(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -118,11 +118,6 @@ func run() error {
|
||||
app.Arrange(ctx, tins2020.Rect(0, 0, w, h))
|
||||
ctx.Settings.Window.Size = tins2020.PtPtr(w, h)
|
||||
}
|
||||
case *sdl.KeyboardEvent:
|
||||
switch e.Keysym.Sym {
|
||||
case sdl.K_ESCAPE:
|
||||
ctx.Quit()
|
||||
}
|
||||
}
|
||||
app.Handle(ctx, event)
|
||||
}
|
||||
|
12
control.go
12
control.go
@ -9,11 +9,13 @@ type Control interface {
|
||||
Render(*Context)
|
||||
}
|
||||
|
||||
type EventFn func(*Context)
|
||||
type EventContextFn func(*Context)
|
||||
|
||||
type EmptyEventFn func()
|
||||
type EventFn func()
|
||||
|
||||
func EmptyEvent(fn EmptyEventFn) EventFn {
|
||||
type EventInterfaceFn func(interface{})
|
||||
|
||||
func EmptyEvent(fn EventFn) EventContextFn {
|
||||
return func(*Context) { fn() }
|
||||
}
|
||||
|
||||
@ -22,7 +24,7 @@ type ControlBase struct {
|
||||
|
||||
IsMouseOver bool
|
||||
|
||||
OnLeftMouseButtonClick EventFn
|
||||
OnLeftMouseButtonClick EventContextFn
|
||||
}
|
||||
|
||||
func (b *ControlBase) Arrange(ctx *Context, bounds Rectangle) { b.Bounds = bounds }
|
||||
@ -40,7 +42,7 @@ func (b *ControlBase) Handle(ctx *Context, event sdl.Event) {
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ControlBase) Invoke(ctx *Context, fn EventFn) {
|
||||
func (b *ControlBase) Invoke(ctx *Context, fn EventContextFn) {
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
|
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) }
|
72
game.go
72
game.go
@ -1,6 +1,7 @@
|
||||
package tins2020
|
||||
|
||||
import (
|
||||
"log"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
@ -8,8 +9,12 @@ import (
|
||||
type Game struct {
|
||||
Balance int
|
||||
Speed GameSpeed
|
||||
SpeedBeforePause GameSpeed
|
||||
Herbarium Herbarium
|
||||
Terrain *Map
|
||||
|
||||
tool Tool
|
||||
toolChanged *Events
|
||||
simulation Animation
|
||||
}
|
||||
|
||||
@ -56,16 +61,31 @@ func NewGame() *Game {
|
||||
PlaceY: NewRandomNoiseMap(rand.Int63()),
|
||||
Flowers: map[Point]Flower{},
|
||||
}
|
||||
terrain.AddFlower(Pt(0, 0), NewPoppyTraits())
|
||||
herbarium := NewHerbarium()
|
||||
herbarium.Add("poppy", FlowerDescriptor{
|
||||
Name: "Poppy",
|
||||
Description: "A very generic flower that thrives in a moderate climate.",
|
||||
IconTemplate: "flower-poppy-%s",
|
||||
Price: 10,
|
||||
Traits: NewPoppyTraits(),
|
||||
Unlocked: true,
|
||||
})
|
||||
return &Game{
|
||||
Speed: GameSpeedNormal,
|
||||
Balance: 100,
|
||||
Terrain: terrain,
|
||||
Herbarium: herbarium,
|
||||
|
||||
toolChanged: NewEvents(),
|
||||
simulation: NewAnimation(time.Millisecond * 10),
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) selectTool(t Tool) {
|
||||
g.tool = t
|
||||
g.toolChanged.Notify(t)
|
||||
}
|
||||
|
||||
func (g *Game) tick() {
|
||||
randomNeighbor := func(pos Point) Point {
|
||||
switch rand.Intn(4) {
|
||||
@ -97,11 +117,46 @@ func (g *Game) tick() {
|
||||
g.Terrain.Flowers = flowers
|
||||
}
|
||||
|
||||
func (g *Game) CancelTool() {
|
||||
g.selectTool(nil)
|
||||
}
|
||||
|
||||
func (g *Game) Dig(tile Point) {
|
||||
// TODO: implement
|
||||
}
|
||||
|
||||
func (g *Game) Pause() {
|
||||
if g.Speed == GameSpeedPaused {
|
||||
return
|
||||
}
|
||||
g.SpeedBeforePause = g.Speed
|
||||
g.Speed = GameSpeedPaused
|
||||
g.simulation.Pause()
|
||||
}
|
||||
|
||||
func (g *Game) PlantFlower(id string, tile Point) {
|
||||
flower, ok := g.Herbarium.Find(id)
|
||||
if !ok {
|
||||
log.Println("user was able to plant a flower that doesn't exist")
|
||||
return
|
||||
}
|
||||
if flower.Price > g.Balance {
|
||||
// TODO: notify user of insufficient balance?
|
||||
return
|
||||
}
|
||||
g.Balance -= flower.Price
|
||||
g.Terrain.AddFlower(tile, flower.Traits)
|
||||
}
|
||||
|
||||
func (g *Game) Resume() {
|
||||
switch g.SpeedBeforePause {
|
||||
case GameSpeedNormal:
|
||||
g.Run()
|
||||
case GameSpeedFast:
|
||||
g.RunFast()
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) Run() {
|
||||
g.Speed = GameSpeedNormal
|
||||
g.simulation.SetInterval(simulationInterval)
|
||||
@ -114,8 +169,23 @@ func (g *Game) RunFast() {
|
||||
g.simulation.Run()
|
||||
}
|
||||
|
||||
func (g *Game) SelectPlantFlowerTool(id string) {
|
||||
g.selectTool(&PlantFlowerTool{FlowerID: id})
|
||||
}
|
||||
|
||||
func (g *Game) Tool() Tool { return g.tool }
|
||||
|
||||
func (g *Game) ToolChanged() EventHandler { return g.toolChanged }
|
||||
|
||||
func (g *Game) Update() {
|
||||
for g.simulation.Animate() {
|
||||
g.tick()
|
||||
}
|
||||
}
|
||||
|
||||
func (g *Game) UserClickedTile(pos Point) {
|
||||
if g.tool == nil {
|
||||
return
|
||||
}
|
||||
g.tool.ClickedTile(g, pos)
|
||||
}
|
||||
|
@ -18,6 +18,34 @@ func NewGameControls(game *Game) *GameControls {
|
||||
return &GameControls{game: game}
|
||||
}
|
||||
|
||||
func (c *GameControls) createBuyFlowerButton(id string) *BuyFlowerButton {
|
||||
flower, _ := c.game.Herbarium.Find(id)
|
||||
return NewBuyFlowerButton(
|
||||
flower.IconTemplate.Variant(1),
|
||||
flower.IconTemplate.Disabled(),
|
||||
id,
|
||||
flower.Name,
|
||||
flower.Price,
|
||||
flower.Description,
|
||||
!flower.Unlocked,
|
||||
EmptyEvent(func() {
|
||||
c.game.SelectPlantFlowerTool(id)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
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.Buttons {
|
||||
button := control.(*BuyFlowerButton)
|
||||
button.IsActive = button.FlowerID == flowerID
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GameControls) updateSpeedControls() {
|
||||
disable := func(b *IconButton, speed GameSpeed) {
|
||||
b.IsDisabled = speed == c.game.Speed
|
||||
@ -34,15 +62,13 @@ func (c *GameControls) Arrange(ctx *Context, bounds Rectangle) {
|
||||
c.flowers.Arrange(ctx, RectSize(bounds.Right()-buttonBarWidth, bounds.Y, buttonBarWidth, bounds.H))
|
||||
}
|
||||
|
||||
func (c *GameControls) buyPoppy(ctx *Context) {
|
||||
c.game.Balance -= 10
|
||||
}
|
||||
|
||||
func (c *GameControls) Init(ctx *Context) error {
|
||||
c.game.ToolChanged().RegisterItf(c.toolChanged)
|
||||
|
||||
c.flowers.Background = MustHexColor("#356dad") // brown alternative? #4ac69a
|
||||
c.flowers.Buttons = []Control{
|
||||
NewBuyFlowerButton("flower-poppy-1", "flower-poppy-disabled", "Poppy", 10, "A very generic flower that thrives in a moderate climate.", false, c.buyPoppy),
|
||||
NewBuyFlowerButton("flower-red-c-1", "flower-poppy-disabled", "Unknown", 100, "Traits are not known yet.", true, nil),
|
||||
|
||||
for _, id := range c.game.Herbarium.Flowers() {
|
||||
c.flowers.Buttons = append(c.flowers.Buttons, c.createBuyFlowerButton(id))
|
||||
}
|
||||
|
||||
c.top.Orientation = OrientationHorizontal
|
||||
|
44
herbarium.go
Normal file
44
herbarium.go
Normal file
@ -0,0 +1,44 @@
|
||||
package tins2020
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Herbarium struct {
|
||||
flowers map[string]FlowerDescriptor
|
||||
order []string
|
||||
}
|
||||
|
||||
func NewHerbarium() Herbarium {
|
||||
return Herbarium{map[string]FlowerDescriptor{}, nil}
|
||||
}
|
||||
|
||||
type FlowerDescriptor struct {
|
||||
Name string
|
||||
Description string
|
||||
Price int
|
||||
Unlocked bool
|
||||
IconTemplate IconTemplate
|
||||
Traits FlowerTraits
|
||||
}
|
||||
|
||||
type IconTemplate string
|
||||
|
||||
func (t IconTemplate) Disabled() string { return t.Fmt("disabled") }
|
||||
|
||||
func (t IconTemplate) Fmt(s string) string { return fmt.Sprintf(string(t), s) }
|
||||
|
||||
func (t IconTemplate) Variant(i int) string { return t.Fmt(strconv.Itoa(i)) }
|
||||
|
||||
func (h *Herbarium) Add(id string, desc FlowerDescriptor) {
|
||||
h.flowers[id] = desc
|
||||
h.order = append(h.order, id)
|
||||
}
|
||||
|
||||
func (h *Herbarium) Find(id string) (FlowerDescriptor, bool) {
|
||||
flower, ok := h.flowers[id]
|
||||
return flower, ok
|
||||
}
|
||||
|
||||
func (h *Herbarium) Flowers() []string { return h.order }
|
@ -18,7 +18,7 @@ const (
|
||||
ScaleStretch
|
||||
)
|
||||
|
||||
func NewIconButton(icon string, onClick EventFn) *IconButton {
|
||||
func NewIconButton(icon string, onClick EventContextFn) *IconButton {
|
||||
return &IconButton{
|
||||
ControlBase: ControlBase{
|
||||
OnLeftMouseButtonClick: onClick,
|
||||
@ -27,7 +27,7 @@ func NewIconButton(icon string, onClick EventFn) *IconButton {
|
||||
}
|
||||
}
|
||||
|
||||
func NewIconButtonConfig(icon string, onClick EventFn, configure func(*IconButton)) *IconButton {
|
||||
func NewIconButtonConfig(icon string, onClick EventContextFn, configure func(*IconButton)) *IconButton {
|
||||
button := NewIconButton(icon, onClick)
|
||||
configure(button)
|
||||
return button
|
||||
|
2
point.go
2
point.go
@ -10,6 +10,8 @@ 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 {
|
||||
|
@ -39,6 +39,11 @@ func (p *projection) screenToMap(x, y int32) PointF {
|
||||
return p.center.Add(pos)
|
||||
}
|
||||
|
||||
func (p *projection) screenToMapInt(x, y int32) Point {
|
||||
pos := p.screenToMap(x, y)
|
||||
return Pt(int32(Round32(pos.X)), int32(Round32(pos.Y)))
|
||||
}
|
||||
|
||||
func (p *projection) screenToMapRel(x, y int32) PointF {
|
||||
normX := p.zoomInv * float32(x)
|
||||
normY := p.zoomInv * float32(y)
|
||||
|
@ -7,21 +7,39 @@ import (
|
||||
)
|
||||
|
||||
type terrainRenderer struct {
|
||||
game *Game
|
||||
terrain *Map
|
||||
hover *Point
|
||||
project projection
|
||||
|
||||
interact interaction
|
||||
drag Drageable
|
||||
}
|
||||
|
||||
type interaction struct {
|
||||
mousePos Point
|
||||
mouseLeftDown bool
|
||||
mouseDrag *Point
|
||||
type Drageable struct {
|
||||
start *Point
|
||||
dragged bool
|
||||
}
|
||||
|
||||
func NewTerrainRenderer(terrain *Map) Control {
|
||||
return &terrainRenderer{terrain: terrain, project: newProjection()}
|
||||
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
|
||||
}
|
||||
|
||||
func NewTerrainRenderer(game *Game) Control {
|
||||
return &terrainRenderer{game: game, terrain: game.Terrain, project: newProjection()}
|
||||
}
|
||||
|
||||
func (r *terrainRenderer) Arrange(ctx *Context, _ Rectangle) {
|
||||
@ -37,26 +55,35 @@ func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) {
|
||||
switch e := event.(type) {
|
||||
case *sdl.MouseButtonEvent:
|
||||
if r.project.windowInteractRect.IsPointInside(e.X, e.Y) {
|
||||
if e.Button == sdl.BUTTON_LEFT {
|
||||
r.interact.mouseLeftDown = e.Type == sdl.MOUSEBUTTONDOWN
|
||||
if r.interact.mouseLeftDown && r.interact.mouseDrag == nil {
|
||||
r.interact.mouseDrag = PtPtr(e.X, e.Y)
|
||||
} else if !r.interact.mouseLeftDown && r.interact.mouseDrag != nil {
|
||||
r.interact.mouseDrag = nil
|
||||
switch e.Button {
|
||||
case sdl.BUTTON_LEFT:
|
||||
down := e.Type == sdl.MOUSEBUTTONDOWN
|
||||
if down && !r.drag.IsDragging() {
|
||||
r.drag.Start(Pt(e.X, e.Y))
|
||||
} else if !down && r.drag.IsDragging() {
|
||||
r.drag.Cancel()
|
||||
}
|
||||
if e.Type == sdl.MOUSEBUTTONUP && !r.drag.HasDragged() {
|
||||
pos := r.project.screenToMapInt(e.X, e.Y)
|
||||
r.game.UserClickedTile(pos)
|
||||
}
|
||||
case sdl.BUTTON_RIGHT:
|
||||
if e.Type == sdl.MOUSEBUTTONDOWN {
|
||||
r.game.CancelTool()
|
||||
}
|
||||
}
|
||||
}
|
||||
case *sdl.MouseMotionEvent:
|
||||
if r.project.windowInteractRect.IsPointInside(e.X, e.Y) {
|
||||
hover := r.project.screenToMap(e.X, e.Y)
|
||||
r.hover = PtPtr(int32(Round32(hover.X)), int32(Round32(hover.Y)))
|
||||
hover := r.project.screenToMapInt(e.X, e.Y)
|
||||
r.hover = &hover
|
||||
} else {
|
||||
r.hover = nil
|
||||
}
|
||||
if r.interact.mouseDrag != nil {
|
||||
r.project.center = r.project.center.Sub(r.project.screenToMapRel(e.X-r.interact.mouseDrag.X, e.Y-r.interact.mouseDrag.Y))
|
||||
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)
|
||||
r.interact.mouseDrag = PtPtr(e.X, e.Y)
|
||||
}
|
||||
case *sdl.MouseWheelEvent:
|
||||
if r.hover != nil {
|
||||
|
24
tools.go
Normal file
24
tools.go
Normal file
@ -0,0 +1,24 @@
|
||||
package tins2020
|
||||
|
||||
type Tool interface {
|
||||
Type() string
|
||||
ClickedTile(*Game, Point)
|
||||
}
|
||||
|
||||
type PlantFlowerTool struct {
|
||||
FlowerID string
|
||||
}
|
||||
|
||||
func (t *PlantFlowerTool) Type() string { return "plant-flower" }
|
||||
|
||||
func (t *PlantFlowerTool) ClickedTile(game *Game, tile Point) {
|
||||
game.PlantFlower(t.FlowerID, tile)
|
||||
}
|
||||
|
||||
type ShovelTool struct{}
|
||||
|
||||
func (t *ShovelTool) Type() string { return "shovel" }
|
||||
|
||||
func (t *ShovelTool) ClickedTile(game *Game, tile Point) {
|
||||
game.Dig(tile)
|
||||
}
|
Loading…
Reference in New Issue
Block a user