Kistaro Windrider
bcfd42970b
Implement a stats panel with a name, intro text, and a rule for how to pull stats out of a collection.
168 lines
5.1 KiB
Go
168 lines
5.1 KiB
Go
package cardsim
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// An InfoPanel displays some set of stats to the player. It does
|
|
// not consume an action. It must not advance the state of the game
|
|
// in any way.
|
|
type InfoPanel[C StatsCollection] interface {
|
|
// Title returns the title of this InfoPanel, which is also used as the
|
|
// label presented to the player to access this panel.
|
|
Title(p *Player[C]) (Message, error)
|
|
|
|
// Info returns the displayable contents of this InfoPanel. A nil Message
|
|
// in the output is interpreted as a paragraph break.
|
|
Info(p *Player[C]) ([]Message, error)
|
|
}
|
|
|
|
// A StatFilter decides whether to show a specific stat in a BasicStatsPanel
|
|
// (and maybe other kinds of stats panels, if they choose to support this).
|
|
type StatFilter[C StatsCollection] func(p *Player[C], s Stat) bool
|
|
|
|
// BasicStatsPanel shows some or all of the stats output by C, under
|
|
// a fixed name, introduced by a specific prompt. Stats are shown as a two
|
|
// column table with the name, then the value.
|
|
type BasicStatsPanel[C StatsCollection] struct {
|
|
// Name stores the name of this stats panel, which is also shown in the menu.
|
|
Name Message
|
|
|
|
// Intro stores a message to always display before the stats. Optional.
|
|
Intro Message
|
|
|
|
// Filter stores a function to decide what stats to show. If this is not
|
|
// provided, the BasicStatsPanel uses VisibleOrDebug by default.
|
|
Filter StatFilter[C]
|
|
}
|
|
|
|
// VisibleOrDebug returns whether s is Visible or p is in debug mode,
|
|
// so a debug-mode player shows all stats.
|
|
func VisibleOrDebug[C StatsCollection](p *Player[C], s Stat) bool {
|
|
return p.DebugLevel > 0 || s.Visible()
|
|
}
|
|
|
|
// Title implements `InfoPanel[C]` by returning b's `Name`.
|
|
func (b *BasicStatsPanel[C]) Title(p *Player[C]) (Message, error) {
|
|
return b.Name, nil
|
|
}
|
|
|
|
// Info implements `InfoPanel[C]` by dumpiing p.Stats, showing those items for
|
|
// whch b.Filter returns true.
|
|
func (b *BasicStatsPanel[C]) Info(p *Player[C]) ([]Message, error) {
|
|
stats := p.Stats.Stats()
|
|
cached := make([]cachedStat, 0, len(stats))
|
|
longestName := 0
|
|
filter := b.Filter
|
|
if filter == nil {
|
|
filter = VisibleOrDebug[C]
|
|
}
|
|
|
|
for _, s := range stats {
|
|
if !filter(p, s) {
|
|
continue
|
|
}
|
|
name := s.StatName()
|
|
if len(name) > longestName {
|
|
longestName = len(name)
|
|
}
|
|
cached = append(cached, cachedStat{name, s.String(), s.Visible()})
|
|
}
|
|
|
|
if len(cached) == 0 {
|
|
return []Message{b.Intro, nil, MsgStr("No stats available")}, nil
|
|
}
|
|
|
|
ret := make([]Message, 0, 2+len(cached))
|
|
ret = append(ret, b.Intro, nil)
|
|
for _, s := range cached {
|
|
ret = append(ret, MsgStr(s.output(longestName)))
|
|
}
|
|
return ret, nil
|
|
}
|
|
|
|
// cachedStat is an implementation detail of BasicStatsPanel.Info. It stores the
|
|
// values out of a stat so they do not need to be recalculated, in case they
|
|
// are expensive to calculate or the filter deciding which stats to output
|
|
// was expensive.
|
|
type cachedStat struct {
|
|
name string
|
|
value string
|
|
visible bool
|
|
}
|
|
|
|
// output returns a string representing this stat, right-aligning the name in
|
|
// a nameWidth-wide field and prefixing it with a bullet: "•" for a visible
|
|
// stat and "◦" for an invisible stat. If nameWidth is 0, the name and
|
|
// colon are omitted. If it is negative, the name is emitted as-is with
|
|
// no alignment. If it is too short for the name and it is nonzero,
|
|
// it is truncated with "⋯".
|
|
func (c cachedStat) output(nameWidth int) string {
|
|
bullet := "◦"
|
|
if c.visible {
|
|
bullet = "•"
|
|
}
|
|
|
|
if nameWidth == 0 {
|
|
return fmt.Sprintf("%s %s", bullet, c.value)
|
|
}
|
|
|
|
name := c.name
|
|
if len(name) < nameWidth {
|
|
name = strings.Repeat(" ", nameWidth-len(name)) + name
|
|
}
|
|
if len(name) > nameWidth && nameWidth > 0 {
|
|
name = name[:nameWidth-1] + "⋯"
|
|
}
|
|
|
|
return fmt.Sprintf("%s %s: %s", bullet, name, c.value)
|
|
}
|
|
|
|
// StatsNamed returns a StatFilter[C] matching any stat with a listed name.
|
|
func StatsNamed[C StatsCollection](names ...string) StatFilter[C] {
|
|
nameSet := make(map[string]bool)
|
|
for _, n := range names {
|
|
nameSet[n] = true
|
|
}
|
|
return func(_ *Player[C], s Stat) bool {
|
|
return nameSet[s.StatName()]
|
|
}
|
|
}
|
|
|
|
// VisibleOrDebugStatsNamed returns a StatFilter[C] matching any visible stat
|
|
// with a listed name, or any stat with a listed name if the player is in
|
|
// debug mode.
|
|
func VisibleOrDebugStatsNamed[C StatsCollection](names ...string) StatFilter[C] {
|
|
return All(VisibleOrDebug[C], StatsNamed[C](names...))
|
|
}
|
|
|
|
// All returns a StatFilter[C] that requires a Stat to match all provided
|
|
// filters. If no filters are provided, All matches every Stat (it's very easy
|
|
// to meet every requirement when there are no requirements).
|
|
func All[C StatsCollection](ff ...StatFilter[C]) StatFilter[C] {
|
|
return func(p *Player[C], s Stat) bool {
|
|
for _, f := range ff {
|
|
if !f(p, s) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
// Any returns a StatFilter[C] that requires a Stat to match any one or more of
|
|
// the filters provided. If no filters are provided, Any never matches a stat
|
|
// (it's very hard to meet at least one requirement out when there are no
|
|
// requirements).
|
|
func Any[C StatsCollection](ff ...StatFilter[C]) StatFilter[C] {
|
|
return func(p *Player[C], s Stat) bool {
|
|
for _, f := range ff {
|
|
if f(p, s) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|