bubble up window size properly, add journal pages

This commit is contained in:
stryan 2022-07-04 17:45:15 -04:00
parent 308097311d
commit 4c8aac46cb
12 changed files with 379 additions and 15 deletions

13
data/journal.toml Normal file
View File

@ -0,0 +1,13 @@
[[journalpage]]
pageid = 0
title = "It Begins"
content = "So much to learn"
[[journalpage.requires]]
[[journalpage]]
pageid = 1
title = "Building"
content = "Craft that motherfucking tea button"
[[journalpage.requires]]
name = "tea"
value = 10

84
jmenu.go Normal file
View File

@ -0,0 +1,84 @@
package main
import (
sim "git.saintnet.tech/stryan/spacetea/simulator"
"github.com/charmbracelet/bubbles/list"
tea "github.com/charmbracelet/bubbletea"
)
type jEntry struct {
title, id string
}
func (i jEntry) Title() string { return i.title }
func (i jEntry) Description() string { return "" }
func (i jEntry) ID() string { return i.id }
func (i jEntry) FilterValue() string { return i.title }
type jMenuModel struct {
list list.Model
lastSize tea.WindowSizeMsg
}
type readMsg string
func newJMenuModel(pages []sim.JournalPage) jMenuModel {
var jm jMenuModel
items := []list.Item{}
for _, v := range pages {
items = append(items, jEntry{v.Title, v.ID()})
}
jm.list = list.New(items, list.NewDefaultDelegate(), 80, 32)
jm.list.DisableQuitKeybindings()
jm.list.Title = "Read which entry?"
return jm
}
func (j jMenuModel) Init() tea.Cmd {
return tea.EnterAltScreen
}
// Update is called when a message is received. Use it to inspect messages
// and, in response, update the model and/or send a command.
func (j jMenuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
switch msg := msg.(type) {
case tea.WindowSizeMsg:
h, v := docStyle.GetFrameSize()
j.list.SetSize(msg.Width-h, msg.Height-v)
j.lastSize = msg
return j, nil
case tea.KeyMsg:
switch keypress := msg.String(); keypress {
case "ctrl-c":
return j, tea.Quit
case "esc":
return initMainscreen(), tea.Batch(j.GetSize, heartbeat())
case "enter":
return initMainscreen(), tea.Batch(j.GetSize, j.buildJmenuMsg, heartbeat())
}
}
var cmd tea.Cmd
j.list, cmd = j.list.Update(msg)
return j, cmd
}
// View renders the program's UI, which is just a string. The view is
// rendered after every Update.
func (j jMenuModel) View() string {
return j.list.View()
}
func (j jMenuModel) buildJmenuMsg() tea.Msg {
if j.list.SelectedItem() == nil {
return ""
}
i := j.list.SelectedItem().(jEntry)
return readMsg(i.ID())
}
func (m jMenuModel) GetSize() tea.Msg {
return m.lastSize
}

128
jview.go Normal file
View File

