diff --git a/animation.go b/animation.go new file mode 100644 index 0000000..4afd21b --- /dev/null +++ b/animation.go @@ -0,0 +1,56 @@ +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() +} + +func (a *Animation) SetInterval(interval time.Duration) { + a.interval = interval +} diff --git a/buttonbar.go b/buttonbar.go new file mode 100644 index 0000000..3fe82ee --- /dev/null +++ b/buttonbar.go @@ -0,0 +1,57 @@ +package tins2020 + +import "github.com/veandco/go-sdl2/sdl" + +type ButtonBar struct { + Container + + Background sdl.Color + 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) + switch b.Orientation { + case OrientationHorizontal: + length := bounds.H + offset := bounds.X + for i := range b.Buttons { + b.Buttons[i].Arrange(ctx, RectSize(offset, bounds.Y, length, length)) + offset += length + } + default: + length := bounds.W + offset := bounds.Y + for i := range b.Buttons { + b.Buttons[i].Arrange(ctx, RectSize(bounds.X, offset, length, length)) + offset += length + } + } +} + +func (b *ButtonBar) Handle(ctx *Context, event sdl.Event) { + b.Container.Handle(ctx, event) +} + +func (b *ButtonBar) Render(ctx *Context) { + ctx.Renderer.SetDrawColor(b.Background.R, b.Background.G, b.Background.B, b.Background.A) + 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 new file mode 100644 index 0000000..282e2b2 --- /dev/null +++ b/buyflowerbutton.go @@ -0,0 +1,95 @@ +package tins2020 + +import ( + "fmt" + "time" + + "github.com/veandco/go-sdl2/sdl" +) + +type BuyFlowerButton struct { + IconButton + + Name string + Price int + Description string + + hoverAnimation *Animation + hoverOffset int32 + hoverTexture *Texture + priceTexture *Texture +} + +func NewBuyFlowerButton(icon, iconDisabled, name string, price int, description string, isDisabled bool, onClick EventFn) *BuyFlowerButton { + return &BuyFlowerButton{ + IconButton: *NewIconButtonConfig(icon, onClick, func(b *IconButton) { + b.IconDisabled = iconDisabled + b.IsDisabled = isDisabled + }), + Name: name, + Price: price, + Description: description, + } +} + +func (b *BuyFlowerButton) animate() { + b.hoverOffset++ + if b.hoverOffset > b.hoverTexture.Size().X+b.Bounds.W { + b.hoverOffset = b.priceTexture.Size().X + } +} + +func (b *BuyFlowerButton) Init(ctx *Context) error { + text := fmt.Sprintf("%s - %s - %s", FmtMoney(b.Price), b.Name, b.Description) + font := ctx.Fonts.Font("small") + color := MustHexColor("#ffffff") + texture, err := font.Render(ctx.Renderer, text, color) + if err != nil { + return err + } + b.hoverTexture = texture + texture, err = font.Render(ctx.Renderer, FmtMoney(b.Price), color) + if err != nil { + return err + } + b.priceTexture = texture + return nil +} + +func (b *BuyFlowerButton) Handle(ctx *Context, event sdl.Event) { + b.IconButton.Handle(ctx, event) + 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) Render(ctx *Context) { + iconTexture := b.activeTexture(ctx) + mouseOverTexture := ctx.Textures.Texture("control-hover") + + pos := Pt(b.Bounds.X, b.Bounds.Y) + iconTexture.CopyResize(ctx.Renderer, RectSize(pos.X, pos.Y-40, buttonBarWidth, 120)) + if b.IsMouseOver && !b.IsDisabled { + mouseOverTexture.Copy(ctx.Renderer, pos) + } + + if b.hoverAnimation != nil { + b.hoverAnimation.AnimateFn(b.animate) + } + + if b.IsMouseOver { + left := buttonBarWidth - 8 - b.hoverOffset + top := pos.Y + buttonBarWidth - 20 + if left < 0 { + part := Rect(-left, 0, b.hoverTexture.Size().X, b.hoverTexture.Size().Y) + b.hoverTexture.CopyPart(ctx.Renderer, part, Pt(pos.X, top)) + } else { + b.hoverTexture.Copy(ctx.Renderer, Pt(pos.X+left, top)) + } + } else { + b.priceTexture.Copy(ctx.Renderer, Pt(pos.X+buttonBarWidth-8-b.priceTexture.Size().X, pos.Y+buttonBarWidth-20)) + } +} diff --git a/cmd/tins2020/res/images/basket.png b/cmd/tins2020/res/images/basket.png new file mode 100644 index 0000000..3750941 Binary files /dev/null and b/cmd/tins2020/res/images/basket.png differ diff --git a/cmd/tins2020/res/images/fastForward.png b/cmd/tins2020/res/images/fastForward.png new file mode 100644 index 0000000..91f4b49 Binary files /dev/null and b/cmd/tins2020/res/images/fastForward.png differ diff --git a/cmd/tins2020/res/images/forward.png b/cmd/tins2020/res/images/forward.png new file mode 100644 index 0000000..edf9f6f Binary files /dev/null and b/cmd/tins2020/res/images/forward.png differ diff --git a/cmd/tins2020/res/images/gear.png b/cmd/tins2020/res/images/gear.png new file mode 100644 index 0000000..9663f90 Binary files /dev/null and b/cmd/tins2020/res/images/gear.png differ diff --git a/cmd/tins2020/res/images/genericItem_color_022.png b/cmd/tins2020/res/images/genericItem_color_022.png new file mode 100644 index 0000000..128d3ef Binary files /dev/null and b/cmd/tins2020/res/images/genericItem_color_022.png differ diff --git a/cmd/tins2020/res/images/genericItem_color_111.png b/cmd/tins2020/res/images/genericItem_color_111.png new file mode 100644 index 0000000..20d5ed6 Binary files /dev/null and b/cmd/tins2020/res/images/genericItem_color_111.png differ diff --git a/cmd/tins2020/res/images/pause.png b/cmd/tins2020/res/images/pause.png new file mode 100644 index 0000000..2fc9a9b Binary files /dev/null and b/cmd/tins2020/res/images/pause.png differ diff --git a/cmd/tins2020/res/images/power.png b/cmd/tins2020/res/images/power.png new file mode 100644 index 0000000..60f2d5f Binary files /dev/null and b/cmd/tins2020/res/images/power.png differ diff --git a/cmd/tins2020/res/images/save.png b/cmd/tins2020/res/images/save.png new file mode 100644 index 0000000..d9e1d6e Binary files /dev/null and b/cmd/tins2020/res/images/save.png differ diff --git a/cmd/tins2020/res/textures.txt b/cmd/tins2020/res/textures.txt index 75fa049..a3fb192 100644 --- a/cmd/tins2020/res/textures.txt +++ b/cmd/tins2020/res/textures.txt @@ -1,4 +1,15 @@ -game-control-hover: images/game_control_hover.png +control-hover: images/game_control_hover.png +control-shovel: images/genericItem_color_022.png +control-research: images/genericItem_color_111.png + +control-settings: images/gear.png +control-save: images/save.png +control-load: images/basket.png +control-quit: images/power.png + +control-pause: images/pause.png +control-run: images/forward.png +control-run-fast: images/fastForward.png tile-dirt: images/tile_dirt.png tile-grass: images/tile_grass.png diff --git a/cmd/tins2020/tins2020.go b/cmd/tins2020/tins2020.go index e50d65e..a1279b8 100644 --- a/cmd/tins2020/tins2020.go +++ b/cmd/tins2020/tins2020.go @@ -67,9 +67,10 @@ func run() error { ctx.Init(renderer) err = ctx.Fonts.LoadDesc( - tins2020.FontDescriptor{Name: "debug", Path: "fonts/OpenSans-Regular.ttf", Size: 12}, - tins2020.FontDescriptor{Name: "default", Path: "fonts/FiraMono-Regular.ttf", Size: 16}, - tins2020.FontDescriptor{Name: "small", Path: "fonts/FiraMono-Regular.ttf", Size: 12}, + tins2020.FontDescriptor{Name: "debug", Path: "fonts/FiraMono-Regular.ttf", Size: 12}, + tins2020.FontDescriptor{Name: "default", Path: "fonts/OpenSans-Regular.ttf", Size: 16}, + tins2020.FontDescriptor{Name: "small", Path: "fonts/OpenSans-Regular.ttf", Size: 12}, + tins2020.FontDescriptor{Name: "balance", Path: "fonts/OpenSans-Bold.ttf", Size: 40}, ) if err != nil { return err @@ -86,7 +87,7 @@ func run() error { app := tins2020.NewContainer() overlays := tins2020.NewContainer() - gameControls := tins2020.NewGameControls() + gameControls := tins2020.NewGameControls(game) overlays.AddChild(gameControls) overlays.AddChild(&tins2020.FPS{}) content := tins2020.NewContainer() @@ -98,6 +99,9 @@ func run() error { return err } + w, h := window.GetSize() + app.Arrange(ctx, tins2020.Rect(0, 0, w, h)) + for { for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { switch e := event.(type) { @@ -111,6 +115,7 @@ func run() error { ctx.Settings.Window.Location = tins2020.PtPtr(x, y) case sdl.WINDOWEVENT_SIZE_CHANGED: w, h := window.GetSize() + app.Arrange(ctx, tins2020.Rect(0, 0, w, h)) ctx.Settings.Window.Size = tins2020.PtPtr(w, h) } case *sdl.KeyboardEvent: diff --git a/color.go b/color.go index 4ef740b..2c65621 100644 --- a/color.go +++ b/color.go @@ -25,6 +25,14 @@ func HexColor(s string) (sdl.Color, error) { return sdl.Color{R: uint8(values[0]), G: uint8(values[1]), B: uint8(values[2]), A: uint8(a)}, nil } +func MustHexColor(s string) sdl.Color { + color, err := HexColor(s) + if err != nil { + panic(err) + } + return color +} + func HexToInt(s string) (int, error) { var i int for _, c := range s { diff --git a/container.go b/container.go index d4fbc90..4ca9673 100644 --- a/container.go +++ b/container.go @@ -5,6 +5,8 @@ import ( ) type Container struct { + ControlBase + Children []Control } @@ -16,19 +18,23 @@ 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) { + c.ControlBase.Handle(ctx, event) for _, child := range c.Children { child.Handle(ctx, event) } } -func (c *Container) Render(ctx *Context) { - for _, child := range c.Children { - child.Render(ctx) - } -} - func (c *Container) Init(ctx *Context) error { + c.ControlBase.Init(ctx) + for _, child := range c.Children { err := child.Init(ctx) if err != nil { @@ -37,3 +43,11 @@ func (c *Container) Init(ctx *Context) error { } return nil } + +func (c *Container) Render(ctx *Context) { + c.ControlBase.Render(ctx) + + for _, child := range c.Children { + child.Render(ctx) + } +} diff --git a/control.go b/control.go index 9a9e66b..e7540e4 100644 --- a/control.go +++ b/control.go @@ -4,23 +4,47 @@ import "github.com/veandco/go-sdl2/sdl" type Control interface { Init(*Context) error + Arrange(*Context, Rectangle) Handle(*Context, sdl.Event) Render(*Context) } +type EventFn func(*Context) + +type EmptyEventFn func() + +func EmptyEvent(fn EmptyEventFn) EventFn { + return func(*Context) { fn() } +} + type ControlBase struct { Bounds Rectangle IsMouseOver bool + + OnLeftMouseButtonClick EventFn } +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) { switch e := event.(type) { case *sdl.MouseMotionEvent: b.IsMouseOver = b.Bounds.IsPointInside(e.X, e.Y) + case *sdl.MouseButtonEvent: + if b.IsMouseOver && e.Button == sdl.BUTTON_LEFT && e.Type == sdl.MOUSEBUTTONDOWN { + b.Invoke(ctx, b.OnLeftMouseButtonClick) + } } } +func (b *ControlBase) Invoke(ctx *Context, fn EventFn) { + if fn == nil { + return + } + fn(ctx) +} + func (b *ControlBase) Render(*Context) {} diff --git a/fonts.go b/fonts.go index 7a0e65c..5751b48 100644 --- a/fonts.go +++ b/fonts.go @@ -20,7 +20,7 @@ type Font struct { *ttf.Font } -func (f *Font) Render(renderer *sdl.Renderer, text string, pos Point, color sdl.Color) (*Texture, error) { +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 @@ -34,31 +34,25 @@ func (f *Font) Render(renderer *sdl.Renderer, text string, pos Point, color sdl. } func (f *Font) RenderCopyAlign(renderer *sdl.Renderer, text string, pos Point, color sdl.Color, align TextAlignment) error { - texture, err := f.Render(renderer, text, pos, color) + texture, err := f.Render(renderer, text, color) if err != nil { return err } defer texture.Destroy() - rect := texture.Rect() + size := texture.Size() switch align { case TextAlignmentLeft: - texture.Copy(renderer, RectSize(pos.X, pos.Y, rect.W, rect.H).SDLPtr()) + texture.Copy(renderer, Pt(pos.X, pos.Y-size.Y)) case TextAlignmentCenter: - texture.Copy(renderer, RectSize(pos.X-(rect.W/2), pos.Y, rect.W, rect.H).SDLPtr()) + texture.Copy(renderer, Pt(pos.X-(size.X/2), pos.Y-size.Y)) case TextAlignmentRight: - texture.Copy(renderer, RectSize(pos.X-rect.W, pos.Y, rect.W, rect.H).SDLPtr()) + 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 { - texture, err := f.Render(renderer, text, pos, color) - if err != nil { - return err - } - defer texture.Destroy() - texture.Copy(renderer, texture.RectOffset(pos)) - return nil + return f.RenderCopyAlign(renderer, text, pos, color, TextAlignmentLeft) } type Fonts struct { diff --git a/fpsrenderer.go b/fpsrenderer.go index 539af83..ddacc9a 100644 --- a/fpsrenderer.go +++ b/fpsrenderer.go @@ -37,5 +37,5 @@ func (f *FPS) Render(ctx *Context) { f.ticks[f.slot]++ font := ctx.Fonts.Font("debug") - font.RenderCopy(ctx.Renderer, fmt.Sprintf("FPS: %d", f.total), Pt(5, 5), sdl.Color{R: 255, G: 255, B: 255, A: 255}) + 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 5802e52..1bd918d 100644 --- a/game.go +++ b/game.go @@ -6,13 +6,21 @@ import ( ) type Game struct { - Money int + Balance int + Speed GameSpeed Terrain *Map - start time.Time - lastUpdate time.Duration + simulation Animation } +type GameSpeed string + +const ( + GameSpeedNormal GameSpeed = "normal" + GameSpeedFast = "fast" + GameSpeedPaused = "paused" +) + type Map struct { Temp NoiseMap Humid NoiseMap @@ -36,6 +44,9 @@ func (m *Map) NewFlower(pos Point, traits FlowerTraits) Flower { return flower } +const simulationInterval = 120 * time.Millisecond +const fastSimulationInterval = 40 * time.Millisecond + func NewGame() *Game { terrain := &Map{ Temp: NewNoiseMap(rand.Int63()), @@ -47,18 +58,10 @@ func NewGame() *Game { } terrain.AddFlower(Pt(0, 0), NewPoppyTraits()) return &Game{ - Money: 100, + Balance: 100, Terrain: terrain, - start: time.Now(), - } -} - -func (g *Game) Update() { - update := time.Since(g.start) - for g.lastUpdate < update { - g.tick() - g.lastUpdate += time.Millisecond * 10 + simulation: NewAnimation(time.Millisecond * 10), } } @@ -92,3 +95,26 @@ func (g *Game) tick() { } g.Terrain.Flowers = flowers } + +func (g *Game) Pause() { + g.Speed = GameSpeedPaused + g.simulation.Pause() +} + +func (g *Game) Run() { + g.Speed = GameSpeedNormal + g.simulation.SetInterval(simulationInterval) + g.simulation.Run() +} + +func (g *Game) RunFast() { + g.Speed = GameSpeedFast + g.simulation.SetInterval(fastSimulationInterval) + g.simulation.Run() +} + +func (g *Game) Update() { + for g.simulation.Animate() { + g.tick() + } +} diff --git a/gamecontrols.go b/gamecontrols.go index a27eabf..f240005 100644 --- a/gamecontrols.go +++ b/gamecontrols.go @@ -1,114 +1,77 @@ package tins2020 -import ( - "log" - - "github.com/veandco/go-sdl2/sdl" -) - type GameControls struct { - ControlBase + Container + + game *Game menu ButtonBar + top ButtonBar flowers ButtonBar + + pause *IconButton + run *IconButton + runFast *IconButton } -type ButtonBar struct { - Top int32 - Left int32 - Bottom int32 - Hover int - Buttons []Button +func NewGameControls(game *Game) *GameControls { + return &GameControls{game: game} } -type Button struct { - Icon string - Disabled string - - IsDisabled bool +func (c *GameControls) updateSpeedControls() { } -const buttonBarWidth = 96 - -func (b *ButtonBar) Handle(ctx *Context, event sdl.Event) { - switch e := event.(type) { - case *sdl.MouseMotionEvent: - if e.X > b.Left && e.X < b.Left+buttonBarWidth { - button := int(e.Y-b.Top) / buttonBarWidth - if button < 0 || button >= len(b.Buttons) || b.Buttons[button].IsDisabled { - button = -1 - } - b.Hover = button - } else { - b.Hover = -1 - } - } +func (c *GameControls) Arrange(ctx *Context, bounds Rectangle) { + c.Bounds = bounds + c.menu.Arrange(ctx, RectSize(bounds.X, bounds.Y, buttonBarWidth, bounds.H)) + c.top.Arrange(ctx, Rect(bounds.X+bounds.W/2+8, bounds.Y, bounds.Right(), bounds.Y+64)) + c.flowers.Arrange(ctx, RectSize(bounds.Right()-buttonBarWidth, bounds.Y, buttonBarWidth, bounds.H)) } -func (b *ButtonBar) Render(ctx *Context) { - ctx.Renderer.FillRect(Rect(b.Left, b.Top, b.Left+buttonBarWidth, b.Bottom).SDLPtr()) - texture := func(b Button) *Texture { - if b.IsDisabled { - texture := ctx.Textures.Texture(b.Disabled) - if texture != nil { - return texture - } - } - return ctx.Textures.Texture(b.Icon) - } - hoverTexture := ctx.Textures.Texture("game-control-hover") - for i, button := range b.Buttons { - pos := Pt(b.Left, b.Top+int32(i)*buttonBarWidth) - texture := texture(button) - texture.Copy(ctx.Renderer, &sdl.Rect{X: pos.X, Y: pos.Y - 40, W: buttonBarWidth, H: 120}) - if b.Hover == i { - hoverTexture.Copy(ctx.Renderer, hoverTexture.RectOffset(pos)) - } - } -} - -func NewGameControls() *GameControls { - return &GameControls{} +func (c *GameControls) buyPoppy(ctx *Context) { + c.game.Balance -= 10 } func (c *GameControls) Init(ctx *Context) error { - c.flowers.Hover = -1 - c.flowers.Buttons = []Button{ - Button{Icon: "flower-poppy-1", Disabled: "flower-poppy-disabled"}, - Button{Icon: "flower-red-c-1", Disabled: "flower-red-c-disabled", IsDisabled: true}, + c.flowers.Background = MustHexColor("#356dad") // brown alternative? #4ac69a + c.flowers.Buttons = []Control{ + NewBuyFlowerButton("flower-poppy-1", "flower-poppy-disabled", "Poppy", 10, "A very generic flower that thrives in a moderate climate.", false, c.buyPoppy), + NewBuyFlowerButton("flower-red-c-1", "flower-poppy-disabled", "Unknown", 100, "Traits are not known yet.", true, nil), } - return c.updateBarPositions(ctx) -} -func (c *GameControls) Handle(ctx *Context, event sdl.Event) { - c.menu.Handle(ctx, event) - c.flowers.Handle(ctx, event) + c.top.Orientation = OrientationHorizontal + c.pause = NewIconButton("control-pause", EmptyEvent(func() { + c.game.Pause() + c.updateSpeedControls() + })) + c.run = NewIconButton("control-run", EmptyEvent(func() { + c.game.Run() + c.updateSpeedControls() + })) + c.runFast = NewIconButton("control-run-fast", EmptyEvent(func() { + c.game.RunFast() + c.updateSpeedControls() + })) + c.top.Buttons = []Control{c.pause, c.run, c.runFast} - switch e := event.(type) { - case *sdl.WindowEvent: - switch e.Event { - case sdl.WINDOWEVENT_RESIZED: - err := c.updateBarPositions(ctx) - if err != nil { - log.Fatal(err) - } - } + c.menu.Background = MustHexColor("#356dad") + c.menu.Buttons = []Control{ + NewIconButton("control-settings", EmptyEvent(func() {})), + NewIconButton("control-save", EmptyEvent(func() {})), + NewIconButton("control-load", EmptyEvent(func() {})), } + + c.Container.AddChild(&c.menu) + c.Container.AddChild(&c.top) + c.Container.AddChild(&c.flowers) + return c.Container.Init(ctx) } func (c *GameControls) Render(ctx *Context) { - // ctx.Renderer.SetDrawColor(74, 198, 154, 255) // #4ac69a - ctx.Renderer.SetDrawColor(53, 109, 173, 255) - c.menu.Render(ctx) - c.flowers.Render(ctx) -} + topBar := MustHexColor("#0000007f") + ctx.Renderer.SetDrawColor(topBar.R, topBar.G, topBar.B, topBar.A) + ctx.Renderer.FillRect(Rect(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("#79A6D9"), TextAlignmentRight) -func (c *GameControls) updateBarPositions(ctx *Context) error { - w, h, err := ctx.Renderer.GetOutputSize() - if err != nil { - return err - } - c.menu.Top, c.menu.Left, c.menu.Bottom = 0, 0, h - c.flowers.Top, c.flowers.Left, c.flowers.Bottom = 0, w-buttonBarWidth, h - return nil + c.Container.Render(ctx) } diff --git a/iconbutton.go b/iconbutton.go new file mode 100644 index 0000000..2cd449d --- /dev/null +++ b/iconbutton.go @@ -0,0 +1,62 @@ +package tins2020 + +type IconButton struct { + ControlBase + + Icon string + IconDisabled string + IconScale Scale + IconWidth int32 + + IsDisabled bool +} + +type Scale int + +const ( + ScaleCenter Scale = iota + ScaleStretch +) + +func NewIconButton(icon string, onClick EventFn) *IconButton { + return &IconButton{ + ControlBase: ControlBase{ + OnLeftMouseButtonClick: onClick, + }, + Icon: icon, + } +} + +func NewIconButtonConfig(icon string, onClick EventFn, configure func(*IconButton)) *IconButton { + button := NewIconButton(icon, onClick) + 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 + } + } + return ctx.Textures.Texture(b.Icon) +} + +func (b *IconButton) Render(ctx *Context) { + iconTexture := b.activeTexture(ctx) + mouseOverTexture := ctx.Textures.Texture("control-hover") + + if b.IconScale == ScaleCenter { + size := iconTexture.Size() + if b.IconWidth != 0 { + size = Pt(b.IconWidth, b.IconWidth*size.Y/size.X) + } + iconTexture.CopyResize(ctx.Renderer, RectSize(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 b.IsMouseOver && !b.IsDisabled { + mouseOverTexture.CopyResize(ctx.Renderer, b.Bounds) + } +} diff --git a/math.go b/math.go index b599c3c..fb059ee 100644 --- a/math.go +++ b/math.go @@ -20,6 +20,13 @@ func Ceil32(x float32) float32 { return float32(math.Ceil(float64(x))) } func Floor32(x float32) float32 { return float32(math.Floor(float64(x))) } +func Max(a, b int32) int32 { + if a > b { + return a + } + return b +} + func Max32(a, b float32) float32 { if a > b { return a @@ -27,6 +34,13 @@ func Max32(a, b float32) float32 { return b } +func Min(a, b int32) int32 { + if a < b { + return a + } + return b +} + func Min32(a, b float32) float32 { if a < b { return a diff --git a/money.go b/money.go new file mode 100644 index 0000000..992653d --- /dev/null +++ b/money.go @@ -0,0 +1,5 @@ +package tins2020 + +import "fmt" + +func FmtMoney(amount int) string { return fmt.Sprintf("$ %d", amount) } diff --git a/projection.go b/projection.go index e1aaa23..9c00816 100644 --- a/projection.go +++ b/projection.go @@ -11,7 +11,8 @@ type projection struct { zoom float32 zoomInv float32 - windowRect Rectangle + windowInteractRect Rectangle + windowVisibleRect Rectangle tileScreenDelta PointF tileScreenDeltaInv PointF tileScreenOffset Point @@ -44,12 +45,12 @@ func (p *projection) screenToMapRel(x, y int32) PointF { 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) *sdl.Rect { - return &sdl.Rect{X: pos.X - p.tileFitScreenSize.X, Y: pos.Y - p.tileFitScreenSize.Y, W: 2 * p.tileFitScreenSize.X, H: 2 * p.tileFitScreenSize.Y} +func (p *projection) screenToTileFitRect(pos Point) Rectangle { + return RectSize(pos.X-p.tileFitScreenSize.X, pos.Y-p.tileFitScreenSize.Y, 2*p.tileFitScreenSize.X, 2*p.tileFitScreenSize.Y) } -func (p *projection) screenToTileRect(pos Point) *sdl.Rect { - return &sdl.Rect{X: pos.X - p.tileScreenOffset.X, Y: pos.Y - p.tileScreenOffset.Y, W: p.tileScreenSize.X, H: p.tileScreenSize.Y} +func (p *projection) screenToTileRect(pos Point) Rectangle { + return RectSize(pos.X-p.tileScreenOffset.X, pos.Y-p.tileScreenOffset.Y, p.tileScreenSize.X, p.tileScreenSize.Y) } func (p *projection) update(renderer *sdl.Renderer) { @@ -64,24 +65,26 @@ func (p *projection) update(renderer *sdl.Renderer) { log.Fatal(err) } p.windowCenter = Pt(windowW/2, windowH/2) - p.windowRect = RectSize(buttonBarWidth, 0, windowW-2*buttonBarWidth, windowH) + p.windowInteractRect = Rect(buttonBarWidth, 64, windowW-buttonBarWidth, windowH) + p.windowVisibleRect = Rect(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)) { - topLeft := p.screenToMap(p.windowRect.X, p.windowRect.Y) - topRight := p.screenToMap(p.windowRect.X+p.windowRect.W, p.windowRect.Y) - bottomLeft := p.screenToMap(p.windowRect.X, p.windowRect.Y+p.windowRect.H) - bottomRight := p.screenToMap(p.windowRect.X+p.windowRect.W, p.windowRect.Y+p.windowRect.H) + 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 < p.windowRect.X || rectFit.Y+rectFit.H < p.windowRect.Y { + if rectFit.X+rectFit.W < visible.X || rectFit.Y+rectFit.H < visible.Y { continue } - if rectFit.X > p.windowRect.X+p.windowRect.W || rectFit.Y > p.windowRect.Y+p.windowRect.H { + if rectFit.X > visible.X+visible.W || rectFit.Y > visible.Y+visible.H { break } action(x, y, pos) diff --git a/rect.go b/rect.go index 5eba166..f345692 100644 --- a/rect.go +++ b/rect.go @@ -6,12 +6,6 @@ type Rectangle struct { sdl.Rect } -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 Rect(x1, y1, x2, y2 int32) Rectangle { if x1 > x2 { x1, x2 = x2, x1 @@ -24,5 +18,15 @@ func Rect(x1, y1, x2, y2 int32) Rectangle { func RectSize(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/terrainrenderer.go b/terrainrenderer.go index 58e65fd..15701c5 100644 --- a/terrainrenderer.go +++ b/terrainrenderer.go @@ -24,6 +24,10 @@ func NewTerrainRenderer(terrain *Map) Control { return &terrainRenderer{terrain: terrain, project: newProjection()} } +func (r *terrainRenderer) Arrange(ctx *Context, _ Rectangle) { + r.project.update(ctx.Renderer) +} + func (r *terrainRenderer) Init(ctx *Context) error { r.project.update(ctx.Renderer) return nil @@ -32,16 +36,18 @@ func (r *terrainRenderer) Init(ctx *Context) error { func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) { switch e := event.(type) { case *sdl.MouseButtonEvent: - if e.Button == sdl.BUTTON_LEFT { - r.interact.mouseLeftDown = e.Type == sdl.MOUSEBUTTONDOWN - if r.interact.mouseLeftDown && r.interact.mouseDrag == nil { - r.interact.mouseDrag = PtPtr(e.X, e.Y) - } else if !r.interact.mouseLeftDown && r.interact.mouseDrag != nil { - r.interact.mouseDrag = nil + if r.project.windowInteractRect.IsPointInside(e.X, e.Y) { + if e.Button == sdl.BUTTON_LEFT { + r.interact.mouseLeftDown = e.Type == sdl.MOUSEBUTTONDOWN + if r.interact.mouseLeftDown && r.interact.mouseDrag == nil { + r.interact.mouseDrag = PtPtr(e.X, e.Y) + } else if !r.interact.mouseLeftDown && r.interact.mouseDrag != nil { + r.interact.mouseDrag = nil + } } } case *sdl.MouseMotionEvent: - if r.project.windowRect.IsPointInside(e.X, e.Y) { + if r.project.windowInteractRect.IsPointInside(e.X, e.Y) { hover := r.project.screenToMap(e.X, e.Y) r.hover = PtPtr(int32(Round32(hover.X)), int32(Round32(hover.Y))) } else { @@ -67,10 +73,6 @@ func (r *terrainRenderer) Handle(ctx *Context, event sdl.Event) { r.project.update(ctx.Renderer) } } - case *sdl.WindowEvent: - if e.Event == sdl.WINDOWEVENT_RESIZED { - r.project.update(ctx.Renderer) - } } } @@ -157,10 +159,10 @@ func (r *terrainRenderer) Render(ctx *Context) { r.project.visibleTiles(func(x, y int32, pos Point) { text := toTileTexture(x, y) rect := r.project.screenToTileRect(pos) - text.Copy(ctx.Renderer, rect) + text.CopyResize(ctx.Renderer, rect) if r.hover != nil && x == r.hover.X && y == r.hover.Y { - ctx.Textures.Texture("tile-hover").Copy(ctx.Renderer, rect) + ctx.Textures.Texture("tile-hover").CopyResize(ctx.Renderer, rect) } }) @@ -172,7 +174,7 @@ func (r *terrainRenderer) Render(ctx *Context) { placeX, placeY := r.terrain.PlaceX.Value(x, y), r.terrain.PlaceY.Value(x, y) pos = r.project.mapToScreenF(float32(x)-.2+float32(.9*placeX-.45), float32(y)-.2+float32(.9*placeY-.45)) - text.Copy(ctx.Renderer, r.project.screenToTileRect(pos)) + text.CopyResize(ctx.Renderer, r.project.screenToTileRect(pos)) }) // gfx.RectangleColor(ctx.Renderer, r.project.windowRect.X, r.project.windowRect.Y, r.project.windowRect.X+r.project.windowRect.W, r.project.windowRect.Y+r.project.windowRect.H, sdl.Color{R: 255, A: 255}) diff --git a/textures.go b/textures.go index d7bbdfb..9128eb1 100644 --- a/textures.go +++ b/textures.go @@ -11,7 +11,7 @@ import ( type Texture struct { texture *sdl.Texture - rect *sdl.Rect + size Point } func NewTextureFromSurface(renderer *sdl.Renderer, surface *sdl.Surface) (*Texture, error) { @@ -19,21 +19,33 @@ func NewTextureFromSurface(renderer *sdl.Renderer, surface *sdl.Surface) (*Textu if err != nil { return nil, err } - return &Texture{texture: texture, rect: &sdl.Rect{X: 0, Y: 0, W: surface.W, H: surface.H}}, nil + return &Texture{texture: texture, size: Pt(surface.W, surface.H)}, nil } -func (t *Texture) Rect() *sdl.Rect { return t.rect } +func (t *Texture) Size() Point { return t.size } -func (t *Texture) RectOffset(offset Point) *sdl.Rect { - return &sdl.Rect{X: offset.X, Y: offset.Y, W: t.rect.W, H: t.rect.H} +// 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, RectSize(dst.X, dst.Y, t.size.X, t.size.Y)) } -func (t *Texture) Copy(renderer *sdl.Renderer, target *sdl.Rect) { - renderer.Copy(t.texture, t.rect, target) +func (t *Texture) CopyPart(renderer *sdl.Renderer, src Rectangle, dst Point) { + t.CopyPartResize(renderer, src, RectSize(dst.X, dst.Y, src.W, src.H)) } -// func (t *Texture) CopyF(renderer *sdl.Renderer, target *sdl.FRect) { -// renderer.CopyF(t.texture, t.rect, target) // Depends on SDL >=2.0.10 +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, Rect(0, 0, t.size.X, t.size.Y), dst) +} + +// 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() }