From 6a7843f31433b25653e52dc18c91d5230c1e724b Mon Sep 17 00:00:00 2001 From: Sander Schobers Date: Mon, 11 May 2020 03:09:01 +0200 Subject: [PATCH] Added serialization of game state. --- buyflowerbutton.go | 21 ++++++- drageable.go | 24 ++++++++ game.go | 136 ++++++++++++++++++++++++++++----------------- gamecontrols.go | 20 +++++-- gamestate.go | 54 ++++++++++++++++++ herbarium.go | 72 +++++++++++++++++++++++- map.go | 1 + noisemap.go | 35 +++++++----- projection.go | 6 +- terrainrenderer.go | 53 +++++------------- 10 files changed, 306 insertions(+), 116 deletions(-) create mode 100644 drageable.go create mode 100644 gamestate.go diff --git a/buyflowerbutton.go b/buyflowerbutton.go index 46380c5..08f79f4 100644 --- a/buyflowerbutton.go +++ b/buyflowerbutton.go @@ -38,29 +38,39 @@ func (b *BuyFlowerButton) animate() { } func (b *BuyFlowerButton) fmtTooltipText() string { - if b.Flower.Unlocked { + if !b.Flower.Unlocked { return fmt.Sprintf("%s - %s - %s", FmtMoney(b.Flower.BuyPrice), b.Flower.Name, "Traits are not known yet.") } return fmt.Sprintf("%s - %s - %s", FmtMoney(b.Flower.BuyPrice), b.Flower.Name, b.Flower.Description) } -func (b *BuyFlowerButton) Init(ctx *Context) error { - text := fmt.Sprintf("%s - %s - %s", FmtMoney(b.Flower.BuyPrice), b.Flower.Name, b.Flower.Description) +func (b *BuyFlowerButton) updateTexts(ctx *Context) error { + text := b.fmtTooltipText() font := ctx.Fonts.Font("small") color := MustHexColor("#ffffff") texture, err := font.Render(ctx.Renderer, text, color) if err != nil { return err } + 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 } + if b.priceTexture != nil { + b.priceTexture.Destroy() + } b.priceTexture = texture return nil } +func (b *BuyFlowerButton) Init(ctx *Context) error { + return b.updateTexts(ctx) +} + func (b *BuyFlowerButton) Handle(ctx *Context, event sdl.Event) { b.IconButton.Handle(ctx, event) if b.IsMouseOver && b.hoverAnimation == nil { @@ -98,3 +108,8 @@ func (b *BuyFlowerButton) Render(ctx *Context) { 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(ctx *Context, desc FlowerDescriptor) { + b.Flower = desc + b.updateTexts(ctx) +} diff --git a/drageable.go b/drageable.go new file mode 100644 index 0000000..b92daa7 --- /dev/null +++ b/drageable.go @@ -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 +} diff --git a/game.go b/game.go index 2fa3184..1d746f0 100644 --- a/game.go +++ b/game.go @@ -13,10 +13,11 @@ type Game struct { Herbarium Herbarium Terrain *Map - tool Tool - toolChanged *Events - speedChanged *Events - simulation Animation + tool Tool + centerChanged *Events + toolChanged *Events + speedChanged *Events + simulation Animation } type GameSpeed string @@ -40,60 +41,16 @@ func NewGame() *Game { Flowers: map[Point]Flower{}, } herbarium := NewHerbarium() - herbarium.Add("anemone", FlowerDescriptor{ - Name: "Anemone", - Description: "A very generic flower that thrives in a temperate climate.", - IconTemplate: "flower-anemone-%s", - BuyPrice: 10, - SellPrice: 3, - Traits: NewAnemoneTraits(), - Unlocked: true, - }) - herbarium.Add("loosestrife", FlowerDescriptor{ - Name: "Loosestrife", - Description: "A simple flower that will spread in temperate and wet climates.", - IconTemplate: "flower-loosestrife-%s", - BuyPrice: 100, - SellPrice: 20, - Traits: NewLoosestrifeTraits(), - Unlocked: false, - }) - herbarium.Add("coneflower", FlowerDescriptor{ - Name: "Coneflower", - Description: "A beautifull flower that can withstand hotter climates.", - IconTemplate: "flower-coneflower-%s", - BuyPrice: 500, - SellPrice: 100, - Traits: NewConeflowerTraits(), - Unlocked: false, - }) - herbarium.Add("tulip", FlowerDescriptor{ - Name: "Tulip", - Description: "A lovely flower that prefers a bit humid and colder climates.", - IconTemplate: "flower-tulip-%s", - BuyPrice: 20000, - SellPrice: 5000, - Traits: NewTulipTraits(), - Unlocked: false, - }) - herbarium.Add("ajuga", FlowerDescriptor{ - Name: "Ajuga", - Description: "A flower that is resitant to cold climates and spreads very easily.", - IconTemplate: "flower-ajuga-%s", - BuyPrice: 100000, - SellPrice: 10000, - Traits: NewAjugaTraits(), - Unlocked: false, - }) return &Game{ Speed: GameSpeedNormal, Balance: 100, Terrain: terrain, Herbarium: herbarium, - speedChanged: NewEvents(), - toolChanged: NewEvents(), - simulation: NewAnimation(time.Millisecond * 10), + centerChanged: NewEvents(), + speedChanged: NewEvents(), + toolChanged: NewEvents(), + simulation: NewAnimation(time.Millisecond * 10), } } @@ -159,6 +116,8 @@ func (g *Game) CancelTool() { g.selectTool(nil) } +func (g *Game) CenterChanged() EventHandler { return g.centerChanged } + func (g *Game) Dig(tile Point) { id := g.Terrain.DigFlower(tile) desc, ok := g.Herbarium.Find(id) @@ -168,6 +127,44 @@ func (g *Game) Dig(tile Point) { g.Balance += desc.SellPrice } +func (g *Game) Load() { + g.CancelTool() + g.Pause() + + var state GameState + err := state.Deserialize(SaveGameName()) + if err != nil { + log.Println("failed to load; error:", err) + return + } + + g.Herbarium = NewHerbarium() + for _, flower := range state.Herbarium.Flowers { + g.Herbarium.Update(flower.ID, func(desc *FlowerDescriptor) { + desc.Unlocked = flower.Unlocked + }) + } + + g.Balance = state.Balance + g.Terrain = &Map{ + Temp: NewNoiseMap(state.Terrain.Temperature), + Humid: NewNoiseMap(state.Terrain.Humidity), + Variant: NewRandomNoiseMap(state.Terrain.Variant), + PlaceX: NewRandomNoiseMap(state.Terrain.PlaceX), + PlaceY: NewRandomNoiseMap(state.Terrain.PlaceY), + 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(g.Terrain.Center) + + g.CancelTool() + g.setSpeed(state.Speed) +} + func (g *Game) Pause() { g.setSpeed(GameSpeedPaused) } func (g *Game) PlantFlower(id string, tile Point) { @@ -190,6 +187,14 @@ func (g *Game) Run() { g.setSpeed(GameSpeedNormal) } func (g *Game) RunFast() { g.setSpeed(GameSpeedFast) } +func (g *Game) Save() { + state := g.State() + err := state.Serialize(SaveGameName()) + if err != nil { + log.Println("failed to save; error:", err) + } +} + func (g *Game) SelectPlantFlowerTool(id string) { g.selectTool(&PlantFlowerTool{FlowerID: id}) } @@ -204,6 +209,33 @@ func (g *Game) SelectResearch() { func (g *Game) SpeedChanged() EventHandler { return g.speedChanged } +func (g *Game) State() GameState { + var state GameState + state.Balance = g.Balance + state.Speed = g.Speed + for _, id := range g.Herbarium.Flowers() { + flower, _ := g.Herbarium.Find(id) + state.Herbarium.Flowers = append(state.Herbarium.Flowers, HerbariumFlowerState{ + ID: id, + Unlocked: flower.Unlocked, + }) + } + flowers := make([]FlowerState, 0, len(g.Terrain.Flowers)) + for pos, flower := range g.Terrain.Flowers { + flowers = append(flowers, FlowerState{ID: flower.ID, Location: pos}) + } + state.Terrain = TerrainState{ + Temperature: g.Terrain.Temp.Seed(), + Humidity: g.Terrain.Humid.Seed(), + Variant: g.Terrain.Variant.Seed(), + PlaceX: g.Terrain.PlaceX.Seed(), + PlaceY: g.Terrain.PlaceY.Seed(), + Flowers: flowers, + } + state.View.Center = g.Terrain.Center + return state +} + func (g *Game) TogglePause() { if g.Speed == GameSpeedPaused { g.Resume() diff --git a/gamecontrols.go b/gamecontrols.go index 7259f0c..ddd39c2 100644 --- a/gamecontrols.go +++ b/gamecontrols.go @@ -58,6 +58,7 @@ func (c *GameControls) toolChanged(state interface{}) { for _, control := range c.flowers.Buttons { button := control.(*BuyFlowerButton) button.IsActive = button.FlowerID == flowerID + button.IsDisabled = !c.game.Herbarium.IsUnlocked(button.FlowerID) } _, shovel := tool.(*ShovelTool) c.shovel.IsActive = shovel @@ -103,13 +104,22 @@ func (c *GameControls) Init(ctx *Context) error { c.menu.Background = MustHexColor("#356dad") c.menu.Buttons = []Control{ - NewIconButton("control-settings", EmptyEvent(func() {})), - NewIconButton("control-save", EmptyEvent(func() {})), - NewIconButton("control-load", EmptyEvent(func() {})), + NewIconButton("control-settings", func(*Context) {}), + NewIconButton("control-save", func(*Context) { c.game.Save() }), + NewIconButton("control-load", func(ctx *Context) { + c.game.Load() + for _, b := range c.flowers.Buttons { + button := b.(*BuyFlowerButton) + flower, ok := c.game.Herbarium.Find(button.FlowerID) + if ok { + button.Update(ctx, flower) + } + } + }), } - c.shovel = NewIconButtonConfig("control-shovel", func(*Context) { c.game.SelectShovel() }, func(b *IconButton) { b.IconHeight = 48 }) - c.research = NewIconButtonConfig("control-research", func(*Context) { c.game.SelectResearch() }, func(b *IconButton) { b.IconHeight = 48 }) + c.shovel = NewIconButtonConfig("control-shovel", func(*Context) { c.game.SelectShovel() }, func(b *IconButton) { b.IconHeight = 32 }) + c.research = NewIconButtonConfig("control-research", func(*Context) { c.game.SelectResearch() }, func(b *IconButton) { b.IconHeight = 32 }) c.otherTools.Buttons = []Control{c.shovel, c.research} c.Container.AddChild(&c.menu) diff --git a/gamestate.go b/gamestate.go new file mode 100644 index 0000000..414fcdd --- /dev/null +++ b/gamestate.go @@ -0,0 +1,54 @@ +package tins2020 + +type FlowerState struct { + ID string + Location Point +} + +type GameState struct { + Balance int + Speed GameSpeed + Herbarium HerbariumState + Terrain TerrainState + View ViewState +} + +type HerbariumState struct { + Flowers []HerbariumFlowerState +} + +type HerbariumFlowerState struct { + ID string + Unlocked bool +} + +type TerrainState struct { + Temperature int64 + Humidity int64 + Variant int64 + PlaceX int64 + PlaceY int64 + Flowers []FlowerState +} + +type ViewState struct { + Center Point +} + +func (s *GameState) Serialize(name string) error { + path, err := UserFile(name) + if err != nil { + return err + } + return EncodeJSON(path, &s) +} + +func (s *GameState) Deserialize(name string) error { + path, err := UserFile(name) + if err != nil { + return err + } + return DecodeJSON(path, &s) +} + +func SaveGameName() string { return "savegame.json" } diff --git a/herbarium.go b/herbarium.go index f2807b2..e8f3011 100644 --- a/herbarium.go +++ b/herbarium.go @@ -11,7 +11,69 @@ type Herbarium struct { } func NewHerbarium() Herbarium { - return Herbarium{map[string]FlowerDescriptor{}, nil} + h := Herbarium{map[string]FlowerDescriptor{}, nil} + h.Reset() + return h +} + +func (h *Herbarium) Reset() { + h.flowers = map[string]FlowerDescriptor{} + h.order = nil + + h.Add("anemone", FlowerDescriptor{ + Name: "Anemone", + Description: "A very generic flower that thrives in a temperate climate.", + IconTemplate: "flower-anemone-%s", + BuyPrice: 10, + SellPrice: 3, + Traits: NewAnemoneTraits(), + Unlocked: true, + }) + h.Add("loosestrife", FlowerDescriptor{ + Name: "Loosestrife", + Description: "A simple flower that will spread in temperate and wet climates.", + IconTemplate: "flower-loosestrife-%s", + BuyPrice: 100, + SellPrice: 20, + Traits: NewLoosestrifeTraits(), + Unlocked: true, + }) + h.Add("coneflower", FlowerDescriptor{ + Name: "Coneflower", + Description: "A beautifull flower that can withstand hotter climates.", + IconTemplate: "flower-coneflower-%s", + BuyPrice: 500, + SellPrice: 100, + Traits: NewConeflowerTraits(), + Unlocked: true, + }) + h.Add("tulip", FlowerDescriptor{ + Name: "Tulip", + Description: "A lovely flower that prefers a bit humid and colder climates.", + IconTemplate: "flower-tulip-%s", + BuyPrice: 20000, + SellPrice: 5000, + Traits: NewTulipTraits(), + Unlocked: true, + }) + h.Add("ajuga", FlowerDescriptor{ + Name: "Ajuga", + Description: "A flower that is resitant to cold climates and spreads very easily.", + IconTemplate: "flower-ajuga-%s", + BuyPrice: 100000, + SellPrice: 10000, + Traits: NewAjugaTraits(), + Unlocked: true, + }) +} + +func (h *Herbarium) Update(id string, update func(*FlowerDescriptor)) { + flower, ok := h.flowers[id] + if !ok { + return + } + update(&flower) + h.flowers[id] = flower } type FlowerDescriptor struct { @@ -44,3 +106,11 @@ func (h *Herbarium) Find(id string) (FlowerDescriptor, bool) { } func (h *Herbarium) Flowers() []string { return h.order } + +func (h *Herbarium) IsUnlocked(id string) bool { + flower, ok := h.flowers[id] + if !ok { + return false + } + return flower.Unlocked +} diff --git a/map.go b/map.go index 95cc068..cb63b79 100644 --- a/map.go +++ b/map.go @@ -7,6 +7,7 @@ type Map struct { PlaceX NoiseMap // displacement map of props PlaceY NoiseMap + Center Point Flowers map[Point]Flower } diff --git a/noisemap.go b/noisemap.go index eeaf8cc..603286a 100644 --- a/noisemap.go +++ b/noisemap.go @@ -2,7 +2,18 @@ package tins2020 import "opslag.de/schobers/geom/noise" +func clipNormalized(x float64) float64 { + if x < 0 { + return 0 + } + if x > 1 { + return 1 + } + return x +} + type NoiseMap interface { + Seed() int64 Value(x, y int32) float64 } @@ -15,32 +26,24 @@ func NewNoiseMap(seed int64) NoiseMap { } } -func NewRandomNoiseMap(seed int64) NoiseMap { - return &randomNoiseMap{noise.NewPerlin(seed)} -} - type noiseMap struct { noise *noise.Perlin alpha, beta float64 harmonics int } -func clipNormalized(x float64) float64 { - if x < 0 { - return 0 - } - if x > 1 { - return 1 - } - return x -} - // Value generates the noise value for an x/y pair. -func (m *noiseMap) Value(x, y int32) 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) } +func (m noiseMap) Seed() int64 { return m.noise.Seed } + +func NewRandomNoiseMap(seed int64) NoiseMap { + return &randomNoiseMap{noise.NewPerlin(seed)} +} + type randomNoiseMap struct { *noise.Perlin } @@ -50,3 +53,5 @@ 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) } + +func (m randomNoiseMap) Seed() int64 { return m.Perlin.Seed } diff --git a/projection.go b/projection.go index 80233d6..6ac9914 100644 --- a/projection.go +++ b/projection.go @@ -6,6 +6,10 @@ import ( "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 @@ -41,7 +45,7 @@ func (p *projection) screenToMap(x, y int32) PointF { func (p *projection) screenToMapInt(x, y int32) Point { pos := p.screenToMap(x, y) - return Pt(int32(Round32(pos.X)), int32(Round32(pos.Y))) + return mapToTile(pos) } func (p *projection) screenToMapRel(x, y int32) PointF { diff --git a/terrainrenderer.go b/terrainrenderer.go index 3be78df..a084123 100644 --- a/terrainrenderer.go +++ b/terrainrenderer.go @@ -8,38 +8,14 @@ import ( type terrainRenderer struct { game *Game - terrain *Map hover *Point project projection drag Drageable } -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 -} - func NewTerrainRenderer(game *Game) Control { - return &terrainRenderer{game: game, terrain: game.Terrain, project: newProjection()} + return &terrainRenderer{game: game, project: newProjection()} } func (r *terrainRenderer) Arrange(ctx *Context, _ Rectangle) { @@ -47,6 +23,11 @@ func (r *terrainRenderer) Arrange(ctx *Context, _ Rectangle) { } 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) + }) r.project.update(ctx.Renderer) return nil } @@ -78,6 +59,7 @@ func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) { } if e.Type == sdl.MOUSEBUTTONUP { if r.drag.IsDragging() { + r.game.Terrain.Center = mapToTile(r.project.center) r.drag.Cancel() } } @@ -113,8 +95,9 @@ func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) { } func (r *terrainRenderer) Render(ctx *Context) { + terrain := r.game.Terrain toTileTexture := func(x, y int32) *Texture { - temp := r.terrain.Temp.Value(x, y) + temp := terrain.Temp.Value(x, y) if temp < .35 { return ctx.Textures.Texture("tile-snow") } @@ -178,14 +161,14 @@ func (r *terrainRenderer) Render(ctx *Context) { } toItemTexture := func(x, y int32) *Texture { - variant := r.terrain.Variant.Value(x, y) - flower, ok := r.terrain.Flowers[Pt(x, y)] + variant := terrain.Variant.Value(x, y) + flower, ok := terrain.Flowers[Pt(x, y)] if ok { desc, _ := r.game.Herbarium.Find(flower.ID) return ctx.Textures.Texture(desc.IconTemplate.Variant(variantToInt(variant))) } - temp := r.terrain.Temp.Value(x, y) - humid := r.terrain.Humid.Value(x, y) + temp := terrain.Temp.Value(x, y) + humid := terrain.Humid.Value(x, y) return toPropTexture(temp, humid, variant) } @@ -209,16 +192,8 @@ func (r *terrainRenderer) Render(ctx *Context) { return } - placeX, placeY := r.terrain.PlaceX.Value(x, y), r.terrain.PlaceY.Value(x, y) + 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)) }) - - // 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}) - // for y := int32(-40); y < 40; y++ { - // for x := int32(-40); x < 40; x++ { - // pos := r.project.mapToScreen(x, y) - // ctx.Fonts.Font("debug").RenderCopy(ctx.Renderer, fmt.Sprintf("(%d, %d)", x, y), pos, sdl.Color{R: 255, A: 255}) - // } - // } }