@ -0,0 +1,128 @@
package main
import (
"fmt"
"strings"
"github.com/charmbracelet/bubbles/viewport"
tea "github.com/charmbracelet/bubbletea"
"github.com/charmbracelet/lipgloss"
)
const useHighPerformanceRenderer = false
var (
titleStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Right = "├"
return lipgloss.NewStyle().BorderStyle(b).Padding(0, 1)
}()
infoStyle = func() lipgloss.Style {
b := lipgloss.RoundedBorder()
b.Left = "┤"
return titleStyle.Copy().BorderStyle(b)
}()
)
type jview struct {
title string
content string
ready bool
lastSize tea.WindowSizeMsg
viewport viewport.Model
}
func newJView(title, content string) jview {
return jview{title: title, content: content}
}
func (m jview) Init() tea.Cmd {
return nil
}
func (m jview) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
var (
cmd tea.Cmd
cmds []tea.Cmd
)
switch msg := msg.(type) {
case tea.KeyMsg:
if k := msg.String(); k == "ctrl+c" || k == "q" || k == "esc" {
return initMainscreen(), tea.Batch(m.GetSize, heartbeat())
}
case tea.WindowSizeMsg:
m.lastSize = msg
headerHeight := lipgloss.Height(m.headerView())
footerHeight := lipgloss.Height(m.footerView())
verticalMarginHeight := headerHeight + footerHeight
if !m.ready {
// Since this program is using the full size of the viewport we
// need to wait until we've received the window dimensions before
// we can initialize the viewport. The initial dimensions come in
// quickly, though asynchronously, which is why we wait for them
// here.
m.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
m.viewport.YPosition = headerHeight
m.viewport.HighPerformanceRendering = useHighPerformanceRenderer
m.viewport.SetContent(m.content)
m.ready = true
// This is only necessary for high performance rendering, which in
// most cases you won't need.
//
// Render the viewport one line below the header.
m.viewport.YPosition = headerHeight + 1
} else {
m.viewport.Width = msg.Width
m.viewport.Height = msg.Height - verticalMarginHeight
}
if useHighPerformanceRenderer {
// Render (or re-render) the whole viewport. Necessary both to
// initialize the viewport and when the window is resized.
//
// This is needed for high-performance rendering only.
cmds = append(cmds, viewport.Sync(m.viewport))
}
}
// Handle keyboard and mouse events in the viewport
m.viewport, cmd = m.viewport.Update(msg)
cmds = append(cmds, cmd)
return m, tea.Batch(cmds...)
}
func (m jview) View() string {
if !m.ready {
return "\n Initializing..."
}
return fmt.Sprintf("%s\n%s\n%s", m.headerView(), m.viewport.View(), m.footerView())
}
func (m jview) headerView() string {
title := titleStyle.Render(m.title)
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(title)))
return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
}
func (m jview) footerView() string {
info := infoStyle.Render(fmt.Sprintf("%3.f%%", m.viewport.ScrollPercent()*100))
line := strings.Repeat("─", max(0, m.viewport.Width-lipgloss.Width(info)))
return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
}
func max(a, b int) int {
if a > b {
return a
}
return b
}
func (m jview) GetSize() tea.Msg {
return m.lastSize
}

View File

@ -14,6 +14,7 @@ type keyMap struct {
Destroy key.Binding Destroy key.Binding
Place key.Binding Place key.Binding
Craft key.Binding Craft key.Binding
Journal key.Binding
} }
// ShortHelp returns keybindings to be shown in the mini help view. It's part // ShortHelp returns keybindings to be shown in the mini help view. It's part
@ -26,7 +27,7 @@ func (k keyMap) ShortHelp() []key.Binding {
// key.Map interface. // key.Map interface.
func (k keyMap) FullHelp() [][]key.Binding { func (k keyMap) FullHelp() [][]key.Binding {
return [][]key.Binding{ return [][]key.Binding{
{k.Up, k.Down, k.Left, k.Right, k.Gather, k.Place, k.Craft, k.Destroy, k.Pickup, k.Help, k.Quit}, //second column {k.Up, k.Down, k.Left, k.Right, k.Gather, k.Place, k.Craft, k.Destroy, k.Pickup, k.Journal, k.Help, k.Quit}, //second column
} }
} }
@ -75,4 +76,8 @@ var keys = keyMap{
key.WithKeys("c"), key.WithKeys("c"),
key.WithHelp("c", "craft object"), key.WithHelp("c", "craft object"),
), ),
Journal: key.NewBinding(
key.WithKeys("v"),
key.WithHelp("v", "view journal"),
),
} }

View File

