From 5ed7ac4ed9dc120465366558de605d6173932e2d Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Mon, 25 May 2026 23:33:46 +0200 Subject: [PATCH] radial fix --- core.engine.go | 34 ++++++++++++++++---------------- core.game.go | 19 ++++++++++++++++++ ui.verb_radial.go | 49 ++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 81 insertions(+), 21 deletions(-) diff --git a/core.engine.go b/core.engine.go index fe0d310..867c676 100644 --- a/core.engine.go +++ b/core.engine.go @@ -99,6 +99,17 @@ func (e *engine) handleSceneInput() { } g.Input.ConsumeLeft() h := e.hotspotAt(mp) + if h == nil { + return + } + g.invokeHotspotVerb(h, g.SelectedVerb()) +} + +// invokeHotspotVerb runs the action bound to verb on hotspot h, honoring +// the SCUMM use-with-item flow when an inventory item is selected. +// Shared by left-click hotspot resolution and the verb-coin widget so +// both paths log, flash and queue identically. +func (g *Game) invokeHotspotVerb(h *Hotspot, verb string) { if h == nil { return } @@ -128,9 +139,9 @@ func (e *engine) handleSceneInput() { g.Inventory.Select("") return } - a := h.handler(g.SelectedVerb()) + a := h.handler(verb) if a == nil { - if v, ok := g.VerbManager.Get(g.SelectedVerb()); ok && v.Default != nil { + if v, ok := g.VerbManager.Get(verb); ok && v.Default != nil { a = v.Default } } @@ -138,8 +149,8 @@ func (e *engine) handleSceneInput() { g.FlashLine("Semmi említésre méltó.") return } - g.LogAction("> " + verbLabel(g, g.SelectedVerb()) + " " + targetLabel) - g.queueAction(a, "hotspot "+g.SelectedVerb()+" "+h.Name) + g.LogAction("> " + verbLabel(g, verb) + " " + targetLabel) + g.queueAction(a, "hotspot "+verb+" "+h.Name) } func verbLabel(g *Game, name string) string { @@ -149,20 +160,7 @@ func verbLabel(g *Game, name string) string { return name } -func (e *engine) hotspotAt(p Point) *Hotspot { - g := e.g - if g.currentScene == "" { - return nil - } - s := g.SceneManager.MustGet(g.currentScene) - for i := range s.Hotspots { - h := &s.Hotspots[i] - if h.Area != nil && h.Area.Contains(p) { - return h - } - } - return nil -} +func (e *engine) hotspotAt(p Point) *Hotspot { return e.g.HotspotAt(p) } func (e *engine) Draw(screen *ebiten.Image) { g := e.g diff --git a/core.game.go b/core.game.go index bc9df78..a1d4ff9 100644 --- a/core.game.go +++ b/core.game.go @@ -185,6 +185,25 @@ func (g *Game) appendLog(m LogMessage) { // Messages returns the current log buffer (read-only). func (g *Game) Messages() []LogMessage { return g.messages } +// HotspotAt returns the topmost hotspot in the current scene whose Area +// contains p, or nil if none. Exposed so widgets (e.g., a verb coin +// that only opens over hotspots) can do their own hit-tests in their +// Tick — the engine's own resolution runs only after every widget had a +// chance, so widgets can't rely on it. +func (g *Game) HotspotAt(p Point) *Hotspot { + if g.currentScene == "" { + return nil + } + s := g.SceneManager.MustGet(g.currentScene) + for i := range s.Hotspots { + h := &s.Hotspots[i] + if h.Area != nil && h.Area.Contains(p) { + return h + } + } + return nil +} + // CharacterInScene reports whether a registered character is listed as // an actor in the current scene. Used by the CharacterPanel widget so // it auto-hides when the character isn't in view. diff --git a/ui.verb_radial.go b/ui.verb_radial.go index 9cde13f..ba28950 100644 --- a/ui.verb_radial.go +++ b/ui.verb_radial.go @@ -25,14 +25,21 @@ type RadialVerbs struct { AlwaysVisible bool Center Point + // HotspotOnly (trigger mode only) makes the wheel open only when the + // trigger lands on a registered hotspot in the current scene. Right- + // clicks on empty space fall through to handleSceneInput (so the + // SCUMM "reset verb to look" behaviour still works there). + HotspotOnly bool + // Labels overrides the rendered verb label per verb Name. Useful when // the regular Verb.Label is too long to fit inside the wheel — e.g. // {"use": "Tedd", "take": "Vedd"}. Falls back to Verb.Label. Labels map[string]string // runtime - visible bool - center Point + visible bool + center Point + pendingHotspot *Hotspot // remembered hotspot when HotspotOnly opens the coin } func (r *RadialVerbs) GetName() string { return r.Name } @@ -64,6 +71,16 @@ func (r *RadialVerbs) Tick(ctx *UICtx) { return } mp := g.Input.Point() + // HotspotOnly: only open when the trigger lands on a hotspot. + // Empty-space clicks fall through (so the scene's right-click + // "reset verb" behaviour still fires from handleSceneInput). + var pending *Hotspot + if r.HotspotOnly { + pending = g.HotspotAt(mp) + if pending == nil { + return + } + } // Don't open over a widget that would also claim this click — // e.g. an InventoryBar. Each widget can declare its dead-zone // via the optional clickBlocker interface. @@ -73,12 +90,14 @@ func (r *RadialVerbs) Tick(ctx *UICtx) { r.consumeTrigger(g) r.center = mp r.visible = true + r.pendingHotspot = pending return } // modal: swallow secondary clicks (close without picking) if r.triggerPressed(g) { r.consumeTrigger(g) r.visible = false + r.pendingHotspot = nil return } if !g.Input.LeftClicked() { @@ -88,9 +107,18 @@ func (r *RadialVerbs) Tick(ctx *UICtx) { idx := r.hitTest(g, g.Input.Point()) if idx >= 0 { names := g.VerbManager.Names() - g.SetSelectedVerb(names[idx]) + verb := names[idx] + g.SetSelectedVerb(verb) + // Verb-coin behaviour: when the coin was opened over a hotspot + // (HotspotOnly mode), picking a slice immediately fires that + // verb's action on that hotspot — one round-trip per command, + // instead of forcing a follow-up left-click on the same target. + if r.pendingHotspot != nil { + g.invokeHotspotVerb(r.pendingHotspot, verb) + } } r.visible = false + r.pendingHotspot = nil } func (r *RadialVerbs) triggerPressed(g *Game) bool { @@ -117,6 +145,21 @@ func (r *RadialVerbs) blockedAt(ctx *UICtx, p Point) bool { continue } w := ctx.Game.UIManager.MustGet(name) + // DialogBox advertises its bounds via clickBlocker even when no + // dialog is open, so the verb-coin would refuse to pop up over + // scenery that happens to sit under the dialog rect. Skip the + // dialog when it's actually inactive. + if _, isDialog := w.(*DialogBox); isDialog && !ctx.Game.dialogueActive() { + continue + } + // CharacterPanel auto-hides when its character isn't in the + // current scene; treat the hidden panel as transparent so the + // coin can open over the empty space it would occupy. + if cp, isCP := w.(*CharacterPanel); isCP { + if cp.Character == "" || !ctx.Game.CharacterInScene(cp.Character) { + continue + } + } if b, ok := w.(clickBlocker); ok && b.BlocksClickAt(p) { return true }