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) } }