radial fix

This commit is contained in:
2026-05-25 23:33:46 +02:00
parent eeb6a5e827
commit 5ed7ac4ed9
3 changed files with 81 additions and 21 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -25,6 +25,12 @@ 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.
@@ -33,6 +39,7 @@ type RadialVerbs struct {
// runtime
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
}