Files
pncdsl/ui.dialog_box.go
2026-05-25 21:28:17 +02:00

135 lines
3.3 KiB
Go

package pncdsl
import (
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/vector"
)
// DialogBox renders the active conversation: one line at a time (advanced
// on click) followed by the player's choices. The widget is dormant
// unless ctx.Game.dialogueActive() — and while active it consumes input
// so nothing underneath reacts.
type DialogBox struct {
Name string
Bounds Rectangle
LineHeight int
Padding int
}
func (d *DialogBox) GetName() string { return d.Name }
func (d *DialogBox) Tick(ctx *UICtx) {
g := ctx.Game
if !g.dialogueActive() {
return
}
// while a dialog is open, ALL clicks belong to it
if g.Input.RightClicked() {
g.Input.ConsumeRight()
}
if !g.Input.LeftClicked() {
return
}
g.Input.ConsumeLeft()
mp := g.Input.Point()
dlg := g.dialog
if dlg.LineIdx >= 0 && dlg.LineIdx < len(dlg.Node.Lines) {
dlg.LineIdx++
if dlg.LineIdx >= len(dlg.Node.Lines) {
dlg.LineIdx = -1
dlg.ChoiceHits = nil
}
return
}
// pick the clicked choice
for i, r := range dlg.ChoiceHits {
if r.Contains(mp) {
choice := d.resolveChoices(ctx)[i]
if choice.Once {
g.State.NoteTalked(dlg.Node.Name + ":" + choice.Text)
}
if len(choice.Actions) > 0 {
g.queueAction(Seq(choice.Actions...), "choice")
}
return
}
}
}
func (d *DialogBox) resolveChoices(ctx *UICtx) []DialogueChoice {
g := ctx.Game
dlg := g.dialog
out := make([]DialogueChoice, 0, len(dlg.Node.Choices))
for _, c := range dlg.Node.Choices {
gctx := g.makeCtx()
if c.Show != nil && !c.Show.Eval(gctx) {
continue
}
if c.Once && g.State.Talked(dlg.Node.Name+":"+c.Text) > 0 {
continue
}
out = append(out, c)
}
return out
}
// BlocksClickAt — when a dialog is open, every click is ours.
func (d *DialogBox) BlocksClickAt(p Point) bool {
// without ctx we can't know if a dialog is active; conservatively block
// when the cursor is in the dialog bounds (used only by RadialVerbs).
b := d.Bounds
if b.W == 0 {
return false
}
return b.Contains(p)
}
func (d *DialogBox) Draw(dst *ebiten.Image, ctx *UICtx) {
g := ctx.Game
if !g.dialogueActive() {
return
}
th := g.Theme()
b := d.Bounds
if b.W == 0 {
b = Rect(0, 140, float64(g.Width), 60)
}
pad := d.Padding
if pad <= 0 {
pad = 6
}
lh := d.LineHeight
if lh <= 0 {
lh = 14
}
vector.DrawFilledRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), th.DialogBG, false)
vector.StrokeRect(dst, float32(b.X), float32(b.Y), float32(b.W), float32(b.H), 1, th.DialogBorder, false)
dlg := g.dialog
if dlg.LineIdx >= 0 && dlg.LineIdx < len(dlg.Node.Lines) {
ln := dlg.Node.Lines[dlg.LineIdx]
g.DrawText(dst, ln.Speaker+":", int(b.X)+pad, int(b.Y)+2, th.DialogSpeaker)
lines := wrapText(ln.Text, int(b.W)-pad*2)
for i, l := range lines {
g.DrawText(dst, l, int(b.X)+pad, int(b.Y)+18+i*lh, th.DialogText)
}
return
}
choices := d.resolveChoices(ctx)
dlg.ChoiceHits = dlg.ChoiceHits[:0]
mp := g.Input.Point()
for i, c := range choices {
r := Rect(b.X+float64(pad), b.Y+4+float64(i*lh), b.W-float64(pad)*2, float64(lh-1))
dlg.ChoiceHits = append(dlg.ChoiceHits, r)
bg := th.DialogChoiceBG
if r.Contains(mp) {
bg = th.DialogChoiceHover
}
vector.DrawFilledRect(dst, float32(r.X), float32(r.Y), float32(r.W), float32(r.H), bg, false)
g.DrawText(dst, c.Text, int(r.X)+2, int(r.Y)-1, th.DialogText)
}
}