package pncdsl import ( "image/color" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/vector" ) // Layout constants in the internal 320×200 coordinate space. const ( uiSceneTop = 0 uiSceneBottom = 140 uiStatusY = 142 uiPanelY = 152 uiPanelH = 48 uiVerbsX = 4 uiVerbsW = 120 uiInvX = 132 uiInvW = 184 uiInvSlotW = 22 ) // UI is the per-game UI state. It's a simple bag of fields the engine // writes into and the renderer reads. No event/signal layer — the engine // drives everything top-down each tick. type UI struct { g *Game hoverLabel string flash string flashTimer float64 speech *speechBubble dialog *dialogBox endCard string verbButtons []verbButton } func newUI(g *Game) *UI { return &UI{ g: g, speech: &speechBubble{}, } } func (u *UI) rebuildVerbButtons() { verbs := u.g.VerbManager.Names() u.verbButtons = u.verbButtons[:0] cols := 2 bw := uiVerbsW / cols bh := 14 for i, name := range verbs { col := i % cols row := i / cols x := uiVerbsX + col*bw y := uiPanelY + row*bh u.verbButtons = append(u.verbButtons, verbButton{ Name: name, Label: u.g.VerbManager.MustGet(name).Label, Bounds: Rect(float64(x), float64(y), float64(bw-2), float64(bh-2)), }) } } type verbButton struct { Name string Label string Bounds Rectangle } // SetSpeech / ClearSpeech are called from the Say action. func (u *UI) SetSpeech(speaker, text string) { u.speech.set(u.g, speaker, text) } func (u *UI) ClearSpeech() { u.speech.clear() } // FlashLine shows a short status string (e.g. "Ehhez kell a kulcs.") for ~2s. func (u *UI) FlashLine(text string) { u.flash = text u.flashTimer = 2.0 } func (u *UI) tick(dt float64) { if u.flashTimer > 0 { u.flashTimer -= dt if u.flashTimer <= 0 { u.flash = "" } } u.speech.tick(dt) } // drawHUD paints the bottom UI panel (status line + verbs + inventory). // Dialog box, if active, is drawn separately by the engine on top. func (u *UI) drawHUD(dst *ebiten.Image) { // status / hover / flash line status := u.flash if status == "" { status = u.composedHoverLabel() } if status != "" { w := textWidth(status) drawText(dst, status, (320-w)/2, uiStatusY, color.White) } // panel background vector.DrawFilledRect(dst, 0, float32(uiPanelY-2), 320, float32(uiPanelH+2), color.RGBA{20, 20, 30, 255}, false) // verb buttons for _, b := range u.verbButtons { bg := color.RGBA{50, 50, 70, 255} if b.Name == u.g.selectedVerb { bg = color.RGBA{120, 90, 40, 255} } vector.DrawFilledRect(dst, float32(b.Bounds.X), float32(b.Bounds.Y), float32(b.Bounds.W), float32(b.Bounds.H), bg, false) drawText(dst, b.Label, int(b.Bounds.X)+2, int(b.Bounds.Y)-1, color.White) } // inventory slots items := u.g.Inventory.Items() for k := 0; k < 8; k++ { x := uiInvX + k*uiInvSlotW y := uiPanelY bg := color.RGBA{40, 40, 50, 255} if k < len(items) && items[k] == u.g.Inventory.Selected() { bg = color.RGBA{120, 90, 40, 255} } vector.DrawFilledRect(dst, float32(x), float32(y), float32(uiInvSlotW-2), float32(uiInvSlotW-2), bg, false) if k < len(items) { it, ok := u.g.ItemManager.Get(items[k]) if ok { img := u.g.loaded.image(u.g.AssetManager, it.Sprite) op := &ebiten.DrawImageOptions{} sw, sh := img.Bounds().Dx(), img.Bounds().Dy() if sw == 0 || sh == 0 { continue } slotPx := float64(uiInvSlotW - 4) sx := slotPx / float64(sw) sy := slotPx / float64(sh) op.GeoM.Scale(sx, sy) op.GeoM.Translate(float64(x+1), float64(y+1)) dst.DrawImage(img, op) } } } } func (u *UI) composedHoverLabel() string { verb := "" if v, ok := u.g.VerbManager.Get(u.g.selectedVerb); ok { verb = v.Label } target := u.hoverLabel if sel := u.g.Inventory.Selected(); sel != "" { if target != "" { return verb + " " + sel + " ezen: " + target } return verb + " " + sel } if target == "" { return "" } return verb + " " + target } // hitTestUI returns the click target (verbButton, inventory slot index) or "". // "verb:", "inv:", "" for none. func (u *UI) hitTest(p Point) string { for _, b := range u.verbButtons { if b.Bounds.Contains(p) { return "verb:" + b.Name } } items := u.g.Inventory.Items() for k := 0; k < len(items); k++ { x := uiInvX + k*uiInvSlotW r := Rect(float64(x), float64(uiPanelY), float64(uiInvSlotW-2), float64(uiInvSlotW-2)) if r.Contains(p) { return "inv:" + items[k] } } return "" }