@ -14,7 +14,7 @@ func main() {
simulator = sim.NewSimulator() simulator = sim.NewSimulator()
simulator.Start() simulator.Start()
parent := parent{initMainscreen()} parent := parent{initMainscreen()}
if err := tea.NewProgram(parent).Start(); err != nil { if err := tea.NewProgram(parent, tea.WithAltScreen()).Start(); err != nil {
fmt.Printf("Uh oh, there was an error: %v\n", err) fmt.Printf("Uh oh, there was an error: %v\n", err)
os.Exit(1) os.Exit(1)
} }

View File

@ -27,6 +27,7 @@ type model struct {
help help.Model help help.Model
keys keyMap keys keyMap
next string next string
lastSize tea.WindowSizeMsg
} }
type beat struct{} type beat struct{}
@ -71,6 +72,8 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, heartbeat() return m, heartbeat()
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
m.help.Width = msg.Width m.help.Width = msg.Width
m.lastSize = msg
case placeMsg: case placeMsg:
simc := fmt.Sprintf("place %v", string(msg)) simc := fmt.Sprintf("place %v", string(msg))
m.s.Input(simc) m.s.Input(simc)
@ -79,6 +82,12 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
simc := fmt.Sprintf("craft %v", string(msg)) simc := fmt.Sprintf("craft %v", string(msg))
m.s.Input(simc) m.s.Input(simc)
return m, nil return m, nil
case readMsg:
pid, err := strconv.Atoi(string(msg))
if err != nil {
panic(err)
}
return newJView(sim.GlobalPages[pid].Title, sim.GlobalPages[pid].Content), m.GetSize
case tea.KeyMsg: case tea.KeyMsg:
switch { switch {
case key.Matches(msg, m.keys.Help): case key.Matches(msg, m.keys.Help):
@ -109,7 +118,7 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
} }
} }
sort.Sort(res) sort.Sort(res)
return newMenuModel(res, placeMenu), nil return newMenuModel(res, placeMenu), m.GetSize
case key.Matches(msg, m.keys.Pickup): case key.Matches(msg, m.keys.Pickup):
m.s.Input("pickup") m.s.Input("pickup")
case key.Matches(msg, m.keys.Destroy): case key.Matches(msg, m.keys.Destroy):
@ -120,7 +129,14 @@ func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
res = append(res, sim.GlobalItems[k].(sim.ItemEntry)) res = append(res, sim.GlobalItems[k].(sim.ItemEntry))
} }
sort.Sort(res) sort.Sort(res)
return newMenuModel(res, craftMenu), nil return newMenuModel(res, craftMenu), m.GetSize
case key.Matches(msg, m.keys.Journal):
var res pagelist
for k := range m.s.Player.Pages {
res = append(res, sim.GlobalPages[k])
}
sort.Sort(res)
return newJMenuModel(res), m.GetSize
} }
} }
m.input, cmd = m.input.Update(msg) m.input, cmd = m.input.Update(msg)
@ -140,3 +156,7 @@ func (m model) View() string {
} }
return render return render
} }
func (m model) GetSize() tea.Msg {
return m.lastSize
}

11
menu.go
View File

