diff --git a/cmd/tins2020/tins2020.go b/cmd/tins2020/tins2020.go index 1890eda..48bb930 100644 --- a/cmd/tins2020/tins2020.go +++ b/cmd/tins2020/tins2020.go @@ -96,7 +96,7 @@ func run() error { app := tins2020.NewContainer() overlays := tins2020.NewContainer() - dialogs := tins2020.NewDialogs() + dialogs := tins2020.NewDialogs(game) overlays.AddChild(dialogs) overlays.AddChild(&tins2020.FPS{Show: &game.Debug}) @@ -112,7 +112,7 @@ func run() error { if err != nil { return err } - dialogs.ShowIntro() + dialogs.ShowIntro(ctx) w, h := window.GetSize() app.Arrange(ctx, tins2020.Rect(0, 0, w, h)) diff --git a/control.go b/control.go index 713ce9e..c4b43d5 100644 --- a/control.go +++ b/control.go @@ -32,30 +32,27 @@ func (b *ControlBase) Arrange(ctx *Context, bounds Rectangle) { b.Bounds = bound func (b *ControlBase) Init(*Context) error { return nil } func (b *ControlBase) Handle(ctx *Context, event sdl.Event) bool { - b.HandleNoFeedback(ctx, event) - return false -} - -func (b *ControlBase) HandleNoFeedback(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) + 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) { +func (b *ControlBase) Invoke(ctx *Context, fn EventContextFn) bool { if fn == nil { - return + return false } fn(ctx) + return true } func (b *ControlBase) Render(*Context) {} diff --git a/dialogs.go b/dialogs.go index 0afc78b..7bc6a44 100644 --- a/dialogs.go +++ b/dialogs.go @@ -5,25 +5,37 @@ type Dialogs struct { intro Control settings Control + research Control dialogClosed *Events dialogOpened *Events } -func NewDialogs() *Dialogs { +func NewDialogs(game *Game) *Dialogs { return &Dialogs{ + intro: &Intro{}, + settings: &LargeDialog{}, + research: NewResearch(game), + dialogClosed: NewEvents(), dialogOpened: NewEvents(), } } +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) Arrange(ctx *Context, bounds Rectangle) { + d.Proxy.Arrange(ctx, bounds) +} + func (d *Dialogs) DialogClosed() EventHandler { return d.dialogClosed } func (d *Dialogs) DialogOpened() EventHandler { return d.dialogOpened } func (d *Dialogs) Init(ctx *Context) error { - d.intro = &Intro{} - d.settings = &DialogBase{} - err := d.intro.Init(ctx) if err != nil { return err @@ -32,22 +44,23 @@ func (d *Dialogs) Init(ctx *Context) error { if err != nil { return err } + err = d.research.Init(ctx) return nil } func (d *Dialogs) Close() { - d.Proxied = nil + d.SetContent(nil, nil) d.dialogClosed.Notify(nil) } -func (d *Dialogs) ShowIntro() { - d.Proxied = d.intro - d.intro.(Dialog).ShowDialog(d.Close) - d.dialogOpened.Notify(nil) +func (d *Dialogs) ShowIntro(ctx *Context) { + d.showDialog(ctx, d.intro) } -func (d *Dialogs) ShowSettings() { - d.Proxied = d.settings - d.settings.(Dialog).ShowDialog(d.Close) - d.dialogOpened.Notify(nil) +func (d *Dialogs) ShowResearch(ctx *Context) { + d.showDialog(ctx, d.research) +} + +func (d *Dialogs) ShowSettings(ctx *Context) { + d.showDialog(ctx, d.settings) } diff --git a/game.go b/game.go index e8942a8..baade03 100644 --- a/game.go +++ b/game.go @@ -213,10 +213,6 @@ func (g *Game) SelectShovel() { g.selectTool(&ShovelTool{}) } -func (g *Game) SelectResearch() { - g.Pause() -} - func (g *Game) SpeedChanged() EventHandler { return g.speedChanged } func (g *Game) State() GameState { @@ -258,6 +254,11 @@ func (g *Game) Tool() Tool { return g.tool } func (g *Game) ToolChanged() EventHandler { return g.toolChanged } +func (g *Game) UnlockNextFlower() { + g.Herbarium.UnlockNext() + g.selectTool(nil) +} + func (g *Game) Update() { for g.simulation.Animate() { g.tick() diff --git a/gamecontrols.go b/gamecontrols.go index 9b3a777..99f29c9 100644 --- a/gamecontrols.go +++ b/gamecontrols.go @@ -117,7 +117,7 @@ func (c *GameControls) Init(ctx *Context) error { c.menu.Background = MustHexColor("#356dad") c.menu.Buttons = []Control{ - NewIconButton("control-settings", func(*Context) { c.dialogs.ShowSettings() }), + NewIconButton("control-settings", c.dialogs.ShowSettings), NewIconButton("control-save", func(*Context) { c.game.Save() }), NewIconButton("control-load", func(ctx *Context) { c.game.Load() @@ -127,11 +127,11 @@ func (c *GameControls) Init(ctx *Context) error { c.game.New() c.updateFlowerControls(ctx) }), - NewIconButton("control-information", func(*Context) { c.dialogs.ShowIntro() }), + NewIconButton("control-information", c.dialogs.ShowIntro), } c.shovel = NewIconButtonConfig("control-shovel", func(*Context) { c.game.SelectShovel() }, func(b *IconButton) { b.IconHeight = 32 }) - c.research = NewIconButtonConfig("control-research", func(*Context) { c.game.SelectResearch() }, func(b *IconButton) { b.IconHeight = 32 }) + c.research = NewIconButtonConfig("control-research", c.dialogs.ShowResearch, func(b *IconButton) { b.IconHeight = 32 }) c.otherTools.Buttons = []Control{c.shovel, c.research} c.Container.AddChild(&c.menu) @@ -160,10 +160,10 @@ func (c *GameControls) Handle(ctx *Context, event sdl.Event) bool { case sdl.K_d: c.game.SelectShovel() case sdl.K_r: - c.game.SelectResearch() + c.dialogs.ShowResearch(ctx) case sdl.K_ESCAPE: if c.game.Tool() == nil { - c.dialogs.ShowIntro() + c.dialogs.ShowIntro(ctx) } else { c.game.CancelTool() } diff --git a/herbarium.go b/herbarium.go index e8f3011..d04a7b5 100644 --- a/herbarium.go +++ b/herbarium.go @@ -36,7 +36,7 @@ func (h *Herbarium) Reset() { BuyPrice: 100, SellPrice: 20, Traits: NewLoosestrifeTraits(), - Unlocked: true, + Unlocked: false, }) h.Add("coneflower", FlowerDescriptor{ Name: "Coneflower", @@ -45,7 +45,7 @@ func (h *Herbarium) Reset() { BuyPrice: 500, SellPrice: 100, Traits: NewConeflowerTraits(), - Unlocked: true, + Unlocked: false, }) h.Add("tulip", FlowerDescriptor{ Name: "Tulip", @@ -54,7 +54,7 @@ func (h *Herbarium) Reset() { BuyPrice: 20000, SellPrice: 5000, Traits: NewTulipTraits(), - Unlocked: true, + Unlocked: false, }) h.Add("ajuga", FlowerDescriptor{ Name: "Ajuga", @@ -63,7 +63,7 @@ func (h *Herbarium) Reset() { BuyPrice: 100000, SellPrice: 10000, Traits: NewAjugaTraits(), - Unlocked: true, + Unlocked: false, }) } @@ -114,3 +114,28 @@ func (h *Herbarium) IsUnlocked(id string) bool { } return flower.Unlocked } + +func (h *Herbarium) NextFlowerToUnlock() (string, FlowerDescriptor, int) { + var previous *FlowerDescriptor + for _, id := range h.Flowers() { + flower, _ := h.Find(id) + if !flower.Unlocked { + if previous == nil { + return "", FlowerDescriptor{}, 0 + } + return id, flower, previous.BuyPrice * 2 + } + previous = &flower + } + return "", FlowerDescriptor{}, 0 +} + +func (h *Herbarium) UnlockNext() { + // id, flower, _ := h.NextFlowerToUnlock() + // if flower == nil { + // return + // } + + // flower.Unlocked = true + // h.flowers[flower] +} diff --git a/label.go b/label.go index f622790..9f56397 100644 --- a/label.go +++ b/label.go @@ -48,10 +48,6 @@ type Paragraph struct { Label } -// func (p *Paragraph) Arrange(ctx *Context, bounds Rectangle) { -// p.Label.Arrange(ctx, bounds) -// } - func (p *Paragraph) Render(ctx *Context) { font := ctx.Fonts.Font(p.fontName()) color := p.fontColor() diff --git a/largedialog.go b/largedialog.go index c257b4e..4339405 100644 --- a/largedialog.go +++ b/largedialog.go @@ -6,12 +6,14 @@ type DialogBase struct { Container content Proxy + onShow *Events close EventFn } type Dialog interface { CloseDialog() - ShowDialog(EventFn) + OnShow() EventHandler + ShowDialog(*Context, EventFn) } func (d *DialogBase) CloseDialog() { @@ -21,17 +23,27 @@ func (d *DialogBase) CloseDialog() { } } +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(close EventFn) { +func (d *DialogBase) ShowDialog(ctx *Context, close EventFn) { d.close = close -} - -func (d *DialogBase) Init(ctx *Context) error { - d.AddChild(&d.content) - return d.Container.Init(ctx) + if d.onShow != nil { + d.onShow.Notify(ctx) + } } type LargeDialog struct { @@ -93,3 +105,5 @@ func (d *LargeDialog) Render(ctx *Context) { d.DialogBase.Render(ctx) } + +func (d *LargeDialog) SetCaption(s string) { d.title.Text = s } diff --git a/proxy.go b/proxy.go index 5ee5a92..570aa6b 100644 --- a/proxy.go +++ b/proxy.go @@ -6,9 +6,12 @@ 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 } @@ -35,3 +38,11 @@ func (p *Proxy) Render(ctx *Context) { } 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/research.go b/research.go new file mode 100644 index 0000000..9ed5a7a --- /dev/null +++ b/research.go @@ -0,0 +1,247 @@ +package tins2020 + +import ( + "fmt" + "math" + "math/rand" + "strconv" + "strings" + "time" + + "github.com/veandco/go-sdl2/sdl" +) + +type Research struct { + Container + + game *Game + botanist Specialist + farmer Specialist + + typing string + digitCount int + + close func() + description Paragraph + specialists Paragraph + input Label + digits []Digit + animate Animation +} + +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() } + return dialog +} + +type Digit struct { + ControlBase + + Value string + + highlight int +} + +func (d *Digit) Render(ctx *Context) { + font := ctx.Fonts.Font("title") + color := White + if d.highlight > 0 { + color = MustHexColor("#356DAD") + } + 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) Blink() { + d.highlight = 10 +} + +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 { + r.digits[i].Value = strconv.Itoa(i) + r.AddChild(&r.digits[i]) + } + + return nil +} + +func (r *Research) lastAvailableFlower() *FlowerDescriptor { + var desc *FlowerDescriptor + for _, id := range r.game.Herbarium.Flowers() { + flower, _ := r.game.Herbarium.Find(id) + if !flower.Unlocked { + return desc + } + desc = &flower + } + return desc + +} + +func (r *Research) firstLockedFlower() *FlowerDescriptor { + for _, id := range r.game.Herbarium.Flowers() { + flower, _ := r.game.Herbarium.Find(id) + if !flower.Unlocked { + return &flower + } + } + return nil +} + +func (r *Research) Arrange(ctx *Context, bounds Rectangle) { + r.Container.Arrange(ctx, bounds) + r.specialists.Arrange(ctx, RectSize(r.Bounds.X, r.Bounds.Y+40, r.Bounds.W, r.Bounds.H-40)) + r.input.Arrange(ctx, RectSize(r.Bounds.X, r.Bounds.X+r.Bounds.H-24, 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, RectSize(digitCenter.X-24, digitCenter.Y-24, 48, 48)) + } +} + +func (r *Research) userTyped(i int) { + r.digits[i].Blink() + digit := strconv.Itoa(i) + if len(r.typing) == 0 { + r.typing = digit + r.digitCount = 1 + } else if digit != r.typing { + r.typing = "" + r.digitCount = 0 + } else { + r.digitCount++ + } + if r.digitCount == i || r.digitCount == 10 { + r.input.Text += digit + r.typing = "" + r.digitCount = 0 + + if !strings.HasPrefix(r.input.Text, r.botanist.Number) { + r.input.Text = "" + } else if r.input.Text == r.botanist.Number { + r.game.UnlockNextFlower() + r.close() + } + } +} + +func (r *Research) Handle(ctx *Context, event sdl.Event) bool { + switch e := event.(type) { + case *sdl.KeyboardEvent: + 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) { + generateNumber := func() string { + var number string + for i := 0; i < 3; i++ { + number += strconv.Itoa(rand.Intn(9) + 1) + } + return number + } + r.digitCount = 0 + r.input.Text = "" + + var specialists string + defer func() { + r.specialists.Text = specialists + }() + available := r.lastAvailableFlower() + locked := r.firstLockedFlower() + if locked == nil { + specialists += "Botanist (unlocks next flower; unavailable)\n" + specialists += "Farmer (fertilizes land; unavailable)\n" + return + } + + r.botanist.Cost = 2 * available.BuyPrice + if r.game.Balance < r.botanist.Cost { + r.botanist.Number = "**unavailable**" + } else { + r.botanist.Number = generateNumber() + } + + specialists += fmt.Sprintf("Botanist: no. %s (unlocks next flower; $ %d)\n", r.botanist.Number, r.botanist.Cost) + specialists += "Farmer: no. **unavailable** (fertilizes land; $ ---)\n" +}