diff --git a/animation.go b/animation.go deleted file mode 100644 index f3b4045..0000000 --- a/animation.go +++ /dev/null @@ -1,57 +0,0 @@ -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() - a.lastUpdate = 0 -} - -func (a *Animation) SetInterval(interval time.Duration) { - a.interval = interval -} diff --git a/buttonbar.go b/buttonbar.go deleted file mode 100644 index 3c2d081..0000000 --- a/buttonbar.go +++ /dev/null @@ -1,59 +0,0 @@ -package tins2020 - -import "github.com/veandco/go-sdl2/sdl" - -type ButtonBar struct { - Container - - Background sdl.Color - ButtonLength int32 - 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) - length := b.ButtonLength - switch b.Orientation { - case OrientationHorizontal: - if length == 0 { - length = bounds.H - } - offset := bounds.X - for i := range b.Buttons { - b.Buttons[i].Arrange(ctx, Rect(offset, bounds.Y, length, bounds.H)) - offset += length - } - default: - if length == 0 { - length = bounds.W - } - offset := bounds.Y - for i := range b.Buttons { - b.Buttons[i].Arrange(ctx, Rect(bounds.X, offset, bounds.W, length)) - offset += length - } - } -} - -func (b *ButtonBar) Render(ctx *Context) { - SetDrawColor(ctx.Renderer, b.Background) - ctx.Renderer.FillRect(b.Bounds.SDLPtr()) - b.Container.Render(ctx) -} - -type Orientation int - -const ( - OrientationVertical Orientation = iota - OrientationHorizontal -) diff --git a/buyflowerbutton.go b/buyflowerbutton.go index 63717b1..be9a236 100644 --- a/buyflowerbutton.go +++ b/buyflowerbutton.go @@ -2,37 +2,76 @@ package tins2020 import ( "fmt" + "image/color" "time" - "github.com/veandco/go-sdl2/sdl" + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg" + "opslag.de/schobers/zntg/ui" ) +type TextureCache struct { + Value ui.Texture +} + +func (c *TextureCache) Height() float32 { + if c.Value == nil { + return 0 + } + return c.Value.Height() +} + +func (c *TextureCache) Update(update func() (ui.Texture, error)) error { + texture, err := update() + if err != nil { + return err + } + if c.Value != nil { + c.Value.Destroy() + } + c.Value = texture + return nil +} + +func (c *TextureCache) Width() float32 { + if c.Value == nil { + return 0 + } + return c.Value.Width() +} + +func textUpdate(render ui.Renderer, font ui.Font, color color.Color, text string) func() (ui.Texture, error) { + return func() (ui.Texture, error) { return render.TextTexture(font, color, text) } +} + type BuyFlowerButton struct { IconButton - FlowerID string - Flower FlowerDescriptor + IconDisabled string + FlowerID string + Flower FlowerDescriptor - hoverAnimation *Animation - hoverOffset int32 - hoverTexture *Texture - priceTexture *Texture + upToDate bool + hoverAnimation zntg.Animation + hoverOffset float32 + hoverTexture TextureCache + priceTexture TextureCache } -func NewBuyFlowerButton(icon, iconDisabled, flowerID string, flower FlowerDescriptor, onClick EventContextFn) *BuyFlowerButton { +func NewBuyFlowerButton(icon, iconDisabled, flowerID string, flower FlowerDescriptor, click ui.EventEmptyFn) *BuyFlowerButton { return &BuyFlowerButton{ - IconButton: *NewIconButtonConfigure(icon, onClick, func(b *IconButton) { - b.IconDisabled = iconDisabled - b.IsDisabled = !flower.Unlocked + IconButton: *NewIconButtonConfigure(icon, click, func(b *IconButton) { + b.Disabled = !flower.Unlocked }), - FlowerID: flowerID, - Flower: flower, + IconDisabled: iconDisabled, + FlowerID: flowerID, + Flower: flower, } } func (b *BuyFlowerButton) animate() { b.hoverOffset++ - if b.hoverOffset > b.hoverTexture.Size().X+b.Bounds.W { + if b.hoverOffset > b.hoverTexture.Width()+b.Bounds().Dx() { b.hoverOffset = 0 } } @@ -44,75 +83,69 @@ func (b *BuyFlowerButton) fmtTooltipText() string { return fmt.Sprintf("%s - %s - %s", FmtMoney(b.Flower.BuyPrice), b.Flower.Name, b.Flower.Description) } -func (b *BuyFlowerButton) updateTexts(ctx *Context) error { +func (b *BuyFlowerButton) updateTexts(ctx ui.Context) error { + if b.upToDate { + return nil + } text := b.fmtTooltipText() - font := ctx.Fonts.Font("small") - color := MustHexColor("#ffffff") - texture, err := font.Render(ctx.Renderer, text, color) - if err != nil { + font := ctx.Fonts().Font("small") + color := zntg.MustHexColor("#FFFFFF") + if err := b.hoverTexture.Update(textUpdate(ctx.Renderer(), font, color, text)); 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 { + if err := b.priceTexture.Update(textUpdate(ctx.Renderer(), font, color, FmtMoney(b.Flower.BuyPrice))); err != nil { return err } - if b.priceTexture != nil { - b.priceTexture.Destroy() - } - b.priceTexture = texture + b.upToDate = true return nil } -func (b *BuyFlowerButton) Init(ctx *Context) error { - return b.updateTexts(ctx) -} - -func (b *BuyFlowerButton) Handle(ctx *Context, event sdl.Event) bool { - if b.IconButton.Handle(ctx, event) { - return true - } - 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) Handle(ctx ui.Context, e ui.Event) bool { + b.updateTexts(ctx) + b.IconButton.Handle(ctx, e) + if b.IsOver() && !b.hoverAnimation.IsActive() { + b.hoverAnimation.Interval = 10 * time.Millisecond + b.hoverAnimation.Start() + b.hoverOffset = b.priceTexture.Width() + } else if !b.IsOver() { + b.hoverAnimation.Pause() } return false } -func (b *BuyFlowerButton) Render(ctx *Context) { - iconTexture := b.activeTexture(ctx) +func (b *BuyFlowerButton) Render(ctx ui.Context) { + b.updateTexts(ctx) - pos := Pt(b.Bounds.X, b.Bounds.Y) - iconTexture.CopyResize(ctx.Renderer, Rect(pos.X, pos.Y-60, b.Bounds.W, 120)) - if (b.IsMouseOver && !b.IsDisabled) || b.IsActive { - SetDrawColor(ctx.Renderer, TransparentWhite) - ctx.Renderer.FillRect(b.Bounds.SDLPtr()) + bounds := b.Bounds() + pos := bounds.Min + + icon := ctx.Textures().Texture(b.Icon) + if !b.Flower.Unlocked { + disabled := ctx.Textures().Texture(b.IconDisabled) + if disabled != nil { + icon = disabled + } } + ctx.Renderer().DrawTexture(icon, geom.RectRelF32(pos.X, pos.Y-60, bounds.Dx(), 120)) + b.RenderActive(ctx) - if b.hoverAnimation != nil { - b.hoverAnimation.AnimateFn(b.animate) - } + b.hoverAnimation.AnimateFn(b.animate) - if b.IsMouseOver { - left := b.Bounds.W - 8 - b.hoverOffset - top := pos.Y + b.Bounds.H - 20 + if b.IsOver() { + left := bounds.Dx() - 8 - b.hoverOffset + top := pos.Y + bounds.Dy() - 20 if left < 0 { - part := RectAbs(-left, 0, b.hoverTexture.Size().X, b.hoverTexture.Size().Y) - b.hoverTexture.CopyPart(ctx.Renderer, part, Pt(pos.X, top)) + part := geom.RectF32(-left, 0, b.hoverTexture.Width(), b.hoverTexture.Height()) + ctx.Renderer().DrawTexturePointOptions(b.hoverTexture.Value, geom.PtF32(pos.X, top), ui.DrawOptions{Source: &part}) } else { - b.hoverTexture.Copy(ctx.Renderer, Pt(pos.X+left, top)) + ctx.Renderer().DrawTexturePoint(b.hoverTexture.Value, geom.PtF32(pos.X+left, top)) } } else { - b.priceTexture.Copy(ctx.Renderer, Pt(pos.X+b.Bounds.W-8-b.priceTexture.Size().X, pos.Y+b.Bounds.H-20)) + ctx.Renderer().DrawTexturePoint(b.priceTexture.Value, geom.PtF32(pos.X+bounds.Dx()-8-b.priceTexture.Width(), pos.Y+bounds.Dy()-20)) } } -func (b *BuyFlowerButton) Update(ctx *Context, desc FlowerDescriptor) { +func (b *BuyFlowerButton) Update(desc FlowerDescriptor) { b.Flower = desc - b.updateTexts(ctx) + b.upToDate = false } diff --git a/cmd/tins2020/tins2020.go b/cmd/tins2020/tins2020.go index 1d5cccd..759b133 100644 --- a/cmd/tins2020/tins2020.go +++ b/cmd/tins2020/tins2020.go @@ -2,11 +2,21 @@ package main import ( "flag" + "image/color" "log" + "opslag.de/schobers/fs/ricefs" + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg" + "opslag.de/schobers/zntg/addons/res" + "opslag.de/schobers/zntg/play" + "opslag.de/schobers/zntg/ui" + + _ "opslag.de/schobers/zntg/sdlui" // rendering backend + // _ "opslag.de/schobers/zntg/allg5ui" // rendering backend + rice "github.com/GeertJohan/go.rice" "github.com/veandco/go-sdl2/sdl" - "github.com/veandco/go-sdl2/ttf" "opslag.de/schobers/tins2020" ) @@ -20,10 +30,91 @@ func main() { } } -func logSDLVersion() { - var version sdl.Version - sdl.GetVersion(&version) - log.Printf("SDL version: %d.%d.%d", version.Major, version.Minor, version.Patch) +func openResources(box *rice.Box) ui.Resources { + fs := ricefs.NewFs(box) + resources, _ := res.NewAferoFallbackResources(`res`, fs, `botanim`) + return resources +} + +type app struct { + ui.Proxy + + settings *tins2020.Settings + game *tins2020.Game + dialogs *tins2020.Dialogs +} + +type fontDescriptor struct { + Name string + Path string + Size int +} + +func (a *app) loadFonts(ctx ui.Context, descriptors ...fontDescriptor) error { + fonts := ctx.Fonts() + for _, desc := range descriptors { + _, err := fonts.CreateFontPath(desc.Name, desc.Path, desc.Size) + if err != nil { + return err + } + } + return nil +} + +func (a *app) Init(ctx ui.Context) error { + if err := a.loadFonts(ctx, + fontDescriptor{"balance", "fonts/OpenSans-Bold.ttf", 40}, + fontDescriptor{"debug", "fonts/FiraMono-Regular.ttf", 12}, + fontDescriptor{"default", "fonts/OpenSans-Regular.ttf", 14}, + fontDescriptor{"small", "fonts/OpenSans-Regular.ttf", 12}, + fontDescriptor{"title", "fonts/OpenSans-Bold.ttf", 40}, + ); err != nil { + return err + } + + textureLoader := tins2020.NewResourceLoader() + textures := ctx.Textures() + if err := textureLoader.LoadFromFile(ctx.Resources(), "textures.txt", func(name, content string) error { + _, err := textures.CreateTexturePath(name, content, true) + return err + }); err != nil { + return err + } + + ctx.Overlays().AddOnTop("fps", &play.FPS{}, false) + + content := tins2020.NewContent(a.dialogs) + content.AddChild(tins2020.NewTerrainRenderer(a.game)) + controls := tins2020.NewGameControls(a.game, a.dialogs) + content.AddChild(controls) + a.Content = content + + a.dialogs.Init(ctx) + a.game.Reset(ctx) + controls.Init(ctx) + a.dialogs.ShowIntro(ctx) + + return nil +} + +func (a *app) Handle(ctx ui.Context, e ui.Event) bool { + switch e := e.(type) { + case *ui.DisplayMoveEvent: + location := e.Bounds.Min.ToInt() + a.settings.Window.Location = &location + case *ui.DisplayResizeEvent: + a.Arrange(ctx, e.Bounds, geom.ZeroPtF32, nil) + size := e.Bounds.Size().ToInt() + a.settings.Window.Size = &size + } + return a.Proxy.Handle(ctx, e) +} + +func (a *app) Render(ctx ui.Context) { + a.game.Update() + + ctx.Renderer().Clear(color.White) + a.Proxy.Render(ctx) } func run() error { @@ -31,135 +122,66 @@ func run() error { flag.BoolVar(&extract, "extract-resources", false, "extracts all resources to the current working directory") flag.Parse() - ctx, err := tins2020.NewContext(rice.MustFindBox("res")) - if err != nil { - return err - } - defer ctx.Destroy() - + box := rice.MustFindBox(`res`) if extract { - return copyBoxToDisk(ctx.Resources.Box()) + return copyBoxToDisk(box) + } + res := openResources(box) + + ptPtr := func(x, y int) *geom.Point { + p := geom.Pt(x, y) + return &p } - if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil { + settings := &tins2020.Settings{} + err := settings.Init() + if err != nil { return err } - defer sdl.Quit() + defer settings.Store() - logSDLVersion() - - if err := ttf.Init(); err != nil { - return err + if settings.Window.Location == nil { + settings.Window.Location = ptPtr(sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED) } - defer ttf.Quit() - - if ctx.Settings.Window.Location == nil { - ctx.Settings.Window.Location = tins2020.PtPtr(sdl.WINDOWPOS_UNDEFINED, sdl.WINDOWPOS_UNDEFINED) + if settings.Window.Size == nil { + settings.Window.Size = ptPtr(800, 600) } - if ctx.Settings.Window.Size == nil { - ctx.Settings.Window.Size = tins2020.PtPtr(800, 600) - } - if ctx.Settings.Window.VSync == nil { + if settings.Window.VSync == nil { vsync := true - ctx.Settings.Window.VSync = &vsync + settings.Window.VSync = &vsync } - - if *ctx.Settings.Window.VSync { - sdl.SetHint(sdl.HINT_RENDER_VSYNC, "1") - } - sdl.SetHint(sdl.HINT_RENDER_SCALE_QUALITY, "1") - window, err := sdl.CreateWindow("Botanim - TINS 2020", - ctx.Settings.Window.Location.X, ctx.Settings.Window.Location.Y, - ctx.Settings.Window.Size.X, ctx.Settings.Window.Size.Y, - sdl.WINDOW_SHOWN|sdl.WINDOW_RESIZABLE) - if err != nil { - return err - } - defer window.Destroy() - - renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED) - if err != nil { - return err - } - defer renderer.Destroy() - - ctx.Init(renderer) - - err = ctx.Fonts.LoadDesc( - tins2020.FontDescriptor{Name: "balance", Path: "fonts/OpenSans-Bold.ttf", Size: 40}, - tins2020.FontDescriptor{Name: "debug", Path: "fonts/FiraMono-Regular.ttf", Size: 12}, - tins2020.FontDescriptor{Name: "default", Path: "fonts/OpenSans-Regular.ttf", Size: 14}, - tins2020.FontDescriptor{Name: "small", Path: "fonts/OpenSans-Regular.ttf", Size: 12}, - tins2020.FontDescriptor{Name: "title", Path: "fonts/OpenSans-Bold.ttf", Size: 40}, - ) - if err != nil { - return err - } - textureLoader := tins2020.NewResourceLoader() - err = textureLoader.LoadFromFile(&ctx.Resources, "textures.txt", func(name, content string) error { - return ctx.Textures.Load(name, content) + renderer, err := ui.NewRenderer("Botanim - TINS 2020", settings.Window.Size.X, settings.Window.Size.Y, ui.NewRendererOptions{ + Location: &geom.PointF32{X: float32(settings.Window.Location.X), Y: float32(settings.Window.Location.Y)}, + Resizable: true, + VSync: *settings.Window.VSync, }) if err != nil { return err } + defer renderer.Destroy() + renderer.SetResourceProvider(func() ui.Resources { return res }) game := tins2020.NewGame() - - app := tins2020.NewContainer() - - overlays := tins2020.NewContainer() - dialogs := tins2020.NewDialogs(game) - - overlays.AddChild(dialogs) - overlays.AddChild(&tins2020.FPS{Show: &game.Debug}) - - content := tins2020.NewContent(dialogs) - content.AddChild(tins2020.NewTerrainRenderer(game)) - content.AddChild(tins2020.NewGameControls(game, dialogs)) - - app.AddChild(content) - app.AddChild(overlays) - - err = app.Init(ctx) - if err != nil { - return err + app := &app{ + game: game, + dialogs: tins2020.NewDialogs(game), + settings: settings, } - dialogs.ShowIntro(ctx) - - w, h := window.GetSize() - app.Arrange(ctx, tins2020.RectAbs(0, 0, w, h)) - - for { - for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { - switch e := event.(type) { - case *sdl.QuitEvent: - ctx.Quit() - break - case *sdl.WindowEvent: - switch e.Event { - case sdl.WINDOWEVENT_MOVED: - x, y := window.GetPosition() - ctx.Settings.Window.Location = tins2020.PtPtr(x, y) - case sdl.WINDOWEVENT_SIZE_CHANGED: - w, h := window.GetSize() - app.Arrange(ctx, tins2020.RectAbs(0, 0, 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) - } - game.Update() - - if ctx.ShouldQuit { - break - } - - renderer.SetDrawColor(0, 0, 0, 255) - renderer.Clear() - app.Render(ctx) - renderer.Present() + style := ui.DefaultStyle() + style.Palette = &ui.Palette{ + Background: zntg.MustHexColor(`#356DAD`), + Disabled: zntg.MustHexColor(`#DEDEDE`), + Primary: zntg.MustHexColor(`#356DAD`), + PrimaryDark: zntg.MustHexColor(`#15569F`), + PrimaryLight: zntg.MustHexColor(`#ABCAED`), + Secondary: zntg.MustHexColor(`#4AC69A`), + SecondaryDark: zntg.MustHexColor(`#0AA36D`), + SecondaryLight: zntg.MustHexColor(`#A6EED4`), + Text: color.White, + TextOnPrimary: color.White, + TextOnSecondary: color.White, + TextNegative: zntg.MustHexColor(`#F3590E`), + TextPositive: zntg.MustHexColor(`#65D80D`), } - return nil + return ui.Run(renderer, style, app) } diff --git a/color.go b/color.go deleted file mode 100644 index a12f61e..0000000 --- a/color.go +++ /dev/null @@ -1,30 +0,0 @@ -package tins2020 - -import ( - "opslag.de/schobers/tins2020/img" - - "github.com/veandco/go-sdl2/sdl" -) - -var Black = sdl.Color{R: 0, G: 0, B: 0, A: 255} -var Transparent = sdl.Color{R: 0, G: 0, B: 0, A: 0} -var TransparentWhite = sdl.Color{R: 255, G: 255, B: 255, A: 31} -var White = sdl.Color{R: 255, G: 255, B: 255, A: 255} - -func HexColor(s string) (sdl.Color, error) { - c, err := img.HexColor(s) - if err != nil { - return sdl.Color{}, err - } - return sdl.Color(c), nil -} - -func MustHexColor(s string) sdl.Color { return sdl.Color(img.MustHexColor(s)) } - -func SetDrawColor(renderer *sdl.Renderer, color sdl.Color) { - renderer.SetDrawColor(color.R, color.G, color.B, color.A) -} - -func SetDrawColorHex(renderer *sdl.Renderer, s string) { - SetDrawColor(renderer, MustHexColor(s)) -} diff --git a/color_test.go b/color_test.go deleted file mode 100644 index 0b6f08e..0000000 --- a/color_test.go +++ /dev/null @@ -1,33 +0,0 @@ -package tins2020 - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/veandco/go-sdl2/sdl" -) - -func TestHexColor(t *testing.T) { - type test struct { - Hex string - Success bool - Expected sdl.Color - } - tests := []test{ - test{Hex: "#AA3939", Success: true, Expected: sdl.Color{R: 170, G: 57, B: 57, A: 255}}, - test{Hex: "AA3939", Success: true, Expected: sdl.Color{R: 170, G: 57, B: 57, A: 255}}, - test{Hex: "#AA3939BB", Success: true, Expected: sdl.Color{R: 170, G: 57, B: 57, A: 187}}, - test{Hex: "AG3939", Success: false, Expected: sdl.Color{}}, - test{Hex: "AA3939A", Success: false, Expected: sdl.Color{}}, - test{Hex: "AA39A", Success: false, Expected: sdl.Color{}}, - } - for _, test := range tests { - color, err := HexColor(test.Hex) - if test.Success { - assert.Nil(t, err) - assert.Equal(t, color, test.Expected) - } else { - assert.NotNil(t, err) - } - } -} diff --git a/container.go b/container.go deleted file mode 100644 index 981b37b..0000000 --- a/container.go +++ /dev/null @@ -1,58 +0,0 @@ -package tins2020 - -import ( - "github.com/veandco/go-sdl2/sdl" -) - -type Container struct { - ControlBase - - Children []Control -} - -func NewContainer() *Container { - return &Container{} -} - -func (c *Container) AddChild(child Control) { - 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) bool { - if c.ControlBase.Handle(ctx, event) { - return true - } - for _, child := range c.Children { - if child.Handle(ctx, event) { - return true - } - } - return false -} - -func (c *Container) Init(ctx *Context) error { - c.ControlBase.Init(ctx) - - for _, child := range c.Children { - err := child.Init(ctx) - if err != nil { - return err - } - } - return nil -} - -func (c *Container) Render(ctx *Context) { - c.ControlBase.Render(ctx) - - for _, child := range c.Children { - child.Render(ctx) - } -} diff --git a/content.go b/content.go index eec93c4..40953bd 100644 --- a/content.go +++ b/content.go @@ -1,28 +1,34 @@ package tins2020 -import "github.com/veandco/go-sdl2/sdl" +import ( + "opslag.de/schobers/zntg/ui" +) // Content shortcuts events when a dialog is opened. type Content struct { - Container + ui.Proxy - dialogOverlayed bool + content ui.ContainerBase + shortcut bool } func NewContent(dialogs *Dialogs) *Content { content := &Content{} - dialogs.DialogOpened().Register(func() { - content.dialogOverlayed = true + content.Proxy.Content = &content.content + dialogs.DialogOpened().AddHandlerEmpty(func(ui.Context) { + content.shortcut = true }) - dialogs.DialogClosed().Register(func() { - content.dialogOverlayed = false + dialogs.DialogClosed().AddHandlerEmpty(func(ui.Context) { + content.shortcut = false }) return content } -func (c *Content) Handle(ctx *Context, event sdl.Event) bool { - if c.dialogOverlayed { +func (c *Content) AddChild(child ui.Control) { c.content.AddChild(child) } + +func (c *Content) Handle(ctx ui.Context, event ui.Event) bool { + if c.shortcut { return false } - return c.Container.Handle(ctx, event) + return c.Proxy.Handle(ctx, event) } diff --git a/context.go b/context.go deleted file mode 100644 index a65e971..0000000 --- a/context.go +++ /dev/null @@ -1,46 +0,0 @@ -package tins2020 - -import ( - rice "github.com/GeertJohan/go.rice" - "github.com/veandco/go-sdl2/sdl" -) - -type Context struct { - Renderer *sdl.Renderer - Fonts Fonts - Resources Resources - Textures Textures - Settings Settings - MousePosition Point - ShouldQuit bool -} - -func NewContext(res *rice.Box) (*Context, error) { - ctx := &Context{} - err := ctx.Settings.Init() - if err != nil { - return nil, err - } - err = ctx.Resources.Open(res) - if err != nil { - return nil, err - } - return ctx, nil -} - -func (c *Context) Destroy() { - c.Fonts.Destroy() - c.Resources.Destroy() - c.Textures.Destroy() - c.Settings.Store() -} - -func (c *Context) Init(renderer *sdl.Renderer) { - c.Renderer = renderer - c.Fonts.Init(c.Resources.Copy()) - c.Textures.Init(renderer, c.Resources.Copy()) - - c.Renderer.SetDrawBlendMode(sdl.BLENDMODE_BLEND) -} - -func (c *Context) Quit() { c.ShouldQuit = true } diff --git a/control.go b/control.go deleted file mode 100644 index 252910c..0000000 --- a/control.go +++ /dev/null @@ -1,82 +0,0 @@ -package tins2020 - -import "github.com/veandco/go-sdl2/sdl" - -type Control interface { - Init(*Context) error - Arrange(*Context, Rectangle) - Handle(*Context, sdl.Event) bool - Render(*Context) -} - -type EventContextFn func(*Context) - -type EventFn func() - -type EventInterfaceFn func(interface{}) - -func EmptyEvent(fn EventFn) EventContextFn { - return func(*Context) { fn() } -} - -type ControlBase struct { - Bounds Rectangle - - FontName string - Foreground sdl.Color - - IsDisabled bool - IsMouseOver bool - - 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 c.FontName == "" { - return "default" - } - return c.FontName -} - -func (b *ControlBase) Arrange(ctx *Context, bounds Rectangle) { b.Bounds = bounds } - -func (b *ControlBase) Init(*Context) error { return nil } - -func (b *ControlBase) Handle(ctx *Context, event sdl.Event) bool { - switch e := event.(type) { - case *sdl.MouseMotionEvent: - b.IsMouseOver = b.Bounds.IsPointInside(e.X, e.Y) - case *sdl.MouseButtonEvent: - if !b.IsDisabled && b.IsMouseOver && e.Button == sdl.BUTTON_LEFT && e.Type == sdl.MOUSEBUTTONDOWN { - return b.Invoke(ctx, b.OnLeftMouseButtonClick) - } - case *sdl.WindowEvent: - if e.Event == sdl.WINDOWEVENT_LEAVE { - b.IsMouseOver = false - } - } - return false -} - -func (b *ControlBase) Invoke(ctx *Context, fn EventContextFn) bool { - if fn == nil { - return false - } - fn(ctx) - return true -} - -func (b *ControlBase) Render(*Context) {} diff --git a/conversion_test.go b/conversion_test.go deleted file mode 100644 index a5cf9e1..0000000 --- a/conversion_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package tins2020_test - -import ( - "math/rand" - "testing" -) - -type native struct{ a, b, c, d int } - -type similar struct{ a, b, c, d int } - -func (s similar) toNative() native { return native{s.a, s.b, s.c, s.d} } - -type wrapper struct{ native } - -func (w wrapper) toNative() native { return w.native } - -func nativeFunction(n native) int { return n.a + n.b + n.c + n.d } - -func BenchmarkNative(b *testing.B) { - var sum int - for i := 0; i < b.N; i++ { - n := native{rand.Int(), rand.Int(), rand.Int(), rand.Int()} - sum += nativeFunction(n) - } -} - -func BenchmarkSimilar(b *testing.B) { - var sum int - for i := 0; i < b.N; i++ { - s := similar{rand.Int(), rand.Int(), rand.Int(), rand.Int()} - sum += nativeFunction(s.toNative()) - } -} - -func BenchmarkWrapper(b *testing.B) { - var sum int - for i := 0; i < b.N; i++ { - w := wrapper{native{rand.Int(), rand.Int(), rand.Int(), rand.Int()}} - sum += nativeFunction(w.toNative()) - } -} diff --git a/dial.go b/dial.go new file mode 100644 index 0000000..11ac3c9 --- /dev/null +++ b/dial.go @@ -0,0 +1,137 @@ +package tins2020 + +import ( + "math" + "strconv" + + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg/ui" +) + +type Dial struct { + ui.ContainerBase + + dialer Dialer + + typing string // current digit + digitCount int // number of times the digit is pressed + digits []DialDigit // digits +} + +func NewDial(dialer Dialer) *Dial { + dial := &Dial{dialer: dialer} + dial.digits = make([]DialDigit, 10) + + for i := range dial.digits { + j := i + dial.digits[i].Value = strconv.Itoa(i) + dial.digits[i].ControlClicked().AddHandler(func(ctx ui.Context, _ ui.ControlClickedArgs) { + dial.userTyped(ctx, j) + }) + dial.AddChild(&dial.digits[i]) + } + return dial +} + +func (d *Dial) userTyped(ctx ui.Context, i int) { + d.digits[i].Blink() + digit := strconv.Itoa(i) + if len(d.typing) == 0 || digit != d.typing { + d.typing = digit + d.digitCount = 1 + } else { + d.digitCount++ + } + + if !d.dialer.CanUserType(i) { + d.typing = "" + d.digitCount = 0 + d.dialer.UserGaveWrongInput() + } else if d.digitCount == i || d.digitCount == 10 { + d.typing = "" + d.digitCount = 0 + d.dialer.UserTyped(ctx, i) + } +} + +func (d *Dial) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) { + d.ControlBase.Arrange(ctx, bounds, offset, parent) + + center := bounds.Center() + size := bounds.Size() + + distance := size.Y * .3 + for i := range d.digits { + angle := (float32((10-i)%10)*0.16 + .2) * math.Pi + pos := geom.PtF32(distance*geom.Cos32(angle), .8*distance*geom.Sin32(angle)) + digitCenter := center.Add(pos) + d.digits[i].Arrange(ctx, geom.RectRelF32(digitCenter.X-24, digitCenter.Y-24, 48, 48), offset, d) + } +} + +func (d *Dial) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 { + return geom.PtF32(size.X, geom.NaN32()) +} + +func (d *Dial) Handle(ctx ui.Context, event ui.Event) bool { + if d.ContainerBase.Handle(ctx, event) { + return true + } + switch e := event.(type) { + case *ui.KeyDownEvent: + switch e.Key { + case ui.Key0: + d.userTyped(ctx, 0) + case ui.KeyPad0: + d.userTyped(ctx, 0) + case ui.Key1: + d.userTyped(ctx, 1) + case ui.KeyPad1: + d.userTyped(ctx, 1) + case ui.Key2: + d.userTyped(ctx, 2) + case ui.KeyPad2: + d.userTyped(ctx, 2) + case ui.Key3: + d.userTyped(ctx, 3) + case ui.KeyPad3: + d.userTyped(ctx, 3) + case ui.Key4: + d.userTyped(ctx, 4) + case ui.KeyPad4: + d.userTyped(ctx, 4) + case ui.Key5: + d.userTyped(ctx, 5) + case ui.KeyPad5: + d.userTyped(ctx, 5) + case ui.Key6: + d.userTyped(ctx, 6) + case ui.KeyPad6: + d.userTyped(ctx, 6) + case ui.Key7: + d.userTyped(ctx, 7) + case ui.KeyPad7: + d.userTyped(ctx, 7) + case ui.Key8: + d.userTyped(ctx, 8) + case ui.KeyPad8: + d.userTyped(ctx, 8) + case ui.Key9: + d.userTyped(ctx, 9) + case ui.KeyPad9: + d.userTyped(ctx, 9) + } + } + return false +} + +func (d *Dial) Reset() { + d.typing = "" + d.digitCount = 0 +} + +func (d *Dial) Tick() { + for i := range d.digits { + d.digits[i].Tick() + } +} diff --git a/dialdigit.go b/dialdigit.go new file mode 100644 index 0000000..6a8c23a --- /dev/null +++ b/dialdigit.go @@ -0,0 +1,34 @@ +package tins2020 + +import ( + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg" + "opslag.de/schobers/zntg/ui" +) + +type DialDigit struct { + ui.ControlBase + + Value string + + highlight int +} + +func (d *DialDigit) Blink() { + d.highlight = 4 +} + +func (d *DialDigit) Render(ctx ui.Context) { + color := zntg.MustHexColor(`#FFFFFF`) + if d.highlight > 0 { + color = zntg.MustHexColor(`#15569F`) + } + bounds := d.Bounds() + ctx.Fonts().TextAlign("title", geom.PtF32(bounds.Center().X, bounds.Min.Y), color, d.Value, ui.AlignCenter) +} + +func (d *DialDigit) Tick() { + if d.highlight > 0 { + d.highlight-- + } +} diff --git a/dialogs.go b/dialogs.go index 7bc6a44..e52bdad 100644 --- a/dialogs.go +++ b/dialogs.go @@ -1,66 +1,84 @@ package tins2020 +import ( + "opslag.de/schobers/zntg/ui" +) + type Dialogs struct { - Proxy + ui.Proxy - intro Control - settings Control - research Control + intro ui.Overlay + research ui.Overlay + settings ui.Overlay + nothing ui.Control - dialogClosed *Events - dialogOpened *Events + closed ui.Events + opened ui.Events } +const dialogsOverlayName = "dialogs" + func NewDialogs(game *Game) *Dialogs { - return &Dialogs{ - intro: &Intro{}, - settings: &LargeDialog{}, - research: NewResearch(game), + intro := NewIntro() + research := NewResearch(game) + settings := NewLargeDialog("Settings", &ui.Label{}) - dialogClosed: NewEvents(), - dialogOpened: NewEvents(), + dialogs := &Dialogs{ + intro: intro, + settings: settings, + research: research, + nothing: &ui.ControlBase{}, + } + + intro.CloseRequested().AddHandlerEmpty(dialogs.Close) + research.CloseRequested().AddHandlerEmpty(dialogs.Close) + settings.CloseRequested().AddHandlerEmpty(dialogs.Close) + + return dialogs +} + +func (d *Dialogs) Init(ctx ui.Context) { + overlays := ctx.Overlays() + overlays.AddOnTop(dialogsOverlayName, d, false) + + d.Content = d.nothing +} + +func (d *Dialogs) showDialog(ctx ui.Context, control ui.Control) { + if control == nil { + ctx.Overlays().Hide(dialogsOverlayName) + d.closed.Notify(ctx, control) + d.Content = d.nothing + } else { + d.Content = control + ctx.Overlays().Show(dialogsOverlayName) + d.opened.Notify(ctx, control) } } -func (d *Dialogs) showDialog(ctx *Context, control Control) { - d.SetContent(ctx, control) - control.(Dialog).ShowDialog(ctx, d.Close) - d.dialogOpened.Notify(nil) +func (d *Dialogs) Close(ctx ui.Context) { + d.showDialog(ctx, nil) } -func (d *Dialogs) Arrange(ctx *Context, bounds Rectangle) { - d.Proxy.Arrange(ctx, bounds) +func (d *Dialogs) DialogClosed() ui.EventHandler { return &d.closed } +func (d *Dialogs) DialogOpened() ui.EventHandler { return &d.opened } + +func (d *Dialogs) Hidden() { + d.Proxy.Hidden() } -func (d *Dialogs) DialogClosed() EventHandler { return d.dialogClosed } -func (d *Dialogs) DialogOpened() EventHandler { return d.dialogOpened } - -func (d *Dialogs) Init(ctx *Context) error { - err := d.intro.Init(ctx) - if err != nil { - return err - } - err = d.settings.Init(ctx) - if err != nil { - return err - } - err = d.research.Init(ctx) - return nil -} - -func (d *Dialogs) Close() { - d.SetContent(nil, nil) - d.dialogClosed.Notify(nil) -} - -func (d *Dialogs) ShowIntro(ctx *Context) { +func (d *Dialogs) ShowIntro(ctx ui.Context) { d.showDialog(ctx, d.intro) } -func (d *Dialogs) ShowResearch(ctx *Context) { +func (d *Dialogs) Shown() { + d.Proxy.Shown() +} + +func (d *Dialogs) ShowResearch(ctx ui.Context) { d.showDialog(ctx, d.research) } -func (d *Dialogs) ShowSettings(ctx *Context) { +func (d *Dialogs) ShowSettings(ctx ui.Context) { d.showDialog(ctx, d.settings) } diff --git a/drageable.go b/drageable.go deleted file mode 100644 index b92daa7..0000000 --- a/drageable.go +++ /dev/null @@ -1,24 +0,0 @@ -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/eventhandler.go b/eventhandler.go deleted file mode 100644 index c112093..0000000 --- a/eventhandler.go +++ /dev/null @@ -1,33 +0,0 @@ -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/fonts.go b/fonts.go deleted file mode 100644 index 5751b48..0000000 --- a/fonts.go +++ /dev/null @@ -1,109 +0,0 @@ -package tins2020 - -import ( - "fmt" - - "github.com/veandco/go-sdl2/sdl" - "github.com/veandco/go-sdl2/ttf" - "opslag.de/schobers/fs/vfs" -) - -type TextAlignment int - -const ( - TextAlignmentLeft TextAlignment = iota - TextAlignmentCenter - TextAlignmentRight -) - -type Font struct { - *ttf.Font -} - -func (f *Font) Render(renderer *sdl.Renderer, text string, color sdl.Color) (*Texture, error) { - surface, err := f.RenderUTF8Blended(text, color) - if err != nil { - return nil, err - } - defer surface.Free() - texture, err := NewTextureFromSurface(renderer, surface) - if err != nil { - return nil, err - } - return texture, nil -} - -func (f *Font) RenderCopyAlign(renderer *sdl.Renderer, text string, pos Point, color sdl.Color, align TextAlignment) error { - texture, err := f.Render(renderer, text, color) - if err != nil { - return err - } - defer texture.Destroy() - size := texture.Size() - switch align { - case TextAlignmentLeft: - texture.Copy(renderer, Pt(pos.X, pos.Y-size.Y)) - case TextAlignmentCenter: - texture.Copy(renderer, Pt(pos.X-(size.X/2), pos.Y-size.Y)) - case TextAlignmentRight: - texture.Copy(renderer, Pt(pos.X-size.X, pos.Y-size.Y)) - } - return nil -} - -func (f *Font) RenderCopy(renderer *sdl.Renderer, text string, pos Point, color sdl.Color) error { - return f.RenderCopyAlign(renderer, text, pos, color, TextAlignmentLeft) -} - -type Fonts struct { - dir vfs.CopyDir - fonts map[string]*Font -} - -func (f *Fonts) Init(dir vfs.CopyDir) { - f.dir = dir - f.fonts = map[string]*Font{} -} - -func (f *Fonts) Font(name string) *Font { return f.fonts[name] } - -type FontDescriptor struct { - Name string - Path string - Size int -} - -func (f *Fonts) LoadDesc(fonts ...FontDescriptor) error { - for _, desc := range fonts { - err := f.Load(desc.Name, desc.Path, desc.Size) - if err != nil { - return fmt.Errorf("error loading '%s'; error: %v", desc.Name, err) - } - } - return nil -} - -func (f *Fonts) Load(name, path string, size int) error { - fontPath, err := f.dir.Retrieve(path) - if err != nil { - return err - } - font, err := ttf.OpenFont(fontPath, size) - if err != nil { - return err - } - if font, ok := f.fonts[name]; ok { - font.Close() - } - f.fonts[name] = &Font{font} - return nil -} - -func (f *Fonts) Destroy() { - if f.fonts == nil { - return - } - for _, f := range f.fonts { - f.Close() - } -} diff --git a/fpsrenderer.go b/fpsrenderer.go deleted file mode 100644 index 57d48d2..0000000 --- a/fpsrenderer.go +++ /dev/null @@ -1,46 +0,0 @@ -package tins2020 - -import ( - "fmt" - "time" - - "github.com/veandco/go-sdl2/sdl" -) - -type FPS struct { - ControlBase - - Show *bool - start time.Time - stamp time.Duration - slot int - ticks []int - total int -} - -func (f *FPS) Init(*Context) error { - f.start = time.Now() - f.stamp = 0 - f.ticks = make([]int, 51) - return nil -} - -func (f *FPS) Render(ctx *Context) { - if f.Show == nil || !*f.Show { - return - } - - elapsed := time.Since(f.start) - stamp := elapsed / (20 * time.Millisecond) - for f.stamp < stamp { - f.total += f.ticks[f.slot] - f.slot = (f.slot + 1) % len(f.ticks) - f.total -= f.ticks[f.slot] - f.ticks[f.slot] = 0 - f.stamp++ - } - f.ticks[f.slot]++ - - font := ctx.Fonts.Font("debug") - font.RenderCopy(ctx.Renderer, fmt.Sprintf("FPS: %d", f.total), Pt(5, 17), sdl.Color{R: 255, G: 255, B: 255, A: 255}) -} diff --git a/game.go b/game.go index 12149d9..a8dc2ee 100644 --- a/game.go +++ b/game.go @@ -4,6 +4,10 @@ import ( "log" "math/rand" "time" + + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg" + "opslag.de/schobers/zntg/ui" ) type Game struct { @@ -16,10 +20,10 @@ type Game struct { Terrain *Map tool Tool - centerChanged *Events - toolChanged *Events - speedChanged *Events - simulation Animation + centerChanged ui.Events + toolChanged ui.Events + speedChanged ui.Events + simulation zntg.Animation } type GameSpeed string @@ -35,21 +39,17 @@ const fastSimulationInterval = 20 * time.Millisecond func NewGame() *Game { game := &Game{ - centerChanged: NewEvents(), - speedChanged: NewEvents(), - toolChanged: NewEvents(), - simulation: NewAnimation(time.Millisecond * 10), + simulation: zntg.Animation{Interval: time.Millisecond * 10}, } - game.Reset() return game } -func (g *Game) selectTool(t Tool) { +func (g *Game) selectTool(ctx ui.Context, t Tool) { g.tool = t - g.toolChanged.Notify(t) + g.toolChanged.Notify(ctx, t) } -func (g *Game) setSpeed(speed GameSpeed) { +func (g *Game) setSpeed(ctx ui.Context, speed GameSpeed) { if speed == g.Speed { return } @@ -57,35 +57,35 @@ func (g *Game) setSpeed(speed GameSpeed) { g.SpeedBeforePause = g.Speed } g.Speed = speed - g.speedChanged.Notify(speed) + g.speedChanged.Notify(ctx, speed) switch speed { case GameSpeedPaused: g.simulation.Pause() case GameSpeedNormal: - g.simulation.SetInterval(simulationInterval) - g.simulation.Run() + g.simulation.Interval = simulationInterval + g.simulation.Start() case GameSpeedFast: - g.simulation.SetInterval(fastSimulationInterval) - g.simulation.Run() + g.simulation.Interval = fastSimulationInterval + g.simulation.Start() } } func (g *Game) tick() { - randomNeighbor := func(pos Point) Point { + randomNeighbor := func(pos geom.Point) geom.Point { switch rand.Intn(4) { case 0: - return Pt(pos.X-1, pos.Y) + return geom.Pt(pos.X-1, pos.Y) case 1: - return Pt(pos.X, pos.Y-1) + return geom.Pt(pos.X, pos.Y-1) case 2: - return Pt(pos.X+1, pos.Y) + return geom.Pt(pos.X+1, pos.Y) case 3: - return Pt(pos.X, pos.Y+1) + return geom.Pt(pos.X, pos.Y+1) } return pos } - flowers := map[Point]Flower{} + flowers := map[geom.Point]Flower{} for pos, flower := range g.Terrain.Flowers { if rand.Float32() < flower.Traits.Spread { dst := randomNeighbor(pos) @@ -102,13 +102,13 @@ func (g *Game) tick() { g.Terrain.Flowers = flowers } -func (g *Game) CancelTool() { - g.selectTool(nil) +func (g *Game) CancelTool(ctx ui.Context) { + g.selectTool(ctx, nil) } -func (g *Game) CenterChanged() EventHandler { return g.centerChanged } +func (g *Game) CenterChanged() ui.EventHandler { return &g.centerChanged } -func (g *Game) Dig(tile Point) { +func (g *Game) Dig(tile geom.Point) { id := g.Terrain.DigFlower(tile) desc, ok := g.Herbarium.Find(id) if !ok { @@ -125,14 +125,14 @@ func (g *Game) Dig(tile Point) { } } -func (g *Game) New() { - g.Pause() - g.Reset() +func (g *Game) New(ctx ui.Context) { + g.Pause(ctx) + g.Reset(ctx) } -func (g *Game) Load() { - g.CancelTool() - g.Pause() +func (g *Game) Load(ctx ui.Context) { + g.CancelTool(ctx) + g.Pause(ctx) var state GameState err := state.Deserialize(SaveGameName()) @@ -155,22 +155,20 @@ func (g *Game) Load() { Variant: NewRandomNoiseMap(state.Terrain.Variant), PlaceX: NewRandomNoiseMap(state.Terrain.PlaceX), PlaceY: NewRandomNoiseMap(state.Terrain.PlaceY), - Flowers: map[Point]Flower{}, + Flowers: map[geom.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) + g.centerChanged.Notify(ctx, g.Terrain.Center) + g.setSpeed(ctx, state.Speed) } -func (g *Game) Pause() { g.setSpeed(GameSpeedPaused) } +func (g *Game) Pause(ctx ui.Context) { g.setSpeed(ctx, GameSpeedPaused) } -func (g *Game) PlantFlower(id string, tile Point) { +func (g *Game) PlantFlower(id string, tile geom.Point) { if g.Terrain.HasFlower(tile) { // TODO: notify user it tried to plant on tile with flower? return @@ -189,7 +187,7 @@ func (g *Game) PlantFlower(id string, tile Point) { g.Terrain.AddFlower(tile, id, flower.Traits) } -func (g *Game) Reset() { +func (g *Game) Reset(ctx ui.Context) { g.Balance = 100 g.Herbarium = NewHerbarium() g.Terrain = &Map{ @@ -198,17 +196,17 @@ func (g *Game) Reset() { Variant: NewRandomNoiseMap(rand.Int63()), PlaceX: NewRandomNoiseMap(rand.Int63()), PlaceY: NewRandomNoiseMap(rand.Int63()), - Flowers: map[Point]Flower{}, + Flowers: map[geom.Point]Flower{}, } - g.CancelTool() - g.setSpeed(GameSpeedNormal) + g.CancelTool(ctx) + g.setSpeed(ctx, GameSpeedNormal) } -func (g *Game) Resume() { g.setSpeed(g.SpeedBeforePause) } +func (g *Game) Resume(ctx ui.Context) { g.setSpeed(ctx, g.SpeedBeforePause) } -func (g *Game) Run() { g.setSpeed(GameSpeedNormal) } +func (g *Game) Run(ctx ui.Context) { g.setSpeed(ctx, GameSpeedNormal) } -func (g *Game) RunFast() { g.setSpeed(GameSpeedFast) } +func (g *Game) RunFast(ctx ui.Context) { g.setSpeed(ctx, GameSpeedFast) } func (g *Game) Save() { state := g.State() @@ -218,15 +216,15 @@ func (g *Game) Save() { } } -func (g *Game) SelectPlantFlowerTool(id string) { - g.selectTool(&PlantFlowerTool{FlowerID: id}) +func (g *Game) SelectPlantFlowerTool(ctx ui.Context, id string) { + g.selectTool(ctx, &PlantFlowerTool{FlowerID: id}) } -func (g *Game) SelectShovel() { - g.selectTool(&ShovelTool{}) +func (g *Game) SelectShovel(ctx ui.Context) { + g.selectTool(ctx, &ShovelTool{}) } -func (g *Game) SpeedChanged() EventHandler { return g.speedChanged } +func (g *Game) SpeedChanged() ui.EventHandler { return &g.speedChanged } func (g *Game) State() GameState { var state GameState @@ -255,22 +253,22 @@ func (g *Game) State() GameState { return state } -func (g *Game) TogglePause() { +func (g *Game) TogglePause(ctx ui.Context) { if g.Speed == GameSpeedPaused { - g.Resume() + g.Resume(ctx) } else { - g.Pause() + g.Pause(ctx) } } func (g *Game) Tool() Tool { return g.tool } -func (g *Game) ToolChanged() EventHandler { return g.toolChanged } +func (g *Game) ToolChanged() ui.EventHandler { return &g.toolChanged } -func (g *Game) UnlockNextFlower() { +func (g *Game) UnlockNextFlower(ctx ui.Context) { price := g.Herbarium.UnlockNext() g.Balance -= price - g.selectTool(nil) + g.selectTool(ctx, nil) } func (g *Game) Update() { @@ -279,7 +277,7 @@ func (g *Game) Update() { } } -func (g *Game) UserClickedTile(pos Point) { +func (g *Game) UserClickedTile(pos geom.Point) { if g.tool == nil { return } diff --git a/gamecontrols.go b/gamecontrols.go index e6b8d2e..5295dfb 100644 --- a/gamecontrols.go +++ b/gamecontrols.go @@ -1,19 +1,24 @@ package tins2020 import ( - "github.com/veandco/go-sdl2/sdl" + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg" + "opslag.de/schobers/zntg/play" + "opslag.de/schobers/zntg/ui" ) +const fpsOverlayName = "fps" + type GameControls struct { - Container + ui.ContainerBase game *Game dialogs *Dialogs - menu ButtonBar - top ButtonBar - flowers ButtonBar - otherTools ButtonBar + menu ui.StackPanel + top ui.StackPanel + flowers ui.StackPanel + otherTools ui.StackPanel pause *IconButton run *IconButton @@ -27,6 +32,90 @@ func NewGameControls(game *Game, dialogs *Dialogs) *GameControls { return &GameControls{game: game, dialogs: dialogs} } +func (c *GameControls) Init(ctx ui.Context) { + ctx.Overlays().AddOnTop(fpsOverlayName, &play.FPS{}, false) + c.game.SpeedChanged().AddHandler(c.speedChanged) + c.game.ToolChanged().AddHandler(c.toolChanged) + c.dialogs.DialogOpened().AddHandlerEmpty(func(ctx ui.Context) { c.game.Pause(ctx) }) + c.dialogs.DialogClosed().AddHandlerEmpty(func(ctx ui.Context) { + c.updateFlowerControls() + c.game.Resume(ctx) + }) + + c.flowers.Background = zntg.MustHexColor("#356DAD") + + for _, id := range c.game.Herbarium.Flowers() { + c.flowers.Children = append(c.flowers.Children, c.createBuyFlowerButton(id)) + } + + c.top.Orientation = ui.OrientationHorizontal + c.pause = NewIconButtonConfigure("control-pause", func(ctx ui.Context) { + c.game.Pause(ctx) + }, func(b *IconButton) { + b.DisabledColor = ctx.Style().Palette.Secondary + b.Tooltip = "Pause game" + }) + c.run = NewIconButtonConfigure("control-run", func(ctx ui.Context) { + c.game.Run(ctx) + }, func(b *IconButton) { + b.DisabledColor = ctx.Style().Palette.Secondary + b.Tooltip = "Run game at normal speed" + }) + c.runFast = NewIconButtonConfigure("control-run-fast", func(ctx ui.Context) { + c.game.RunFast(ctx) + }, func(b *IconButton) { + b.DisabledColor = ctx.Style().Palette.Secondary + b.Tooltip = "Run game at fast speed" + }) + c.speedChanged(nil, c.game.Speed) + c.top.Children = []ui.Control{c.pause, c.run, c.runFast} + + c.menu.Background = zntg.MustHexColor("#356DAD") + c.menu.Children = []ui.Control{ + NewIconButtonConfigure("control-settings", c.dialogs.ShowSettings, func(b *IconButton) { + b.Disabled = true + b.DisabledColor = zntg.MustHexColor("#AFAFAF") + }), + NewIconButtonConfigure("control-save", func(ui.Context) { c.game.Save() }, func(b *IconButton) { + b.Tooltip = "Save game (overwrites previous save; no confirmation)" + }), + NewIconButtonConfigure("control-load", func(ctx ui.Context) { + c.game.Load(ctx) + c.updateFlowerControls() + }, func(b *IconButton) { + b.Tooltip = "Load last saved game (no confirmation)" + }), + NewIconButtonConfigure("control-new", func(ctx ui.Context) { + c.game.New(ctx) + c.updateFlowerControls() + }, func(b *IconButton) { + b.Tooltip = "Start new game (no confirmation)" + }), + NewIconButtonConfigure("control-information", c.dialogs.ShowIntro, func(b *IconButton) { + b.Tooltip = "Show information/intro" + }), + } + for i, child := range c.menu.Children { + c.menu.Children[i] = ui.FixedHeight(child, 96) + } + + c.shovel = NewIconButtonConfigure("control-shovel", func(ctx ui.Context) { c.game.SelectShovel(ctx) }, func(b *IconButton) { + b.Tooltip = "Select harvest tool (key: H)" + }) + c.research = NewIconButtonConfigure("control-research", c.dialogs.ShowResearch, func(b *IconButton) { + b.Tooltip = "Conduct research (key: R)" + }) + c.otherTools.Children = []ui.Control{c.shovel, c.research} + for i, child := range c.otherTools.Children { + c.otherTools.Children[i] = ui.FixedHeight(child, 96) + } + + c.AddChild(&c.menu) + c.AddChild(&c.top) + c.AddChild(&c.flowers) + c.AddChild(&c.otherTools) +} + func (c *GameControls) createBuyFlowerButton(id string) *BuyFlowerButton { flower, _ := c.game.Herbarium.Find(id) return NewBuyFlowerButton( @@ -34,176 +123,96 @@ func (c *GameControls) createBuyFlowerButton(id string) *BuyFlowerButton { flower.IconTemplate.Disabled(), id, flower, - EmptyEvent(func() { - c.game.SelectPlantFlowerTool(id) - }), + func(ctx ui.Context) { + c.game.SelectPlantFlowerTool(ctx, id) + }, ) } -func (c *GameControls) speedChanged(state interface{}) { +func (c *GameControls) speedChanged(_ ui.Context, state interface{}) { speed := state.(GameSpeed) disable := func(b *IconButton, expected GameSpeed) { - b.IsDisabled = speed == expected + b.Disabled = speed == expected } disable(c.pause, GameSpeedPaused) disable(c.run, GameSpeedNormal) disable(c.runFast, GameSpeedFast) } -func (c *GameControls) toolChanged(state interface{}) { +func (c *GameControls) toolChanged(_ ui.Context, state interface{}) { tool, _ := state.(Tool) var flowerID string if tool, ok := tool.(*PlantFlowerTool); ok { flowerID = tool.FlowerID } - for _, control := range c.flowers.Buttons { + for _, control := range c.flowers.Children { button := control.(*BuyFlowerButton) - button.IsActive = button.FlowerID == flowerID - button.IsDisabled = !c.game.Herbarium.IsUnlocked(button.FlowerID) + button.Active = button.FlowerID == flowerID + button.Disabled = !c.game.Herbarium.IsUnlocked(button.FlowerID) } _, shovel := tool.(*ShovelTool) - c.shovel.IsActive = shovel + c.shovel.Active = shovel } -func (c *GameControls) updateFlowerControls(ctx *Context) { - for _, b := range c.flowers.Buttons { +func (c *GameControls) updateFlowerControls() { + for _, b := range c.flowers.Children { button := b.(*BuyFlowerButton) flower, ok := c.game.Herbarium.Find(button.FlowerID) if ok { - button.Update(ctx, flower) + button.Update(flower) } } } -func (c *GameControls) Arrange(ctx *Context, bounds Rectangle) { - c.Bounds = bounds - c.menu.Arrange(ctx, Rect(bounds.X, bounds.Y, buttonBarWidth, bounds.H)) - c.top.Arrange(ctx, RectAbs(bounds.X+bounds.W/2+8, bounds.Y, bounds.Right(), bounds.Y+64)) - c.flowers.Arrange(ctx, Rect(bounds.Right()-buttonBarWidth, bounds.Y, buttonBarWidth, bounds.H)) - c.otherTools.Arrange(ctx, Rect(bounds.Right()-buttonBarWidth, bounds.Bottom()-2*buttonBarWidth, buttonBarWidth, 2*buttonBarWidth)) +const buttonBarWidth = 96 + +func (c *GameControls) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) { + c.ContainerBase.Arrange(ctx, bounds, offset, parent) + + c.menu.Arrange(ctx, geom.RectRelF32(bounds.Min.X, bounds.Min.Y, buttonBarWidth, bounds.Dy()), offset, c) + c.top.Arrange(ctx, geom.RectF32(bounds.Min.X+bounds.Dx()/2+8, bounds.Min.Y, bounds.Max.X, bounds.Min.Y+64), offset, c) + c.flowers.Arrange(ctx, geom.RectRelF32(bounds.Max.X-buttonBarWidth, bounds.Min.Y, buttonBarWidth, bounds.Dy()), offset, c) + otherToolsSize := c.otherTools.DesiredSize(ctx, bounds.Size()) + c.otherTools.Arrange(ctx, geom.RectRelF32(bounds.Max.X-buttonBarWidth, bounds.Max.Y-otherToolsSize.Y, buttonBarWidth, 2*buttonBarWidth), offset, c) } -func (c *GameControls) Init(ctx *Context) error { - c.game.SpeedChanged().RegisterItf(c.speedChanged) - c.game.ToolChanged().RegisterItf(c.toolChanged) - c.dialogs.DialogOpened().Register(func() { c.game.Pause() }) - c.dialogs.DialogClosed().Register(func() { - c.updateFlowerControls(ctx) - c.game.Resume() - }) - - c.flowers.Background = MustHexColor("#356dad") - c.flowers.ButtonLength = 64 - - for _, id := range c.game.Herbarium.Flowers() { - c.flowers.Buttons = append(c.flowers.Buttons, c.createBuyFlowerButton(id)) - } - - c.top.Orientation = OrientationHorizontal - c.pause = NewIconButtonConfigure("control-pause", EmptyEvent(func() { - c.game.Pause() - }), func(b *IconButton) { - b.IconDisabled = "control-pause-disabled" - b.Tooltip.Text = "Pause game" - }) - c.run = NewIconButtonConfigure("control-run", EmptyEvent(func() { - c.game.Run() - }), func(b *IconButton) { - b.IconDisabled = "control-run-disabled" - b.Tooltip.Text = "Run game at normal speed" - }) - c.runFast = NewIconButtonConfigure("control-run-fast", EmptyEvent(func() { - c.game.RunFast() - }), func(b *IconButton) { - b.IconDisabled = "control-run-fast-disabled" - b.Tooltip.Text = "Run game at fast speed" - }) - c.speedChanged(c.game.Speed) - c.top.Buttons = []Control{c.pause, c.run, c.runFast} - - c.menu.Background = MustHexColor("#356dad") - c.menu.Buttons = []Control{ - NewIconButtonConfigure("control-settings", c.dialogs.ShowSettings, func(b *IconButton) { - b.IsDisabled = true - b.IconDisabled = "#afafaf" - }), - NewIconButtonConfigure("control-save", func(*Context) { c.game.Save() }, func(b *IconButton) { - b.Tooltip.Text = "Save game (overwrites previous save; no confirmation)" - }), - NewIconButtonConfigure("control-load", func(ctx *Context) { - c.game.Load() - c.updateFlowerControls(ctx) - }, func(b *IconButton) { - b.Tooltip.Text = "Load last saved game (no confirmation)" - }), - NewIconButtonConfigure("control-new", func(ctx *Context) { - c.game.New() - 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" - }), - } - - c.shovel = NewIconButtonConfigure("control-shovel", func(*Context) { c.game.SelectShovel() }, func(b *IconButton) { - 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.Container.AddChild(&c.menu) - c.Container.AddChild(&c.top) - c.Container.AddChild(&c.flowers) - c.Container.AddChild(&c.otherTools) - - return c.Container.Init(ctx) -} - -func (c *GameControls) Handle(ctx *Context, event sdl.Event) bool { - if c.Container.Handle(ctx, event) { +func (c *GameControls) Handle(ctx ui.Context, event ui.Event) bool { + if c.ContainerBase.Handle(ctx, event) { return true } switch e := event.(type) { - case *sdl.KeyboardEvent: - if e.Type == sdl.KEYDOWN { - switch e.Keysym.Sym { - case sdl.K_SPACE: - c.game.TogglePause() - case sdl.K_1: - c.game.Run() - case sdl.K_2: - c.game.RunFast() - case sdl.K_h: - c.game.SelectShovel() - case sdl.K_r: - c.dialogs.ShowResearch(ctx) - case sdl.K_ESCAPE: - if c.game.Tool() == nil { - c.dialogs.ShowIntro(ctx) - } else { - c.game.CancelTool() - } - return true - case sdl.K_F3: - c.game.Debug = !c.game.Debug + case *ui.KeyDownEvent: + switch e.Key { + case ui.KeySpace: + c.game.TogglePause(ctx) + case ui.Key1: + c.game.Run(ctx) + case ui.Key2: + c.game.RunFast(ctx) + case ui.KeyH: + c.game.SelectShovel(ctx) + case ui.KeyR: + c.dialogs.ShowResearch(ctx) + case ui.KeyEscape: + if c.game.Tool() == nil { + c.dialogs.ShowIntro(ctx) + } else { + c.game.CancelTool(ctx) } + return true + case ui.KeyF4: + c.game.Debug = !c.game.Debug + ctx.Overlays().Toggle(fpsOverlayName) } } return false } -func (c *GameControls) Render(ctx *Context) { - topBar := MustHexColor("#0000007f") - SetDrawColor(ctx.Renderer, topBar) - ctx.Renderer.FillRect(RectAbs(c.menu.Bounds.Right(), 0, c.flowers.Bounds.X, 64).SDLPtr()) - ctx.Fonts.Font("balance").RenderCopyAlign(ctx.Renderer, FmtMoney(c.game.Balance), Pt(c.top.Bounds.X-8, 58), MustHexColor("#4AC69A"), TextAlignmentRight) +func (c *GameControls) Render(ctx ui.Context) { + topBar := zntg.MustHexColor("#0000007F") + ctx.Renderer().FillRectangle(geom.RectF32(c.menu.Bounds().Max.X, 0, c.flowers.Bounds().Min.X, 64), topBar) + ctx.Fonts().TextAlign("balance", geom.PtF32(c.top.Bounds().Min.X-8, 4), zntg.MustHexColor("#4AC69A"), FmtMoney(c.game.Balance), ui.AlignRight) - c.Container.Render(ctx) + c.ContainerBase.Render(ctx) } diff --git a/gamestate.go b/gamestate.go index 414fcdd..5bba7cc 100644 --- a/gamestate.go +++ b/gamestate.go @@ -1,8 +1,13 @@ package tins2020 +import ( + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg" +) + type FlowerState struct { ID string - Location Point + Location geom.Point } type GameState struct { @@ -32,7 +37,7 @@ type TerrainState struct { } type ViewState struct { - Center Point + Center geom.Point } func (s *GameState) Serialize(name string) error { @@ -40,7 +45,7 @@ func (s *GameState) Serialize(name string) error { if err != nil { return err } - return EncodeJSON(path, &s) + return zntg.EncodeJSON(path, &s) } func (s *GameState) Deserialize(name string) error { @@ -48,7 +53,7 @@ func (s *GameState) Deserialize(name string) error { if err != nil { return err } - return DecodeJSON(path, &s) + return zntg.DecodeJSON(path, &s) } func SaveGameName() string { return "savegame.json" } diff --git a/iconbutton.go b/iconbutton.go index 0481e15..30bdb88 100644 --- a/iconbutton.go +++ b/iconbutton.go @@ -1,125 +1,47 @@ package tins2020 import ( - "github.com/veandco/go-sdl2/sdl" -) + "image/color" -type HoverEffect int - -const ( - HoverEffectLigthen HoverEffect = iota - HoverEffectColor + "opslag.de/schobers/zntg" + "opslag.de/schobers/zntg/ui" ) type IconButton struct { - ControlBase + ui.Button - Icon string - IconDisabled string - IconHeight int32 - IconScale Scale - IconWidth int32 - - IconActive HoverEffect - IconHover HoverEffect - - Tooltip Tooltip - IsActive bool + Active bool } -func NewIconButton(icon string, onClick EventContextFn) *IconButton { - return &IconButton{ - ControlBase: ControlBase{ - OnLeftMouseButtonClick: onClick, +func NewIconButton(icon string, click ui.EventEmptyFn) *IconButton { + b := &IconButton{ + Button: ui.Button{ + Icon: icon, + IconHeight: 48, + Type: ui.ButtonTypeText, + HoverColor: hoverTransparentColor, }, - Icon: icon, } + b.Font.Color = color.White + b.ButtonClicked().AddHandler(func(ctx ui.Context, _ ui.ControlClickedArgs) { click(ctx) }) + return b } -func NewIconButtonConfigure(icon string, onClick EventContextFn, configure func(*IconButton)) *IconButton { - button := NewIconButton(icon, onClick) +func NewIconButtonConfigure(icon string, click ui.EventEmptyFn, configure func(*IconButton)) *IconButton { + button := NewIconButton(icon, click) 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 - } +var hoverTransparentColor = zntg.MustHexColor(`#FFFFFF1F`) - texture = ctx.Textures.Texture(b.Icon) - if len(b.IconDisabled) == 0 { - return texture - } - color, err := HexColor(b.IconDisabled) - if err == nil { - texture.SetColor(color) - } - return texture - } - return ctx.Textures.Texture(b.Icon) +func (b *IconButton) Render(ctx ui.Context) { + b.RenderActive(ctx) + b.Button.Render(ctx) } -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) { - iconTexture := b.activeTexture(ctx) - - hover := b.IsMouseOver && !b.IsDisabled - if (hover && b.IconHover == HoverEffectColor) || (b.IsActive && b.IconActive == HoverEffectColor) { - iconTexture.SetColor(MustHexColor("#15569F")) - } - - if b.IconScale == ScaleCenter { - size := iconTexture.Size() - if b.IconWidth != 0 { - size = Pt(b.IconWidth, b.IconWidth*size.Y/size.X) - } else if b.IconHeight != 0 { - size = Pt(b.IconHeight*size.X/size.Y, b.IconHeight) - } - iconTexture.CopyResize(ctx.Renderer, Rect(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 (hover && b.IconHover == HoverEffectLigthen) || (b.IsActive && b.IconActive == HoverEffectLigthen) { - SetDrawColor(ctx.Renderer, TransparentWhite) - ctx.Renderer.FillRect(b.Bounds.SDLPtr()) - } - iconTexture.SetColor(White) - - if len(b.Tooltip.Text) > 0 && b.IsMouseOver { - b.Tooltip.Render(ctx) +func (b *IconButton) RenderActive(ctx ui.Context) { + if b.Active && !b.Disabled && !b.IsOver() { + ctx.Renderer().FillRectangle(b.Bounds(), hoverTransparentColor) } } - -type Scale int - -const ( - ScaleCenter Scale = iota - ScaleStretch -) diff --git a/img/color.go b/img/color.go deleted file mode 100644 index 9013da9..0000000 --- a/img/color.go +++ /dev/null @@ -1,62 +0,0 @@ -package img - -import ( - "errors" - "image/color" - "regexp" -) - -var hexColorRE = regexp.MustCompile(`^#?([0-9A-Fa-f]{2})-?([0-9A-Fa-f]{2})-?([0-9A-Fa-f]{2})-?([0-9A-Fa-f]{2})?$`) - -func HexColor(s string) (color.RGBA, error) { - match := hexColorRE.FindStringSubmatch(s) - if match == nil { - return color.RGBA{}, errors.New("invalid color format") - } - values, err := HexToInts(match[1:]...) - if err != nil { - return color.RGBA{}, err - } - a := 255 - if len(match[4]) > 0 { - a = values[3] - } - return color.RGBA{R: uint8(values[0]), G: uint8(values[1]), B: uint8(values[2]), A: uint8(a)}, nil -} - -func MustHexColor(s string) color.RGBA { - color, err := HexColor(s) - if err != nil { - panic(err) - } - return color -} - -func HexToInt(s string) (int, error) { - var i int - for _, c := range s { - i *= 16 - if c >= '0' && c <= '9' { - i += int(c - '0') - } else if c >= 'A' && c <= 'F' { - i += int(c - 'A' + 10) - } else if c >= 'a' && c <= 'f' { - i += int(c - 'a' + 10) - } else { - return 0, errors.New("hex digit not supported") - } - } - return i, nil -} - -func HexToInts(s ...string) ([]int, error) { - ints := make([]int, len(s)) - for i, s := range s { - value, err := HexToInt(s) - if err != nil { - return nil, err - } - ints[i] = value - } - return ints, nil -} diff --git a/img/io.go b/img/io.go deleted file mode 100644 index b2ba033..0000000 --- a/img/io.go +++ /dev/null @@ -1,29 +0,0 @@ -package img - -import ( - "image" - "image/png" - "os" -) - -func EncodePNG(path string, im image.Image) error { - dst, err := os.Create(path) - if err != nil { - return err - } - defer dst.Close() - return png.Encode(dst, im) -} - -func DecodeImage(path string) (image.Image, error) { - f, err := os.Open(path) - if err != nil { - return nil, err - } - defer f.Close() - im, _, err := image.Decode(f) - if err != nil { - return nil, err - } - return im, nil -} diff --git a/intro.go b/intro.go index a4dadba..818a099 100644 --- a/intro.go +++ b/intro.go @@ -1,13 +1,19 @@ package tins2020 -type Intro struct { - LargeDialog +import ( + "image/color" - welcome Paragraph + "opslag.de/schobers/zntg/ui" +) + +type Intro struct { + ui.Paragraph } -func (i *Intro) Init(ctx *Context) error { - i.welcome.Text = +func NewIntro() *LargeDialog { + i := &Intro{} + i.Font.Color = color.White + i.Text = "Welcome to Botanim!\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 harvest flowers again to collect more money.\n\n" + @@ -21,7 +27,5 @@ func (i *Intro) Init(ctx *Context) error { " - W, A, S, D keys or CTRL + left mouse button or middle mouse button: pans landscape\n" + "\n" + "Have fun playing!" - i.SetContent(&i.welcome) - - return i.LargeDialog.Init(ctx) + return NewLargeDialog("Botanim", i) } diff --git a/io.go b/io.go index 388ab9c..314b81b 100644 --- a/io.go +++ b/io.go @@ -1,50 +1,11 @@ package tins2020 import ( - "encoding/json" - "os" - "path/filepath" + "opslag.de/schobers/zntg" ) -func DecodeJSON(path string, v interface{}) error { - f, err := os.Open(path) - if err != nil { - return err - } - defer f.Close() - err = json.NewDecoder(f).Decode(v) - if err != nil { - return err - } - return nil -} +const appName = "tins2020_botanim" -func EncodeJSON(path string, v interface{}) error { - f, err := os.Create(path) - if err != nil { - return err - } - defer f.Close() - return json.NewEncoder(f).Encode(v) -} +func UserDir() (string, error) { return zntg.UserDir(appName) } -func UserDir() (string, error) { - config, err := os.UserConfigDir() - if err != nil { - return "", err - } - dir := filepath.Join(config, "tins2020_botanim") - err = os.MkdirAll(dir, 0777) - if err != nil { - return "", err - } - return dir, nil -} - -func UserFile(name string) (string, error) { - dir, err := UserDir() - if err != nil { - return "", err - } - return filepath.Join(dir, name), nil -} +func UserFile(name string) (string, error) { return zntg.UserFile(appName, name) } diff --git a/label.go b/label.go deleted file mode 100644 index 4cc803c..0000000 --- a/label.go +++ /dev/null @@ -1,85 +0,0 @@ -package tins2020 - -import ( - "strings" -) - -type Label struct { - ControlBase - - Text string - Alignment TextAlignment -} - -func (l *Label) Render(ctx *Context) { - font := ctx.Fonts.Font(l.ActualFontName()) - color := l.ActualForeground() - bottom := l.Bounds.Y + l.Bounds.H - switch l.Alignment { - case TextAlignmentCenter: - font.RenderCopyAlign(ctx.Renderer, l.Text, Pt(l.Bounds.X+l.Bounds.W/2, bottom), color, TextAlignmentCenter) - case TextAlignmentLeft: - font.RenderCopyAlign(ctx.Renderer, l.Text, Pt(l.Bounds.X, bottom), color, TextAlignmentLeft) - case TextAlignmentRight: - font.RenderCopyAlign(ctx.Renderer, l.Text, Pt(l.Bounds.X+l.Bounds.W, bottom), color, TextAlignmentRight) - } -} - -type Paragraph struct { - Label -} - -func (p *Paragraph) Render(ctx *Context) { - font := ctx.Fonts.Font(p.ActualFontName()) - color := p.ActualForeground() - fontHeight := int32(font.Height()) - lines := strings.Split(p.Text, "\n") - - measure := func(s string) int32 { - w, _, _ := font.SizeUTF8(s) - return int32(w) - } - - spaces := func(s string) []int { - var spaces []int - offset := 0 - for { - space := strings.Index(s[offset:], " ") - if space == -1 { - return spaces - } - offset += space - spaces = append(spaces, offset) - offset++ - } - } - - fit := func(s string) string { - if measure(s) < p.Bounds.W { - return s - } - spaces := spaces(s) - for split := len(spaces) - 1; split >= 0; split-- { - clipped := s[:spaces[split]] - if measure(clipped) < p.Bounds.W { - return clipped - } - } - return s - } - - offset := p.Bounds.Y - for _, line := range lines { - if len(line) == 0 { - offset += fontHeight - continue - } - - for len(line) > 0 { - offset += fontHeight - clipped := fit(line) - line = strings.TrimLeft(line[len(clipped):], " ") - font.RenderCopy(ctx.Renderer, clipped, Pt(p.Bounds.X, offset), color) - } - } -} diff --git a/largedialog.go b/largedialog.go index 9230064..e88c976 100644 --- a/largedialog.go +++ b/largedialog.go @@ -1,108 +1,100 @@ package tins2020 -import "github.com/veandco/go-sdl2/sdl" +import ( + "image/color" -type DialogBase struct { - Container + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg" + "opslag.de/schobers/zntg/ui" +) - content Proxy - onShow *Events - close EventFn -} - -type Dialog interface { - CloseDialog() - OnShow() EventHandler - ShowDialog(*Context, EventFn) -} - -func (d *DialogBase) CloseDialog() { - close := d.close - if close != nil { - close() - } -} - -func (d *DialogBase) Init(ctx *Context) error { - d.AddChild(&d.content) - return d.Container.Init(ctx) -} - -func (d *DialogBase) OnShow() EventHandler { - if d.onShow == nil { - d.onShow = NewEvents() - } - return d.onShow -} - -func (d *DialogBase) SetContent(control Control) { - d.content.Proxied = control -} - -func (d *DialogBase) ShowDialog(ctx *Context, close EventFn) { - d.close = close - if d.onShow != nil { - d.onShow.Notify(ctx) - } -} +const titleBarHeight = 64 type LargeDialog struct { - DialogBase + ui.StackPanel - title Label - close IconButton + titleBar *LargeDialogTitleBar + content ui.Proxy + + closeRequested ui.Events } -func (d *LargeDialog) Arrange(ctx *Context, bounds Rectangle) { - const titleHeight = 64 - d.ControlBase.Arrange(ctx, bounds) - d.title.Arrange(ctx, Rect(bounds.X, bounds.Y, bounds.W, titleHeight)) - d.close.Arrange(ctx, Rect(bounds.W-64, 0, 64, 64)) - d.content.Arrange(ctx, Rect(bounds.X+titleHeight, 96, bounds.W-2*titleHeight, bounds.H-titleHeight)) +func NewLargeDialog(title string, content ui.Control) *LargeDialog { + dialog := &LargeDialog{} + + dialog.Orientation = ui.OrientationVertical + dialog.titleBar = NewLargeDialogTitleBar(title, func(ctx ui.Context, state interface{}) { + dialog.closeRequested.Notify(ctx, state) + }) + dialog.content.Content = ui.Margins(content, titleBarHeight, 20, titleBarHeight, 0) + dialog.Children = []ui.Control{dialog.titleBar, &dialog.content} + + return dialog } -func (d *LargeDialog) Init(ctx *Context) error { - d.title.Text = "Botanim" - d.title.FontName = "title" - d.title.Alignment = TextAlignmentCenter +func (d *LargeDialog) CloseRequested() ui.EventHandler { return &d.closeRequested } - d.close = IconButton{ - Icon: "control-cancel", - IconHover: HoverEffectColor, - IconWidth: 32, - } - d.close.OnLeftMouseButtonClick = EmptyEvent(d.CloseDialog) - d.AddChild(&d.title) - d.AddChild(&d.close) - return d.DialogBase.Init(ctx) -} - -func (d *LargeDialog) Handle(ctx *Context, event sdl.Event) bool { - if d.DialogBase.Handle(ctx, event) { +func (d *LargeDialog) Handle(ctx ui.Context, e ui.Event) bool { + if d.StackPanel.Handle(ctx, e) { return true } - switch e := event.(type) { - case *sdl.KeyboardEvent: - if e.Type == sdl.KEYDOWN { - switch e.Keysym.Sym { - case sdl.K_ESCAPE: - d.CloseDialog() - return true - case sdl.K_RETURN: - d.CloseDialog() - return true - } + switch e := e.(type) { + case *ui.KeyDownEvent: + switch e.Key { + case ui.KeyEscape: + d.closeRequested.Notify(ctx, nil) + return true + case ui.KeyEnter: + d.closeRequested.Notify(ctx, nil) + return true } } return false } -func (d *LargeDialog) Render(ctx *Context) { - SetDrawColor(ctx.Renderer, MustHexColor("#356DAD")) - ctx.Renderer.FillRect(d.Bounds.SDLPtr()) +func (d *LargeDialog) Hidden() { d.content.Hidden() } - d.DialogBase.Render(ctx) +func (d *LargeDialog) Render(ctx ui.Context) { + ctx.Renderer().Clear(zntg.MustHexColor("#356DAD")) + d.StackPanel.Render(ctx) } -func (d *LargeDialog) SetCaption(s string) { d.title.Text = s } +func (d *LargeDialog) Shown() { d.content.Shown() } + +type LargeDialogTitleBar struct { + ui.ContainerBase + + title ui.Label + close IconButton +} + +func NewLargeDialogTitleBar(title string, closeRequested ui.EventFn) *LargeDialogTitleBar { + titleBar := &LargeDialogTitleBar{} + titleBar.Children = []ui.Control{&titleBar.title, &titleBar.close} + titleBar.close.ButtonClicked().AddHandler(func(ctx ui.Context, args ui.ControlClickedArgs) { + closeRequested(ctx, args) + }) + titleBar.title.Font.Color = color.White + titleBar.title.Font.Name = "title" + titleBar.title.Text = title + titleBar.title.TextAlignment = ui.AlignCenter + + titleBar.close.Icon = "control-cancel" + titleBar.close.IconHeight = 32 + titleBar.close.Type = ui.ButtonTypeIcon + titleBar.close.HoverColor = zntg.MustHexColor(`#ABCAED`) + + return titleBar +} + +func (b *LargeDialogTitleBar) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) { + b.ControlBase.Arrange(ctx, bounds, offset, parent) + b.title.Arrange(ctx, bounds, offset, parent) + height := bounds.Dy() + b.close.Arrange(ctx, geom.RectRelF32(bounds.Max.X-height, bounds.Min.Y, height, height), offset, parent) +} + +func (b *LargeDialogTitleBar) DesiredSize(ctx ui.Context, size geom.PointF32) geom.PointF32 { + return geom.PtF32(size.X, titleBarHeight) +} diff --git a/map.go b/map.go index d585564..c63caed 100644 --- a/map.go +++ b/map.go @@ -1,5 +1,7 @@ package tins2020 +import "opslag.de/schobers/geom" + type Map struct { Temp NoiseMap Humid NoiseMap @@ -7,32 +9,32 @@ type Map struct { PlaceX NoiseMap // displacement map of props PlaceY NoiseMap - Center Point - Flowers map[Point]Flower + Center geom.Point + Flowers map[geom.Point]Flower } -func (m *Map) AddFlower(pos Point, id string, traits FlowerTraits) { +func (m *Map) AddFlower(pos geom.Point, id string, traits FlowerTraits) { m.Flowers[pos] = m.NewFlower(pos, id, traits) } -func (m *Map) FlowersOnAdjacentTiles(pos Point) int { +func (m *Map) FlowersOnAdjacentTiles(pos geom.Point) int { var count int - if _, ok := m.Flowers[Pt(pos.X+1, pos.Y)]; ok { + if _, ok := m.Flowers[geom.Pt(pos.X+1, pos.Y)]; ok { count++ } - if _, ok := m.Flowers[Pt(pos.X-1, pos.Y)]; ok { + if _, ok := m.Flowers[geom.Pt(pos.X-1, pos.Y)]; ok { count++ } - if _, ok := m.Flowers[Pt(pos.X, pos.Y+1)]; ok { + if _, ok := m.Flowers[geom.Pt(pos.X, pos.Y+1)]; ok { count++ } - if _, ok := m.Flowers[Pt(pos.X, pos.Y-1)]; ok { + if _, ok := m.Flowers[geom.Pt(pos.X, pos.Y-1)]; ok { count++ } return count } -func (m *Map) DigFlower(pos Point) string { +func (m *Map) DigFlower(pos geom.Point) string { flower, ok := m.Flowers[pos] if !ok { return "" @@ -41,12 +43,12 @@ func (m *Map) DigFlower(pos Point) string { return flower.ID } -func (m *Map) HasFlower(pos Point) bool { +func (m *Map) HasFlower(pos geom.Point) bool { _, ok := m.Flowers[pos] return ok } -func (m *Map) NewFlower(pos Point, id string, traits FlowerTraits) Flower { +func (m *Map) NewFlower(pos geom.Point, id string, traits FlowerTraits) Flower { flower := Flower{ ID: id, Traits: traits, diff --git a/noisemap.go b/noisemap.go index 603286a..d7cdc53 100644 --- a/noisemap.go +++ b/noisemap.go @@ -14,7 +14,7 @@ func clipNormalized(x float64) float64 { type NoiseMap interface { Seed() int64 - Value(x, y int32) float64 + Value(x, y int) float64 } func NewNoiseMap(seed int64) NoiseMap { @@ -33,7 +33,7 @@ type noiseMap struct { } // Value generates the noise value for an x/y pair. -func (m noiseMap) Value(x, y int32) float64 { +func (m noiseMap) Value(x, y int) float64 { value := m.noise.Noise2D(float64(x)*.01, float64(y)*.01, m.alpha, m.beta, m.harmonics)*.565 + .5 return clipNormalized(value) } @@ -49,7 +49,7 @@ type randomNoiseMap struct { } // Value generates the noise value for an x/y pair. -func (m randomNoiseMap) Value(x, y int32) float64 { +func (m randomNoiseMap) Value(x, y int) float64 { value := m.Noise2D(float64(x)*.53, float64(y)*.53, 1.01, 2, 2)*.5 + .5 return clipNormalized(value) } diff --git a/point.go b/point.go deleted file mode 100644 index 528a0bd..0000000 --- a/point.go +++ /dev/null @@ -1,42 +0,0 @@ -package tins2020 - -import "github.com/veandco/go-sdl2/sdl" - -type Point struct { - sdl.Point -} - -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 { - sdl.FPoint -} - -func (p PointF) Add(q PointF) PointF { - return PtF(p.X+q.X, p.Y+q.Y) -} - -func (p PointF) Mul(f float32) PointF { - return PtF(f*p.X, f*p.Y) -} - -func (p PointF) Sub(q PointF) PointF { return PtF(p.X-q.X, p.Y-q.Y) } - -func Pt(x, y int32) Point { return Point{sdl.Point{X: x, Y: y}} } -func PtF(x, y float32) PointF { return PointF{sdl.FPoint{X: x, Y: y}} } - -func PtPtr(x, y int32) *Point { - p := Pt(x, y) - return &p -} - -func PtFPtr(x, y float32) *PointF { - p := PtF(x, y) - return &p -} diff --git a/projection.go b/projection.go deleted file mode 100644 index 1b80773..0000000 --- a/projection.go +++ /dev/null @@ -1,130 +0,0 @@ -package tins2020 - -import ( - "log" - - "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 - zoomInv float32 - - windowInteractRect Rectangle - windowVisibleRect Rectangle - tileScreenDelta PointF - tileScreenDeltaInv PointF - tileScreenOffset Point - tileScreenSize Point - tileFitScreenSize Point - windowCenter Point -} - -func newProjection() projection { - return projection{zoom: 1, tileScreenDelta: PtF(64, 32), tileScreenDeltaInv: PtF(1./64, 1./32)} -} - -func (p *projection) mapToScreen(x, y int32) Point { - return p.mapToScreenF(float32(x), float32(y)) -} - -func (p *projection) mapToScreenF(x, y float32) Point { - translated := PtF(x-p.center.X, y-p.center.Y) - return Pt(p.windowCenter.X+int32((translated.X-translated.Y)*64*p.zoom), p.windowCenter.Y+int32((translated.X+translated.Y)*32*p.zoom)) -} - -func (p *projection) screenToMap(x, y int32) PointF { - pos := p.screenToMapRel(x-p.windowCenter.X, y-p.windowCenter.Y) - return p.center.Add(pos) -} - -func (p *projection) screenToMapInt(x, y int32) Point { - pos := p.screenToMap(x, y) - return mapToTile(pos) -} - -func (p *projection) screenToMapRel(x, y int32) PointF { - normX := p.zoomInv * float32(x) - normY := p.zoomInv * float32(y) - 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) Rectangle { - return Rect(pos.X-p.tileFitScreenSize.X, pos.Y-p.tileFitScreenSize.Y, 2*p.tileFitScreenSize.X, 2*p.tileFitScreenSize.Y) -} - -func (p *projection) screenToTileRect(pos Point) Rectangle { - return Rect(pos.X-p.tileScreenOffset.X, pos.Y-p.tileScreenOffset.Y, p.tileScreenSize.X, p.tileScreenSize.Y) -} - -func (p *projection) update(renderer *sdl.Renderer) { - p.zoomInv = 1 / p.zoom - - p.tileScreenOffset = Pt(int32(p.zoom*64), int32(p.zoom*112)) - p.tileScreenSize = Pt(int32(p.zoom*128), int32(p.zoom*160)) - p.tileFitScreenSize = Pt(int32(p.zoom*64), int32(p.zoom*32)) - - windowW, windowH, err := renderer.GetOutputSize() - if err != nil { - log.Fatal(err) - } - p.windowCenter = Pt(windowW/2, windowH/2) - p.windowInteractRect = RectAbs(buttonBarWidth, 64, windowW-buttonBarWidth, windowH) - p.windowVisibleRect = RectAbs(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)) { - visible := p.windowVisibleRect - topLeft := p.screenToMap(visible.X, visible.Y) - topRight := p.screenToMap(visible.X+visible.W, visible.Y) - 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)) - minX, maxX := int32(Floor32(topLeft.X)), int32(Ceil32(bottomRight.X)) - for y := minY; y <= maxY; y++ { - for x := minX; x <= maxX; x++ { - pos := p.mapToScreen(x, y) - rectFit := p.screenToTileFitRect(pos) - if rectFit.X+rectFit.W < visible.X || rectFit.Y+rectFit.H < visible.Y { - continue - } - if rectFit.X > visible.X+visible.W || rectFit.Y > visible.Y+visible.H { - break - } - action(x, y, pos) - } - } -} - -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) { - if p.zoom <= .25 { - return - } - p.SetZoom(ctx, center, .5*p.zoom) -} - -func (p *projection) ZoomIn(ctx *Context, center PointF) { - if p.zoom >= 2 { - return - } - p.SetZoom(ctx, center, 2*p.zoom) -} diff --git a/proxy.go b/proxy.go deleted file mode 100644 index 570aa6b..0000000 --- a/proxy.go +++ /dev/null @@ -1,48 +0,0 @@ -package tins2020 - -import "github.com/veandco/go-sdl2/sdl" - -var _ Control = &Proxy{} - -type Proxy struct { - Proxied Control - - bounds Rectangle -} - -func (p *Proxy) Arrange(ctx *Context, bounds Rectangle) { - p.bounds = bounds - if p.Proxied == nil { - return - } - p.Proxied.Arrange(ctx, bounds) -} - -func (p *Proxy) Handle(ctx *Context, event sdl.Event) bool { - if p.Proxied == nil { - return false - } - return p.Proxied.Handle(ctx, event) -} - -func (p *Proxy) Init(ctx *Context) error { - if p.Proxied == nil { - return nil - } - return p.Proxied.Init(ctx) -} - -func (p *Proxy) Render(ctx *Context) { - if p.Proxied == nil { - return - } - p.Proxied.Render(ctx) -} - -func (p *Proxy) SetContent(ctx *Context, content Control) { - p.Proxied = content - if content == nil { - return - } - content.Arrange(ctx, p.bounds) -} diff --git a/rect.go b/rect.go deleted file mode 100644 index 27bd0d4..0000000 --- a/rect.go +++ /dev/null @@ -1,32 +0,0 @@ -package tins2020 - -import "github.com/veandco/go-sdl2/sdl" - -type Rectangle struct { - sdl.Rect -} - -func RectAbs(x1, y1, x2, y2 int32) Rectangle { - if x1 > x2 { - x1, x2 = x2, x1 - } - if y1 > y2 { - y1, y2 = y2, y1 - } - return Rectangle{sdl.Rect{X: x1, Y: y1, W: x2 - x1, H: y2 - y1}} -} - -func Rect(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) SDLPtr() *sdl.Rect { return &r.Rect } diff --git a/research.go b/research.go index 9d0dd01..eb5ef93 100644 --- a/research.go +++ b/research.go @@ -2,205 +2,76 @@ package tins2020 import ( "fmt" - "math" "math/rand" "strconv" "strings" "time" - "github.com/veandco/go-sdl2/sdl" + "opslag.de/schobers/geom" + + "opslag.de/schobers/zntg" + "opslag.de/schobers/zntg/ui" ) type Research struct { - Container + ui.StackPanel game *Game botanist Specialist farmer Specialist - typing string - digitCount int + description ui.Paragraph + specialists ui.Paragraph + dial *Dial + input ui.Label - close func() - description Paragraph - specialists Paragraph - input Label - digits []Digit - animate Animation + animate zntg.Animation + closeRequested ui.Events } -func NewResearch(game *Game) Control { - research := &Research{ - game: game, - animate: NewAnimation(20 * time.Millisecond), - } - dialog := &LargeDialog{} - dialog.SetCaption("Research") - dialog.SetContent(research) - dialog.OnShow().RegisterItf(func(state interface{}) { - research.onShow(state.(*Context)) - }) - research.close = func() { dialog.CloseDialog() } +type Dialer interface { + CanUserType(int) bool + UserGaveWrongInput() + UserTyped(ui.Context, int) +} + +func NewResearch(game *Game) *LargeDialog { + research := &Research{game: game} + research.animate.Interval = 20 * time.Millisecond + research.animate.Start() + + research.description.Text = "Call a specialist to conduct research with." + research.dial = NewDial(research) + research.Children = []ui.Control{&research.description, &research.specialists, research.dial, &research.input} + + dialog := NewLargeDialog("Research", ui.Stretch(research)) + research.closeRequested.AddHandlerEmpty(func(ctx ui.Context) { dialog.closeRequested.Notify(ctx, nil) }) return dialog } -type Digit struct { - ControlBase - - Value string - - highlight int -} - -func (d *Digit) Blink() { - d.highlight = 4 -} - -func (d *Digit) Render(ctx *Context) { - font := ctx.Fonts.Font("title") - color := White - if d.highlight > 0 { - color = MustHexColor("#15569F") - } - font.RenderCopyAlign(ctx.Renderer, d.Value, Pt(d.Bounds.X+d.Bounds.W/2, d.Bounds.Y+int32(font.Height())), color, TextAlignmentCenter) -} - -func (d *Digit) Tick() { - if d.highlight > 0 { - d.highlight-- - } -} - type Specialist struct { Cost int Number string } -func (r *Research) Init(ctx *Context) error { - r.AddChild(&r.description) - r.AddChild(&r.specialists) - r.AddChild(&r.input) - - r.description.Text = "Call a specialist to conduct research with." - r.digits = make([]Digit, 10) - for i := range r.digits { - j := i - r.digits[i].Value = strconv.Itoa(i) - r.digits[i].OnLeftMouseButtonClick = func(*Context) { - r.userTyped(j) - } - r.AddChild(&r.digits[i]) - } - - return nil +func (r *Research) Arrange(ctx ui.Context, bounds geom.RectangleF32, offset geom.PointF32, parent ui.Control) { + r.input.TextAlignment = ui.AlignCenter + r.StackPanel.Arrange(ctx, bounds, offset, parent) } -func (r *Research) Arrange(ctx *Context, bounds Rectangle) { - r.Container.Arrange(ctx, bounds) - r.specialists.Arrange(ctx, Rect(r.Bounds.X, r.Bounds.Y+40, r.Bounds.W, r.Bounds.H-40)) - r.input.Arrange(ctx, Rect(r.Bounds.X, r.Bounds.X+r.Bounds.H-48, r.Bounds.W, 24)) - r.input.Alignment = TextAlignmentCenter - - center := Pt(r.Bounds.X+r.Bounds.W/2, r.Bounds.Y+r.Bounds.H/2) - - distance := float64(bounds.H) * .3 - for i := range r.digits { - angle := (float64((10-i)%10)*0.16 + .2) * math.Pi - pos := Pt(int32(distance*math.Cos(angle)), int32(.8*distance*math.Sin(angle))) - digitCenter := center.Add(pos) - r.digits[i].Arrange(ctx, Rect(digitCenter.X-24, digitCenter.Y-24, 48, 48)) - } +func (r *Research) CanUserType(digit int) bool { + typing := strconv.Itoa(digit) + return strings.HasPrefix(r.botanist.Number, r.input.Text+typing) } -func (r *Research) userTyped(i int) { - r.digits[i].Blink() - digit := strconv.Itoa(i) - if len(r.typing) == 0 || digit != r.typing { - r.typing = digit - r.digitCount = 1 - } else { - r.digitCount++ - } +func (r *Research) Hidden() {} - if !strings.HasPrefix(r.botanist.Number, r.input.Text+r.typing) { - r.input.Text = "" - r.typing = "" - r.digitCount = 0 - } else if r.digitCount == i || r.digitCount == 10 { - r.input.Text += digit - r.typing = "" - r.digitCount = 0 - - if r.input.Text == r.botanist.Number { - r.game.UnlockNextFlower() - r.close() - r.input.Text = "" - } - } +func (r *Research) Render(ctx ui.Context) { + r.animate.AnimateFn(r.dial.Tick) + r.StackPanel.Render(ctx) } -func (r *Research) Handle(ctx *Context, event sdl.Event) bool { - if r.Container.Handle(ctx, event) { - return true - } - switch e := event.(type) { - case *sdl.KeyboardEvent: - if e.Type == sdl.KEYDOWN { - switch e.Keysym.Sym { - case sdl.K_0: - r.userTyped(0) - case sdl.K_KP_0: - r.userTyped(0) - case sdl.K_1: - r.userTyped(1) - case sdl.K_KP_1: - r.userTyped(1) - case sdl.K_2: - r.userTyped(2) - case sdl.K_KP_2: - r.userTyped(2) - case sdl.K_3: - r.userTyped(3) - case sdl.K_KP_3: - r.userTyped(3) - case sdl.K_4: - r.userTyped(4) - case sdl.K_KP_4: - r.userTyped(4) - case sdl.K_5: - r.userTyped(5) - case sdl.K_KP_5: - r.userTyped(5) - case sdl.K_6: - r.userTyped(6) - case sdl.K_KP_6: - r.userTyped(6) - case sdl.K_7: - r.userTyped(7) - case sdl.K_KP_7: - r.userTyped(7) - case sdl.K_8: - r.userTyped(8) - case sdl.K_KP_8: - r.userTyped(8) - case sdl.K_9: - r.userTyped(9) - case sdl.K_KP_9: - r.userTyped(9) - } - } - } - return false -} - -func (r *Research) Render(ctx *Context) { - for i := range r.digits { - r.digits[i].Tick() - } - r.Container.Render(ctx) -} - -func (r *Research) onShow(ctx *Context) { +func (r *Research) Shown() { generateNumber := func() string { var number string for i := 0; i < 3; i++ { @@ -208,8 +79,9 @@ func (r *Research) onShow(ctx *Context) { } return number } - r.digitCount = 0 + r.input.Text = "" + r.dial.Reset() var specialists string defer func() { @@ -233,3 +105,16 @@ func (r *Research) onShow(ctx *Context) { specialists += fmt.Sprintf("Botanist: no. %s (unlocks next flower; $ %d)\n", r.botanist.Number, r.botanist.Cost) specialists += "Farmer: no. **unavailable** (fertilizes land; $ ---)\n" } + +func (r *Research) UserGaveWrongInput() { + r.input.Text = "" +} + +func (r *Research) UserTyped(ctx ui.Context, digit int) { + r.input.Text += strconv.Itoa(digit) + if r.input.Text == r.botanist.Number { + r.game.UnlockNextFlower(ctx) + r.input.Text = "" + r.closeRequested.Notify(ctx, nil) + } +} diff --git a/resourceloader.go b/resourceloader.go index 58048d4..4b4fa12 100644 --- a/resourceloader.go +++ b/resourceloader.go @@ -4,6 +4,8 @@ import ( "bufio" "fmt" "strings" + + "opslag.de/schobers/zntg/ui" ) type ResourceLoader struct { @@ -14,8 +16,8 @@ func NewResourceLoader() *ResourceLoader { return &ResourceLoader{} } -func (l *ResourceLoader) parseResourcesFile(res *Resources, name string) error { - f, err := res.Fs().Open(name) +func (l *ResourceLoader) parseResourcesFile(res ui.Resources, name string) error { + f, err := res.OpenResource(name) if err != nil { return err } @@ -36,7 +38,7 @@ func (l *ResourceLoader) parseResourcesFile(res *Resources, name string) error { return nil } -func (l *ResourceLoader) LoadFromFile(res *Resources, name string, action func(string, string) error) error { +func (l *ResourceLoader) LoadFromFile(res ui.Resources, name string, action func(string, string) error) error { err := l.parseResourcesFile(res, name) if err != nil { return err diff --git a/settings.go b/settings.go index 5223811..2c638f2 100644 --- a/settings.go +++ b/settings.go @@ -1,6 +1,11 @@ package tins2020 -import "os" +import ( + "os" + + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg" +) type Settings struct { Window WindowSettings @@ -18,7 +23,7 @@ func (s *Settings) Init() error { if _, err := os.Stat(path); os.IsNotExist(err) { return nil } - return DecodeJSON(path, s) + return zntg.DecodeJSON(path, s) } func (s *Settings) Store() error { @@ -26,11 +31,11 @@ func (s *Settings) Store() error { if err != nil { return err } - return EncodeJSON(path, s) + return zntg.EncodeJSON(path, s) } type WindowSettings struct { - Location *Point - Size *Point + Location *geom.Point + Size *geom.Point VSync *bool } diff --git a/terrainrenderer.go b/terrainrenderer.go index 860c946..82e7039 100644 --- a/terrainrenderer.go +++ b/terrainrenderer.go @@ -3,129 +3,127 @@ package tins2020 import ( "fmt" - "github.com/veandco/go-sdl2/sdl" + "opslag.de/schobers/zntg/play" + + "opslag.de/schobers/geom" + "opslag.de/schobers/zntg/ui" ) type terrainRenderer struct { - game *Game - hover *Point - project projection + ui.ControlBase - drag Drageable + game *Game + hover *geom.Point + viewBounds geom.RectangleF32 + interactBounds geom.RectangleF32 + isometric *play.IsometricProjection + + drag ui.Dragable } -func NewTerrainRenderer(game *Game) Control { - return &terrainRenderer{game: game, project: newProjection()} -} +func NewTerrainRenderer(game *Game) ui.Control { + renderer := &terrainRenderer{game: game, isometric: play.NewIsometricProjection(geom.PtF32(128, 64), geom.RectF32(0, 0, 100, 100))} -func (r *terrainRenderer) Arrange(ctx *Context, _ Rectangle) { - r.project.update(ctx.Renderer) -} - -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) + renderer.game.CenterChanged().AddHandler(func(ctx ui.Context, state interface{}) { + center := state.(geom.Point) + renderer.isometric.MoveCenterTo(center.ToF32()) }) - r.project.update(ctx.Renderer) - return nil + return renderer } -func isControlKeyDown() bool { - state := sdl.GetKeyboardState() - return state[sdl.SCANCODE_LCTRL] == 1 || state[sdl.SCANCODE_RCTRL] == 1 || state[sdl.SCANCODE_LGUI] == 1 || state[sdl.SCANCODE_RGUI] == 1 +func (r *terrainRenderer) Arrange(ctx ui.Context, bounds geom.RectangleF32, _ geom.PointF32, _ ui.Control) { + r.viewBounds = geom.RectF32(buttonBarWidth, 0, bounds.Dx()-buttonBarWidth, bounds.Dy()) + r.isometric.SetViewBounds(r.viewBounds) + r.interactBounds = r.viewBounds + r.interactBounds.Min.Y += 64 } -func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) bool { +func isControlKeyDown(ctx ui.Context) bool { + modifiers := ctx.KeyModifiers() + return modifiers&(ui.KeyModifierControl|ui.KeyModifierOSCommand) != 0 +} + +func (r *terrainRenderer) Handle(ctx ui.Context, event ui.Event) bool { switch e := event.(type) { - case *sdl.MouseButtonEvent: - if r.project.windowInteractRect.IsPointInside(e.X, e.Y) { - if e.Type == sdl.MOUSEBUTTONDOWN { - controlKeyDown := isControlKeyDown() - if e.Button == sdl.BUTTON_MIDDLE || (e.Button == sdl.BUTTON_LEFT && controlKeyDown) { - if !r.drag.IsDragging() { - r.drag.Start(Pt(e.X, e.Y)) - } - } - if e.Button == sdl.BUTTON_LEFT && !controlKeyDown { - pos := r.project.screenToMapInt(e.X, e.Y) - r.game.UserClickedTile(pos) - } - if e.Button == sdl.BUTTON_RIGHT { - if e.Type == sdl.MOUSEBUTTONDOWN { - r.game.CancelTool() - } + case *ui.MouseButtonDownEvent: + pos := e.Pos() + if pos.In(r.interactBounds) { + controlKeyDown := isControlKeyDown(ctx) + if e.Button == ui.MouseButtonMiddle || (e.Button == ui.MouseButtonLeft && controlKeyDown) { + if _, ok := r.drag.IsDragging(); !ok { + r.drag.Start(pos) } } - if e.Type == sdl.MOUSEBUTTONUP { - if r.drag.IsDragging() { - r.game.Terrain.Center = mapToTile(r.project.center) - r.drag.Cancel() - } + if e.Button == ui.MouseButtonLeft && !controlKeyDown { + pos := r.isometric.ViewToTileInt(pos) + r.game.UserClickedTile(pos) + } + if e.Button == ui.MouseButtonRight { + r.game.CancelTool(ctx) } } - case *sdl.MouseMotionEvent: - if r.project.windowInteractRect.IsPointInside(e.X, e.Y) { - hover := r.project.screenToMapInt(e.X, e.Y) + case *ui.MouseButtonUpEvent: + pos := e.Pos() + if pos.In(r.interactBounds) { + if _, ok := r.drag.IsDragging(); ok { + r.game.Terrain.Center = r.isometric.TileInt(r.isometric.Center()) + r.drag.Cancel() + } + } + case *ui.MouseMoveEvent: + pos := e.Pos() + if pos.In(r.interactBounds) { + hover := r.isometric.ViewToTileInt(pos) r.hover = &hover } else { r.hover = nil } - 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) + if _, ok := r.drag.IsDragging(); ok { + delta, _ := r.drag.Move(pos) + r.isometric.Pan(r.isometric.ViewToTileRelative(delta.Invert())) } - case *sdl.MouseWheelEvent: if r.hover != nil { - if e.Y < 0 { - r.project.ZoomOut(ctx, r.hover.ToPtF()) - } else { - r.project.ZoomIn(ctx, r.hover.ToPtF()) + if e.MouseWheel < 0 { + r.isometric.ZoomOut(r.hover.ToF32()) + } else if e.MouseWheel > 0 { + r.isometric.ZoomIn(r.hover.ToF32()) } } - case *sdl.WindowEvent: - if e.Event == sdl.WINDOWEVENT_LEAVE { - r.hover = nil - r.project.update(ctx.Renderer) - } - case *sdl.KeyboardEvent: - if e.Type == sdl.KEYDOWN { - switch e.Keysym.Sym { - case sdl.K_PLUS: - r.project.ZoomIn(ctx, r.project.center) - case sdl.K_KP_PLUS: - r.project.ZoomIn(ctx, r.project.center) - case sdl.K_MINUS: - r.project.ZoomOut(ctx, r.project.center) - case sdl.K_KP_MINUS: - 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)) - } + case *ui.MouseLeaveEvent: + r.hover = nil + case *ui.KeyDownEvent: + switch e.Key { + case ui.KeyPadPlus: + r.isometric.ZoomIn(r.isometric.Center()) + case ui.KeyMinus: + r.isometric.ZoomOut(r.isometric.Center()) + case ui.KeyPadMinus: + r.isometric.ZoomOut(r.isometric.Center()) + case ui.KeyW: + r.isometric.Pan(geom.PtF32(-1, -1)) + case ui.KeyA: + r.isometric.Pan(geom.PtF32(-1, 1)) + case ui.KeyS: + r.isometric.Pan(geom.PtF32(1, 1)) + case ui.KeyD: + r.isometric.Pan(geom.PtF32(1, -1)) } } return false } -func (r *terrainRenderer) Render(ctx *Context) { +func (r *terrainRenderer) Render(ctx ui.Context) { + zoom := r.isometric.Zoom() terrain := r.game.Terrain - toTileTexture := func(x, y int32) *Texture { - temp := terrain.Temp.Value(x, y) + toTileTexture := func(tile geom.Point) ui.Texture { + temp := terrain.Temp.Value(tile.X, tile.Y) if temp < .35 { - return ctx.Textures.Texture("tile-snow") + return ctx.Textures().ScaledByName("tile-snow", zoom) } if temp > .65 { - return ctx.Textures.Texture("tile-dirt") + return ctx.Textures().ScaledByName("tile-dirt", zoom) } - return ctx.Textures.Texture("tile-grass") + return ctx.Textures().ScaledByName("tile-grass", zoom) } variantToInt := func(variant float64) int { @@ -144,14 +142,13 @@ func (r *terrainRenderer) Render(ctx *Context) { return -1 } - variantToTexture := func(format string, variant float64) *Texture { + variantToTexture := func(format string, variant float64) ui.Texture { textName := fmt.Sprintf(format, variantToInt(variant)) - return ctx.Textures.Texture(textName) + return ctx.Textures().ScaledByName(textName, zoom) } - stretch := func(x, from, to float64) float64 { return (x - from) * 1 / (to - from) } - toPropTexture := func(temp, humid, variant float64) *Texture { + toPropTexture := func(temp, humid, variant float64) ui.Texture { if temp < .35 { if humid < .2 { return nil @@ -181,12 +178,12 @@ func (r *terrainRenderer) Render(ctx *Context) { return variantToTexture("bush-large-%d", stretch(variant, .8, 1)*multiplier) } - toItemTexture := func(x, y int32) *Texture { + toItemTexture := func(x, y int) ui.Texture { variant := terrain.Variant.Value(x, y) - flower, ok := terrain.Flowers[Pt(x, y)] + flower, ok := terrain.Flowers[geom.Pt(x, y)] if ok { desc, _ := r.game.Herbarium.Find(flower.ID) - return ctx.Textures.Texture(desc.IconTemplate.Variant(variantToInt(variant))) + return ctx.Textures().ScaledByName(desc.IconTemplate.Variant(variantToInt(variant)), zoom) } temp := terrain.Temp.Value(x, y) humid := terrain.Humid.Value(x, y) @@ -197,24 +194,35 @@ func (r *terrainRenderer) Render(ctx *Context) { // vertical (tile): [96,160) = 64 // vertical (total): [0,160) = 160 - r.project.visibleTiles(func(x, y int32, pos Point) { - text := toTileTexture(x, y) - rect := r.project.screenToTileRect(pos) - text.CopyResize(ctx.Renderer, rect) + topLeft := geom.PtF32(-64*zoom, -112*zoom) + bottomRight := geom.PtF32(64*zoom, 48*zoom) + textureRect := func(center geom.PointF32) geom.RectangleF32 { + return geom.RectangleF32{Min: center.Add(topLeft), Max: center.Add(bottomRight)} + } + hoverTexture := ctx.Textures().ScaledByName("tile-hover", zoom) - if r.hover != nil && x == r.hover.X && y == r.hover.Y { - ctx.Textures.Texture("tile-hover").CopyResize(ctx.Renderer, rect) + r.isometric.EnumerateInt(func(tile geom.Point, view geom.PointF32) { + text := toTileTexture(tile) + rect := textureRect(view) + ctx.Renderer().DrawTexture(text, rect) + // if r.game.Debug { + // ctx.Renderer().FillRectangle(view.Add2D(-1, -1).RectRel2D(2, 2), color.White) + // ctx.Fonts().TextAlign("debug", view, color.White, fmt.Sprintf("%d, %d", tile.X, tile.Y), ui.AlignCenter) + // } + if r.hover != nil && tile.X == r.hover.X && tile.Y == r.hover.Y { + ctx.Renderer().DrawTexture(hoverTexture, rect) } }) - r.project.visibleTiles(func(x, y int32, pos Point) { - text := toItemTexture(x, y) + r.isometric.EnumerateInt(func(tile geom.Point, view geom.PointF32) { + text := toItemTexture(tile.X, tile.Y) if text == nil { return } - 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)) + placeX, placeY := terrain.PlaceX.Value(tile.X, tile.Y), terrain.PlaceY.Value(tile.X, tile.Y) + displaced := r.isometric.TileToView(tile.ToF32().Add2D(-.2+.9*float32(placeX)-.45, -.2+.9*float32(placeY)-.45)) + rect := textureRect(displaced) + ctx.Renderer().DrawTexture(text, rect) }) } diff --git a/textures.go b/textures.go deleted file mode 100644 index 83bf6f9..0000000 --- a/textures.go +++ /dev/null @@ -1,116 +0,0 @@ -package tins2020 - -import ( - "errors" - "fmt" - - "github.com/veandco/go-sdl2/img" - "github.com/veandco/go-sdl2/sdl" - "opslag.de/schobers/fs/vfs" -) - -type Texture struct { - texture *sdl.Texture - size Point -} - -func NewTextureFromSurface(renderer *sdl.Renderer, surface *sdl.Surface) (*Texture, error) { - texture, err := renderer.CreateTextureFromSurface(surface) - if err != nil { - return nil, err - } - return &Texture{texture: texture, size: Pt(surface.W, surface.H)}, nil -} - -func (t *Texture) Size() Point { return t.size } - -// func (t *Texture) Rect() Rectangle { return t.rect } - -// func (t *Texture) SDLRectPtr() *sdl.Rect { return t.rect.SDLPtr() } - -func (t *Texture) Copy(renderer *sdl.Renderer, dst Point) { - t.CopyResize(renderer, Rect(dst.X, dst.Y, t.size.X, t.size.Y)) -} - -func (t *Texture) CopyPart(renderer *sdl.Renderer, src Rectangle, dst Point) { - t.CopyPartResize(renderer, src, Rect(dst.X, dst.Y, src.W, src.H)) -} - -func (t *Texture) CopyPartResize(renderer *sdl.Renderer, src Rectangle, dst Rectangle) { - renderer.Copy(t.texture, src.SDLPtr(), dst.SDLPtr()) -} - -func (t *Texture) CopyResize(renderer *sdl.Renderer, dst Rectangle) { - t.CopyPartResize(renderer, RectAbs(0, 0, t.size.X, t.size.Y), dst) -} - -func (t *Texture) SetColor(color sdl.Color) { - t.texture.SetColorMod(color.R, color.G, color.B) -} - -// 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() } - -type Textures struct { - dir vfs.CopyDir - renderer *sdl.Renderer - textures map[string]*Texture -} - -func (t *Textures) Init(renderer *sdl.Renderer, dir vfs.CopyDir) { - t.dir = dir - t.renderer = renderer - t.textures = map[string]*Texture{} -} - -func (t *Textures) Load(name, path string, other ...string) error { - err := t.load(name, path) - if err != nil { - return err - } - if len(other) == 0 { - return nil - } - if len(other)%2 != 0 { - return errors.New("expected name/path pairs") - } - for i := 0; i < len(other); i += 2 { - err = t.load(other[i], other[i+1]) - if err != nil { - return fmt.Errorf("error loading '%s'; error: %v", other[i], err) - } - } - return nil -} - -func (t *Textures) load(name, path string) error { - texturePath, err := t.dir.Retrieve(path) - if err != nil { - return err - } - surface, err := img.Load(texturePath) - if err != nil { - return err - } - defer surface.Free() - texture, err := NewTextureFromSurface(t.renderer, surface) - if err != nil { - return err - } - t.textures[name] = texture - return nil -} - -func (t *Textures) Texture(name string) *Texture { return t.textures[name] } - -func (t *Textures) Destroy() { - if t.textures == nil { - return - } - for _, t := range t.textures { - t.Destroy() - } -} diff --git a/tools.go b/tools.go index a532c3a..d8b8f5a 100644 --- a/tools.go +++ b/tools.go @@ -1,8 +1,10 @@ package tins2020 +import "opslag.de/schobers/geom" + type Tool interface { Type() string - ClickedTile(*Game, Point) + ClickedTile(*Game, geom.Point) } type PlantFlowerTool struct { @@ -11,7 +13,7 @@ type PlantFlowerTool struct { func (t *PlantFlowerTool) Type() string { return "plant-flower" } -func (t *PlantFlowerTool) ClickedTile(game *Game, tile Point) { +func (t *PlantFlowerTool) ClickedTile(game *Game, tile geom.Point) { game.PlantFlower(t.FlowerID, tile) } @@ -19,6 +21,6 @@ type ShovelTool struct{} func (t *ShovelTool) Type() string { return "shovel" } -func (t *ShovelTool) ClickedTile(game *Game, tile Point) { +func (t *ShovelTool) ClickedTile(game *Game, tile geom.Point) { game.Dig(tile) } diff --git a/tooltip.go b/tooltip.go deleted file mode 100644 index e4a90f9..0000000 --- a/tooltip.go +++ /dev/null @@ -1,72 +0,0 @@ -package tins2020 - -import "github.com/veandco/go-sdl2/sdl" - -type Tooltip struct { - ControlBase - - Text string -} - -const tooltipBorderThickness = 1 -const tooltipHorizontalPadding = 6 -const tooltipVerticalPadding = 2 -const tooltipMouseDistance = 12 - -func (t *Tooltip) ActualFontName() string { - if t.FontName == "" { - return "small" - } - return t.FontName -} - -func (t *Tooltip) Handle(ctx *Context, event sdl.Event) bool { - if len(t.Text) == 0 { - return false - } - - font := ctx.Fonts.Font(t.ActualFontName()) - if font == nil { - font = ctx.Fonts.Font("default") - } - 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 + 2*tooltipVerticalPadding - - 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) { - almostBlack := MustHexColor("#0000007f") - SetDrawColor(ctx.Renderer, almostBlack) - ctx.Renderer.FillRect(t.Bounds.SDLPtr()) - - almostWhite := MustHexColor("ffffff7f") - SetDrawColor(ctx.Renderer, almostWhite) - ctx.Renderer.DrawRect(t.Bounds.SDLPtr()) - - font := ctx.Fonts.Font(t.ActualFontName()) - - bottomLeft := Pt(t.Bounds.X+tooltipBorderThickness+tooltipHorizontalPadding, t.Bounds.Y+t.Bounds.H-tooltipBorderThickness-tooltipVerticalPadding) - font.RenderCopy(ctx.Renderer, t.Text, bottomLeft, White) -}