From 243e204f48ead521d85627ee9f7cb0bd4382ae32 Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Sun, 10 May 2020 20:44:20 +0200 Subject: [PATCH] Added game mechanic to buy/plant flowers. --- buyflowerbutton.go | 8 +++- cmd/tins2020/tins2020.go | 7 +--- control.go | 12 +++--- eventhandler.go | 33 +++++++++++++++ game.go | 88 ++++++++++++++++++++++++++++++++++++---- gamecontrols.go | 40 ++++++++++++++---- herbarium.go | 44 ++++++++++++++++++++ iconbutton.go | 4 +- point.go | 2 + projection.go | 5 +++ terrainrenderer.go | 63 ++++++++++++++++++++-------- tools.go | 24 +++++++++++ 12 files changed, 281 insertions(+), 49 deletions(-) create mode 100644 eventhandler.go create mode 100644 herbarium.go create mode 100644 tools.go diff --git a/buyflowerbutton.go b/buyflowerbutton.go index 282e2b2..aec86a5 100644 --- a/buyflowerbutton.go +++ b/buyflowerbutton.go @@ -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) } diff --git a/cmd/tins2020/tins2020.go b/cmd/tins2020/tins2020.go index a1279b8..602d830 100644 --- a/cmd/tins2020/tins2020.go +++ b/cmd/tins2020/tins2020.go @@ -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) } diff --git a/control.go b/control.go index e7540e4..511ff95 100644 --- a/control.go +++ b/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 } diff --git a/eventhandler.go b/eventhandler.go new file mode 100644 index 0000000..c112093 --- /dev/null +++ b/eventhandler.go @@ -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) } diff --git a/game.go b/game.go index dbb6f99..8487b07 100644 --- a/game.go +++ b/game.go @@ -1,16 +1,21 @@ package tins2020 import ( + "log" "math/rand" "time" ) type Game struct { - Balance int - Speed GameSpeed - Terrain *Map + Balance int + Speed GameSpeed + SpeedBeforePause GameSpeed + Herbarium Herbarium + Terrain *Map - simulation Animation + tool Tool + toolChanged *Events + simulation Animation } type GameSpeed string @@ -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, + Speed: GameSpeedNormal, + Balance: 100, + Terrain: terrain, + Herbarium: herbarium, - simulation: NewAnimation(time.Millisecond * 10), + 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) +} diff --git a/gamecontrols.go b/gamecontrols.go index 83fa6f9..5675171 100644 --- a/gamecontrols.go +++ b/gamecontrols.go @@ -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 diff --git a/herbarium.go b/herbarium.go new file mode 100644 index 0000000..271ea22 --- /dev/null +++ b/herbarium.go @@ -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 } diff --git a/iconbutton.go b/iconbutton.go index 2cd449d..d6eea12 100644 --- a/iconbutton.go +++ b/iconbutton.go @@ -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 diff --git a/point.go b/point.go index 2a3fb3d..528a0bd 100644 --- a/point.go +++ b/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 { diff --git a/projection.go b/projection.go index 9c00816..80233d6 100644 --- a/projection.go +++ b/projection.go @@ -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) diff --git a/terrainrenderer.go b/terrainrenderer.go index 15701c5..b232380 100644 --- a/terrainrenderer.go +++ b/terrainrenderer.go @@ -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 { diff --git a/tools.go b/tools.go new file mode 100644 index 0000000..a532c3a --- /dev/null +++ b/tools.go @@ -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) +}