@ -14,6 +14,7 @@ type menutype int
const ( const (
placeMenu menutype = iota placeMenu menutype = iota
craftMenu craftMenu
journalMenu
) )
type item struct { type item struct {
@ -29,6 +30,7 @@ func (i item) FilterValue() string { return i.title }
type menuModel struct { type menuModel struct {
list list.Model list list.Model
kind menutype kind menutype
lastSize tea.WindowSizeMsg
} }
type placeMsg string type placeMsg string
@ -80,15 +82,16 @@ func (p menuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case tea.WindowSizeMsg: case tea.WindowSizeMsg:
h, v := docStyle.GetFrameSize() h, v := docStyle.GetFrameSize()
p.list.SetSize(msg.Width-h, msg.Height-v) p.list.SetSize(msg.Width-h, msg.Height-v)
p.lastSize = msg
return p, nil return p, nil
case tea.KeyMsg: case tea.KeyMsg:
switch keypress := msg.String(); keypress { switch keypress := msg.String(); keypress {
case "ctrl-c": case "ctrl-c":
return p, tea.Quit return p, tea.Quit
case "esc": case "esc":
return initMainscreen(), heartbeat() return initMainscreen(), tea.Batch(p.GetSize, heartbeat())
case "enter": case "enter":
return initMainscreen(), tea.Batch(p.buildMenuMsg, heartbeat()) return initMainscreen(), tea.Batch(p.GetSize, p.buildMenuMsg, heartbeat())
} }
} }
@ -102,3 +105,7 @@ func (p menuModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
func (p menuModel) View() string { func (p menuModel) View() string {
return p.list.View() return p.list.View()
} }
func (p menuModel) GetSize() tea.Msg {
return p.lastSize
}

36
pagelist.go Normal file
View File

@ -0,0 +1,36 @@
package main
import sim "git.saintnet.tech/stryan/spacetea/simulator"
type pagelist []sim.JournalPage
// Len is the number of elements in the collection.
func (e pagelist) Len() int {
return len(e)
}
// Less reports whether the element with index i
// must sort before the element with index j.
//
// If both Less(i, j) and Less(j, i) are false,
// then the elements at index i and j are considered equal.
// Sort may place equal elements in any order in the final result,
// while Stable preserves the original input order of equal elements.
//
// Less must describe a transitive ordering:
// - if both Less(i, j) and Less(j, k) are true, then Less(i, k) must be true as well.
// - if both Less(i, j) and Less(j, k) are false, then Less(i, k) must be false as well.
//
// Note that floating-point comparison (the < operator on float32 or float64 values)
// is not a transitive ordering when not-a-number (NaN) values are involved.
// See Float64Slice.Less for a correct implementation for floating-point values.
func (e pagelist) Less(i int, j int) bool {
return e[i].ID() < e[j].ID()
}
// Swap swaps the elements with indexes i and j.
func (e pagelist) Swap(i int, j int) {
tmp := e[i]
e[i] = e[j]
e[j] = tmp
}

View File

@ -7,7 +7,7 @@ type parent struct {
} }
func (m parent) Init() tea.Cmd { func (m parent) Init() tea.Cmd {
return tea.Batch(m.current.Init(), tea.EnterAltScreen) return tea.Batch(tea.EnterAltScreen, m.current.Init())
} }
func (m parent) Update(msg tea.Msg) (tea.Model, tea.Cmd) { func (m parent) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

38
simulator/journal.go Normal file
View File

@ -0,0 +1,38 @@
package simulator
import (
"strconv"
"github.com/BurntSushi/toml"
)
//PageID is a journal page
type PageID int
//JournalPage is a "flavour event"
type JournalPage struct {
PageID PageID `toml:"pageid"`
Title string `toml:"title"`
Content string `toml:"content"`
Requires []relation `toml:"requires"`
}
func (j JournalPage) ID() string {
return strconv.Itoa(int(j.PageID))
}
//GlobalPages is a list of all pages
var GlobalPages []JournalPage
type pages struct {
JournalPage []JournalPage
}
func loadPages(filename string) {
var res pages
_, err := toml.DecodeFile(filename, &res)
if err != nil {
panic(err)
}
GlobalPages = res.JournalPage
}

View File

@ -11,6 +11,7 @@ type Player struct {
Resources map[itemType]int Resources map[itemType]int
Craftables map[itemType]struct{} Craftables map[itemType]struct{}
Techs map[TechID]Tech Techs map[TechID]Tech
Pages map[PageID]JournalPage
CurrentTile *Tile CurrentTile *Tile
log []string log []string
logIndex int logIndex int
@ -18,7 +19,13 @@ type Player struct {
//NewPlayer initializes a player //NewPlayer initializes a player
func NewPlayer() *Player { func NewPlayer() *Player {
return &Player{Resources: make(map[itemType]int), Techs: make(map[TechID]Tech), Craftables: make(map[itemType]struct{})} p := &Player{
Resources: make(map[itemType]int),
Techs: make(map[TechID]Tech),
Craftables: make(map[itemType]struct{}),
Pages: make(map[PageID]JournalPage),
}
return p
} }
//AddItemByName adds the given amount of the item using the item name //AddItemByName adds the given amount of the item using the item name
@ -100,6 +107,26 @@ func (p *Player) research() {
} }
} }
func (p *Player) journal() {
for _, page := range GlobalPages {
if _, ok := p.Pages[page.PageID]; ok {
continue
}
i := 0
for _, v := range page.Requires {
req := lookupByName(v.Name)
if p.Resources[req.ID()] >= v.Value {
i++
}
}
if i == len(page.Requires) {
p.Pages[page.PageID] = page
p.Announce(fmt.Sprintf("New Journal: %v", page.Title))
}
}
}
//Announce adds an entry to a players log //Announce adds an entry to a players log
func (p *Player) Announce(msg string) { func (p *Player) Announce(msg string) {
p.logIndex++ p.logIndex++

View File

@ -28,12 +28,17 @@ func NewSimulator() *Simulator {
loadResources("data/resources.toml") loadResources("data/resources.toml")
log.Println("loading converters") log.Println("loading converters")
loadConverters("data/converters.toml") loadConverters("data/converters.toml")
log.Println("loading journal")
loadPages("data/journal.toml")
if len(GlobalItems) < 1 { if len(GlobalItems) < 1 {
panic("Loaded items but nothing in global items table") panic("Loaded items but nothing in global items table")
} }
if len(GlobalTechs) < 1 { if len(GlobalTechs) < 1 {
panic("Loaded items but nothing in global items table") panic("Loaded items but nothing in global items table")
} }
if len(GlobalPages) < 1 {
panic("Loaded journal but no pages in table")
}
pod.Place(newResource(lookupByName("tea").ID()), 4, 4) pod.Place(newResource(lookupByName("tea").ID()), 4, 4)
player.AddItem(itemType(1), 30) player.AddItem(itemType(1), 30)
player.AddItem(itemType(3), 5) player.AddItem(itemType(3), 5)
@ -167,6 +172,7 @@ func (s *Simulator) main() {
s.Time = s.Time + 1 s.Time = s.Time + 1
s.Place.Tick() s.Place.Tick()
s.Player.research() s.Player.research()
s.Player.journal()
} }
} }
} }