custom theme with chat widget
This commit is contained in:
244
lib/widget.gamechat.go
Normal file
244
lib/widget.gamechat.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package lib
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"math/rand"
|
||||
"strings"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||
|
||||
p "git.teletypegames.org/games/pncdsl"
|
||||
)
|
||||
|
||||
// GameChat is a stand-alone chat panel: click to focus, type a message,
|
||||
// press Enter to send, and the widget answers with a random canned line.
|
||||
// It does not talk to any game system — the in-game action log stays
|
||||
// decoupled from this gameplay-flavoured chatter, so the widget can be
|
||||
// reused across games without dragging the demo's content with it.
|
||||
type GameChat struct {
|
||||
Name string
|
||||
Bounds p.Rectangle
|
||||
LineHeight int
|
||||
Padding int
|
||||
InputHeight int
|
||||
|
||||
BG color.Color
|
||||
Border color.Color
|
||||
FocusBorder color.Color
|
||||
InputBG color.Color
|
||||
UserText color.Color
|
||||
BotText color.Color
|
||||
PromptText color.Color
|
||||
CursorColor color.Color
|
||||
|
||||
UserPrefix string
|
||||
BotPrefix string
|
||||
Replies []string
|
||||
|
||||
history []chatEntry
|
||||
buffer []rune
|
||||
focused bool
|
||||
blink float64
|
||||
scratch []rune
|
||||
}
|
||||
|
||||
type chatEntry struct {
|
||||
text string
|
||||
self bool
|
||||
}
|
||||
|
||||
func (c *GameChat) GetName() string { return c.Name }
|
||||
|
||||
func (c *GameChat) Tick(ctx *p.UICtx) {
|
||||
in := ctx.Game.Input
|
||||
c.blink += ctx.DT
|
||||
|
||||
if in.LeftClicked() {
|
||||
if c.Bounds.Contains(in.Point()) {
|
||||
c.focused = true
|
||||
in.ConsumeLeft()
|
||||
} else {
|
||||
c.focused = false
|
||||
}
|
||||
}
|
||||
if !c.focused {
|
||||
return
|
||||
}
|
||||
|
||||
c.scratch = ebiten.AppendInputChars(c.scratch[:0])
|
||||
if len(c.scratch) > 0 {
|
||||
c.buffer = append(c.buffer, c.scratch...)
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyBackspace) && len(c.buffer) > 0 {
|
||||
c.buffer = c.buffer[:len(c.buffer)-1]
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEscape) {
|
||||
c.focused = false
|
||||
}
|
||||
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) || inpututil.IsKeyJustPressed(ebiten.KeyNumpadEnter) {
|
||||
text := strings.TrimSpace(string(c.buffer))
|
||||
c.buffer = c.buffer[:0]
|
||||
if text == "" {
|
||||
return
|
||||
}
|
||||
c.history = append(c.history, chatEntry{text: text, self: true})
|
||||
c.history = append(c.history, chatEntry{text: c.randomReply(), self: false})
|
||||
}
|
||||
}
|
||||
|
||||
func (c *GameChat) randomReply() string {
|
||||
src := c.Replies
|
||||
if len(src) == 0 {
|
||||
src = defaultChatReplies
|
||||
}
|
||||
return src[rand.Intn(len(src))]
|
||||
}
|
||||
|
||||
var defaultChatReplies = []string{
|
||||
"Hmm…",
|
||||
"Aha.",
|
||||
"Mióóóu.",
|
||||
"Komolyan?",
|
||||
"Talán.",
|
||||
"Igen, igen.",
|
||||
"Hagyj békén, álmos vagyok.",
|
||||
"Most főzöm a kávét.",
|
||||
"Persze, persze.",
|
||||
}
|
||||
|
||||
func (c *GameChat) Draw(dst *ebiten.Image, ctx *p.UICtx) {
|
||||
g := ctx.Game
|
||||
b := c.Bounds
|
||||
if b.W <= 0 || b.H <= 0 {
|
||||
return
|
||||
}
|
||||
lh := c.LineHeight
|
||||
if lh <= 0 {
|
||||
lh = 10
|
||||
}
|
||||
pad := c.Padding
|
||||
if pad <= 0 {
|
||||
pad = 3
|
||||
}
|
||||
inputH := c.InputHeight
|
||||
if inputH <= 0 {
|
||||
inputH = lh + 4
|
||||
}
|
||||
userPrefix := c.UserPrefix
|
||||
if userPrefix == "" {
|
||||
userPrefix = "Te: "
|
||||
}
|
||||
botPrefix := c.BotPrefix
|
||||
if botPrefix == "" {
|
||||
botPrefix = "Cirmos: "
|
||||
}
|
||||
|
||||
historyH := int(b.H) - inputH
|
||||
if historyH < lh {
|
||||
historyH = lh
|
||||
}
|
||||
|
||||
vector.DrawFilledRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), c.BG, false)
|
||||
|
||||
wrapW := int(b.W) - pad*2
|
||||
type line struct {
|
||||
text string
|
||||
col color.Color
|
||||
}
|
||||
var rendered []line
|
||||
for _, m := range c.history {
|
||||
col := c.BotText
|
||||
prefix := botPrefix
|
||||
if m.self {
|
||||
col = c.UserText
|
||||
prefix = userPrefix
|
||||
}
|
||||
for _, w := range chatWrap(prefix+m.text, wrapW) {
|
||||
rendered = append(rendered, line{text: w, col: col})
|
||||
}
|
||||
}
|
||||
maxLines := (historyH - pad*2) / lh
|
||||
if maxLines < 0 {
|
||||
maxLines = 0
|
||||
}
|
||||
start := len(rendered) - maxLines
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
for i, ln := range rendered[start:] {
|
||||
y := int(b.Y) + pad + i*lh
|
||||
g.DrawText(dst, ln.text, int(b.X)+pad, y-1, ln.col)
|
||||
}
|
||||
|
||||
inY := int(b.Y) + historyH
|
||||
vector.DrawFilledRect(dst, float32(b.X)+1, float32(inY)+1, float32(int(b.W)-2), float32(inputH-2), c.InputBG, false)
|
||||
txt := "> " + string(c.buffer)
|
||||
g.DrawText(dst, txt, int(b.X)+pad, inY+pad-1, c.PromptText)
|
||||
if c.focused && int(c.blink*2)%2 == 0 {
|
||||
cx := int(b.X) + pad + chatTextWidth(txt)
|
||||
cy := inY + pad - 1
|
||||
vector.DrawFilledRect(dst, float32(cx), float32(cy), 1, float32(lh-1), c.CursorColor, false)
|
||||
}
|
||||
|
||||
border := c.Border
|
||||
if c.focused {
|
||||
border = c.FocusBorder
|
||||
}
|
||||
vector.StrokeRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), 1, border, false)
|
||||
vector.DrawFilledRect(dst, float32(b.X), float32(inY), float32(b.W), 1, border, false)
|
||||
}
|
||||
|
||||
// BlocksClickAt implements the pncdsl clickBlocker contract so the
|
||||
// verb-coin doesn't pop up over the chat panel.
|
||||
func (c *GameChat) BlocksClickAt(pt p.Point) bool { return c.Bounds.Contains(pt) }
|
||||
|
||||
// chatTextWidth mirrors the pncdsl text width estimate for the bundled
|
||||
// debug font (~6 px / glyph). Kept private to the demo.
|
||||
func chatTextWidth(s string) int { return len(s) * 6 }
|
||||
|
||||
func chatWrap(s string, maxPx int) []string {
|
||||
if maxPx <= 0 || chatTextWidth(s) <= maxPx {
|
||||
return []string{s}
|
||||
}
|
||||
var out []string
|
||||
var line string
|
||||
flush := func() {
|
||||
if line != "" {
|
||||
out = append(out, line)
|
||||
line = ""
|
||||
}
|
||||
}
|
||||
word := ""
|
||||
for _, r := range s {
|
||||
if r == ' ' || r == '\n' {
|
||||
if line == "" {
|
||||
line = word
|
||||
} else if chatTextWidth(line+" "+word) <= maxPx {
|
||||
line += " " + word
|
||||
} else {
|
||||
flush()
|
||||
line = word
|
||||
}
|
||||
word = ""
|
||||
if r == '\n' {
|
||||
flush()
|
||||
}
|
||||
continue
|
||||
}
|
||||
word += string(r)
|
||||
}
|
||||
if word != "" {
|
||||
if line == "" {
|
||||
line = word
|
||||
} else if chatTextWidth(line+" "+word) <= maxPx {
|
||||
line += " " + word
|
||||
} else {
|
||||
flush()
|
||||
line = word
|
||||
}
|
||||
}
|
||||
flush()
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user