custom theme with chat widget
This commit is contained in:
@@ -20,6 +20,9 @@ func Run() {
|
||||
func Build() *p.Game {
|
||||
g := p.NewGame("Reggeli Kávé", 320, 200)
|
||||
|
||||
g.ThemeManager.Register(MorningCoffeeTheme)
|
||||
g.UseTheme("morning-coffee")
|
||||
|
||||
for _, a := range Assets {
|
||||
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
|
||||
// arg to put it back.
|
||||
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,
|
||||
// hotspot-only verb coin. The default wheel sits over the bedroom
|
||||
// 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