custom theme with chat widget
This commit is contained in:
@@ -20,6 +20,9 @@ func Run() {
|
|||||||
func Build() *p.Game {
|
func Build() *p.Game {
|
||||||
g := p.NewGame("Reggeli Kávé", 320, 200)
|
g := p.NewGame("Reggeli Kávé", 320, 200)
|
||||||
|
|
||||||
|
g.ThemeManager.Register(MorningCoffeeTheme)
|
||||||
|
g.UseTheme("morning-coffee")
|
||||||
|
|
||||||
for _, a := range Assets {
|
for _, a := range Assets {
|
||||||
g.AssetManager.Register(a)
|
g.AssetManager.Register(a)
|
||||||
}
|
}
|
||||||
@@ -45,6 +48,11 @@ func Build() *p.Game {
|
|||||||
// in the corner just gets in the way. Pass "player" as the second
|
// in the corner just gets in the way. Pass "player" as the second
|
||||||
// arg to put it back.
|
// arg to put it back.
|
||||||
p.RegisterRichUI(g, "", "cat")
|
p.RegisterRichUI(g, "", "cat")
|
||||||
|
// Replace the framework's in-game ChatLog with the demo's own
|
||||||
|
// click-and-type GameChat panel — game-independent chatter that
|
||||||
|
// does not belong in the pncdsl library.
|
||||||
|
g.UIManager.Remove("chat")
|
||||||
|
g.UIManager.Register(Chat)
|
||||||
// Swap the rich UI's always-visible verb wheel for a right-click,
|
// Swap the rich UI's always-visible verb wheel for a right-click,
|
||||||
// hotspot-only verb coin. The default wheel sits over the bedroom
|
// hotspot-only verb coin. The default wheel sits over the bedroom
|
||||||
// door, blocking that exit — the coin only appears on a right-click
|
// door, blocking that exit — the coin only appears on a right-click
|
||||||
|
|||||||
74
lib/theme.go
Normal file
74
lib/theme.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package lib
|
||||||
|
|
||||||
|
import (
|
||||||
|
"image/color"
|
||||||
|
|
||||||
|
p "git.teletypegames.org/games/pncdsl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MorningCoffeeTheme is the game-specific palette: warm coffee browns
|
||||||
|
// over deep espresso black, paired with the GameChat colors below so the
|
||||||
|
// stand-alone chat panel reads as part of the same visual set.
|
||||||
|
var MorningCoffeeTheme = p.Theme{
|
||||||
|
Name: "morning-coffee",
|
||||||
|
|
||||||
|
PanelBG: color.RGBA{30, 22, 18, 255},
|
||||||
|
StatusText: color.RGBA{245, 230, 200, 255},
|
||||||
|
FlashText: color.RGBA{255, 200, 110, 255},
|
||||||
|
VerbButtonBG: color.RGBA{60, 40, 30, 255},
|
||||||
|
VerbButtonSelectedBG: color.RGBA{180, 110, 50, 255},
|
||||||
|
VerbButtonText: color.RGBA{255, 240, 215, 255},
|
||||||
|
InventorySlotBG: color.RGBA{45, 32, 25, 255},
|
||||||
|
InventorySlotSelectedBG: color.RGBA{180, 110, 50, 255},
|
||||||
|
SpeechBubbleBG: color.RGBA{20, 14, 10, 215},
|
||||||
|
SpeechDefaultText: color.RGBA{255, 240, 215, 255},
|
||||||
|
DialogBG: color.RGBA{25, 18, 14, 245},
|
||||||
|
DialogBorder: color.RGBA{180, 110, 50, 255},
|
||||||
|
DialogChoiceBG: color.RGBA{45, 30, 22, 255},
|
||||||
|
DialogChoiceHover: color.RGBA{95, 60, 35, 255},
|
||||||
|
DialogSpeaker: color.RGBA{255, 200, 110, 255},
|
||||||
|
DialogText: color.RGBA{255, 240, 215, 255},
|
||||||
|
EndCardBG: color.RGBA{10, 6, 4, 235},
|
||||||
|
EndCardText: color.RGBA{255, 200, 110, 255},
|
||||||
|
CursorColor: color.RGBA{255, 230, 180, 255},
|
||||||
|
HotspotOutline: color.RGBA{255, 200, 110, 200},
|
||||||
|
SceneBackdrop: color.RGBA{20, 14, 10, 255},
|
||||||
|
|
||||||
|
TopBarBG: color.RGBA{18, 12, 8, 255},
|
||||||
|
TopBarText: color.RGBA{255, 230, 200, 255},
|
||||||
|
TopBarAccent: color.RGBA{255, 200, 110, 255},
|
||||||
|
|
||||||
|
// Built-in ChatLog colors stay defined for completeness; the demo
|
||||||
|
// replaces the widget with GameChat, but other rich-UI widgets may
|
||||||
|
// still index these slots.
|
||||||
|
ChatLogBG: color.RGBA{18, 12, 8, 235},
|
||||||
|
ChatLogPrompt: color.RGBA{255, 200, 110, 255},
|
||||||
|
ChatLogResponse: color.RGBA{255, 240, 215, 255},
|
||||||
|
ChatLogSystem: color.RGBA{160, 130, 100, 255},
|
||||||
|
|
||||||
|
CharacterPanelBG: color.RGBA{15, 10, 6, 180},
|
||||||
|
CharacterPanelBorder: color.RGBA{180, 110, 50, 255},
|
||||||
|
CharacterPanelTitle: color.RGBA{255, 200, 110, 255},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chat is the GameChat instance used by the demo. Its colors stay in sync
|
||||||
|
// with MorningCoffeeTheme by reusing the same warm-brown palette.
|
||||||
|
var Chat = &GameChat{
|
||||||
|
Name: "chat",
|
||||||
|
Bounds: p.Rect(2, 148, 280, 38),
|
||||||
|
LineHeight: 9,
|
||||||
|
Padding: 3,
|
||||||
|
InputHeight: 13,
|
||||||
|
|
||||||
|
BG: color.RGBA{18, 12, 8, 235},
|
||||||
|
Border: color.RGBA{120, 80, 40, 255},
|
||||||
|
FocusBorder: color.RGBA{255, 200, 110, 255},
|
||||||
|
InputBG: color.RGBA{28, 18, 12, 240},
|
||||||
|
UserText: color.RGBA{255, 220, 140, 255},
|
||||||
|
BotText: color.RGBA{220, 200, 170, 255},
|
||||||
|
PromptText: color.RGBA{255, 240, 215, 255},
|
||||||
|
CursorColor: color.RGBA{255, 230, 180, 255},
|
||||||
|
|
||||||
|
UserPrefix: "Te: ",
|
||||||
|
BotPrefix: "Cirmos: ",
|
||||||
|
}
|
||||||
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